diff --git a/api/bolt/migrator/migrate_dbversion30.go b/api/bolt/migrator/migrate_dbversion29.go
similarity index 64%
rename from api/bolt/migrator/migrate_dbversion30.go
rename to api/bolt/migrator/migrate_dbversion29.go
index 77b9023c1..0ef7546d2 100644
--- a/api/bolt/migrator/migrate_dbversion30.go
+++ b/api/bolt/migrator/migrate_dbversion29.go
@@ -1,13 +1,14 @@
package migrator
-func (m *Migrator) migrateDBVersionTo30() error {
- if err := m.migrateSettings(); err != nil {
+func (m *Migrator) migrateDBVersionToDB30() error {
+ if err := m.migrateSettingsToDB30(); err != nil {
return err
}
+
return nil
}
-func (m *Migrator) migrateSettings() error {
+func (m *Migrator) migrateSettingsToDB30() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
diff --git a/api/bolt/migrator/migrate_dbversion30_test.go b/api/bolt/migrator/migrate_dbversion29_test.go
similarity index 98%
rename from api/bolt/migrator/migrate_dbversion30_test.go
rename to api/bolt/migrator/migrate_dbversion29_test.go
index 42f09b4ab..707fe8796 100644
--- a/api/bolt/migrator/migrate_dbversion30_test.go
+++ b/api/bolt/migrator/migrate_dbversion29_test.go
@@ -76,7 +76,7 @@ func TestMigrateSettings(t *testing.T) {
db: dbConn,
settingsService: settingsService,
}
- if err := m.migrateSettings(); err != nil {
+ if err := m.migrateSettingsToDB30(); err != nil {
t.Errorf("failed to update settings: %v", err)
}
updatedSettings, err := m.settingsService.Settings()
diff --git a/api/bolt/migrator/migrate_dbversion32.go b/api/bolt/migrator/migrate_dbversion31.go
similarity index 52%
rename from api/bolt/migrator/migrate_dbversion32.go
rename to api/bolt/migrator/migrate_dbversion31.go
index 3d800bd36..b6dcfa7ee 100644
--- a/api/bolt/migrator/migrate_dbversion32.go
+++ b/api/bolt/migrator/migrate_dbversion31.go
@@ -1,11 +1,15 @@
package migrator
import (
+ "fmt"
+
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
+ endpointutils "github.com/portainer/portainer/api/internal/endpoint"
+ snapshotutils "github.com/portainer/portainer/api/internal/snapshot"
)
-func (m *Migrator) migrateDBVersionTo32() error {
+func (m *Migrator) migrateDBVersionToDB32() error {
err := m.updateRegistriesToDB32()
if err != nil {
return err
@@ -16,6 +20,10 @@ func (m *Migrator) migrateDBVersionTo32() error {
return err
}
+ if err := m.updateVolumeResourceControlToDB32(); err != nil {
+ return err
+ }
+
return nil
}
@@ -122,3 +130,84 @@ func (m *Migrator) updateDockerhubToDB32() error {
return m.registryService.CreateRegistry(registry)
}
+
+func (m *Migrator) updateVolumeResourceControlToDB32() error {
+ endpoints, err := m.endpointService.Endpoints()
+ if err != nil {
+ return fmt.Errorf("failed fetching endpoints: %w", err)
+ }
+
+ resourceControls, err := m.resourceControlService.ResourceControls()
+ if err != nil {
+ return fmt.Errorf("failed fetching resource controls: %w", err)
+ }
+
+ toUpdate := map[portainer.ResourceControlID]string{}
+ volumeResourceControls := map[string]*portainer.ResourceControl{}
+
+ for i := range resourceControls {
+ resourceControl := resourceControls[i]
+ if resourceControl.Type == portainer.VolumeResourceControl {
+ volumeResourceControls[resourceControl.ResourceID] = &resourceControl
+ }
+ }
+
+ for _, endpoint := range endpoints {
+ if !endpointutils.IsDockerEndpoint(&endpoint) {
+ continue
+ }
+
+ totalSnapshots := len(endpoint.Snapshots)
+ if totalSnapshots == 0 {
+ continue
+ }
+
+ snapshot := endpoint.Snapshots[totalSnapshots-1]
+
+ endpointDockerID, err := snapshotutils.FetchDockerID(snapshot)
+ if err != nil {
+ return fmt.Errorf("failed fetching endpoint docker id: %w", err)
+ }
+
+ if volumesData, done := snapshot.SnapshotRaw.Volumes.(map[string]interface{}); done {
+ if volumesData["Volumes"] == nil {
+ continue
+ }
+
+ findResourcesToUpdateForDB32(endpointDockerID, volumesData, toUpdate, volumeResourceControls)
+ }
+ }
+
+ for _, resourceControl := range volumeResourceControls {
+ if newResourceID, ok := toUpdate[resourceControl.ID]; ok {
+ resourceControl.ResourceID = newResourceID
+ err := m.resourceControlService.UpdateResourceControl(resourceControl.ID, resourceControl)
+ if err != nil {
+ return fmt.Errorf("failed updating resource control %d: %w", resourceControl.ID, err)
+ }
+
+ } else {
+ err := m.resourceControlService.DeleteResourceControl(resourceControl.ID)
+ if err != nil {
+ return fmt.Errorf("failed deleting resource control %d: %w", resourceControl.ID, err)
+ }
+
+ }
+ }
+
+ return nil
+}
+
+func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interface{}, toUpdate map[portainer.ResourceControlID]string, volumeResourceControls map[string]*portainer.ResourceControl) {
+ volumes := volumesData["Volumes"].([]interface{})
+ for _, volumeMeta := range volumes {
+ volume := volumeMeta.(map[string]interface{})
+ volumeName := volume["Name"].(string)
+ oldResourceID := fmt.Sprintf("%s%s", volumeName, volume["CreatedAt"].(string))
+ resourceControl, ok := volumeResourceControls[oldResourceID]
+
+ if ok {
+ toUpdate[resourceControl.ID] = fmt.Sprintf("%s_%s", volumeName, dockerID)
+ }
+ }
+}
diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go
index daae8b184..fc00578f0 100644
--- a/api/bolt/migrator/migrator.go
+++ b/api/bolt/migrator/migrator.go
@@ -367,7 +367,7 @@ func (m *Migrator) Migrate() error {
// Portainer 2.6.0
if m.currentDBVersion < 30 {
- err := m.migrateDBVersionTo30()
+ err := m.migrateDBVersionToDB30()
if err != nil {
return err
}
@@ -375,7 +375,7 @@ func (m *Migrator) Migrate() error {
// Portainer 2.9.0
if m.currentDBVersion < 32 {
- err := m.migrateDBVersionTo32()
+ err := m.migrateDBVersionToDB32()
if err != nil {
return err
}
diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go
index 501a521fa..98c755d37 100644
--- a/api/http/proxy/factory/docker/transport.go
+++ b/api/http/proxy/factory/docker/transport.go
@@ -161,9 +161,7 @@ func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response,
volumeName := volumeIDParameter[0]
- agentTargetHeader := r.Header.Get(portainer.PortainerAgentTargetHeader)
-
- resourceID, err := transport.getVolumeResourceID(agentTargetHeader, volumeName)
+ resourceID, err := transport.getVolumeResourceID(volumeName)
if err != nil {
return nil, err
}
@@ -300,7 +298,7 @@ func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Re
func (transport *Transport) proxyVolumeRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/volumes/create":
- return transport.decorateVolumeResourceCreationOperation(request, volumeObjectIdentifier, portainer.VolumeResourceControl)
+ return transport.decorateVolumeResourceCreationOperation(request, portainer.VolumeResourceControl)
case "/volumes/prune":
return transport.administratorOperation(request)
diff --git a/api/http/proxy/factory/docker/volumes.go b/api/http/proxy/factory/docker/volumes.go
index 2c0b304f7..a09788c9f 100644
--- a/api/http/proxy/factory/docker/volumes.go
+++ b/api/http/proxy/factory/docker/volumes.go
@@ -3,6 +3,7 @@ package docker
import (
"context"
"errors"
+ "fmt"
"net/http"
"path"
@@ -12,10 +13,11 @@ import (
"github.com/portainer/portainer/api/http/proxy/factory/utils"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
+ "github.com/portainer/portainer/api/internal/snapshot"
)
const (
- volumeObjectIdentifier = "ID"
+ volumeObjectIdentifier = "ResourceID"
)
func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, endpointID portainer.EndpointID, volumeID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
@@ -48,10 +50,12 @@ func (transport *Transport) volumeListOperation(response *http.Response, executo
for _, volumeObject := range volumeData {
volume := volumeObject.(map[string]interface{})
- if volume["Name"] == nil || volume["CreatedAt"] == nil {
- return errors.New("missing identifier in Docker resource list response")
+
+ err = transport.decorateVolumeResponseWithResourceID(volume)
+ if err != nil {
+ return fmt.Errorf("failed decorating volume response: %w", err)
}
- volume[volumeObjectIdentifier] = volume["Name"].(string) + volume["CreatedAt"].(string)
+
}
resourceOperationParameters := &resourceOperationParameters{
@@ -81,10 +85,10 @@ func (transport *Transport) volumeInspectOperation(response *http.Response, exec
return err
}
- if responseObject["Name"] == nil || responseObject["CreatedAt"] == nil {
- return errors.New("missing identifier in Docker resource detail response")
+ err = transport.decorateVolumeResponseWithResourceID(responseObject)
+ if err != nil {
+ return fmt.Errorf("failed decorating volume response: %w", err)
}
- responseObject[volumeObjectIdentifier] = responseObject["Name"].(string) + responseObject["CreatedAt"].(string)
resourceOperationParameters := &resourceOperationParameters{
resourceIdentifierAttribute: volumeObjectIdentifier,
@@ -95,6 +99,21 @@ func (transport *Transport) volumeInspectOperation(response *http.Response, exec
return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor)
}
+func (transport *Transport) decorateVolumeResponseWithResourceID(responseObject map[string]interface{}) error {
+ if responseObject["Name"] == nil {
+ return errors.New("missing identifier in Docker resource detail response")
+ }
+
+ resourceID, err := transport.getVolumeResourceID(responseObject["Name"].(string))
+ if err != nil {
+ return fmt.Errorf("failed fetching resource id: %w", err)
+ }
+
+ responseObject[volumeObjectIdentifier] = resourceID
+
+ return nil
+}
+
// selectorVolumeLabels retrieve the labels object associated to the volume object.
// Labels are available under the "Labels" property.
// API schema references:
@@ -104,7 +123,7 @@ func selectorVolumeLabels(responseObject map[string]interface{}) map[string]inte
return utils.GetJSONObject(responseObject, "Labels")
}
-func (transport *Transport) decorateVolumeResourceCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
+func (transport *Transport) decorateVolumeResourceCreationOperation(request *http.Request, resourceType portainer.ResourceControlType) (*http.Response, error) {
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
@@ -136,27 +155,33 @@ func (transport *Transport) decorateVolumeResourceCreationOperation(request *htt
}
if response.StatusCode == http.StatusCreated {
- err = transport.decorateVolumeCreationResponse(response, resourceIdentifierAttribute, resourceType, tokenData.ID)
+ err = transport.decorateVolumeCreationResponse(response, resourceType, tokenData.ID)
}
return response, err
}
-func (transport *Transport) decorateVolumeCreationResponse(response *http.Response, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType, userID portainer.UserID) error {
+func (transport *Transport) decorateVolumeCreationResponse(response *http.Response, resourceType portainer.ResourceControlType, userID portainer.UserID) error {
responseObject, err := utils.GetResponseAsJSONObject(response)
if err != nil {
return err
}
- if responseObject["Name"] == nil || responseObject["CreatedAt"] == nil {
+ if responseObject["Name"] == nil {
return errors.New("missing identifier in Docker resource creation response")
}
- resourceID := responseObject["Name"].(string) + responseObject["CreatedAt"].(string)
+
+ resourceID, err := transport.getVolumeResourceID(responseObject["Name"].(string))
+ if err != nil {
+ return fmt.Errorf("failed fetching resource id: %w", err)
+ }
resourceControl, err := transport.createPrivateResourceControl(resourceID, resourceType, userID)
if err != nil {
return err
}
+ responseObject[volumeObjectIdentifier] = resourceID
+
responseObject = decorateObject(responseObject, resourceControl)
return utils.RewriteResponse(response, responseObject, http.StatusOK)
@@ -169,9 +194,8 @@ func (transport *Transport) restrictedVolumeOperation(requestPath string, reques
}
volumeName := path.Base(requestPath)
- agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)
- resourceID, err := transport.getVolumeResourceID(agentTargetHeader, volumeName)
+ resourceID, err := transport.getVolumeResourceID(volumeName)
if err != nil {
return nil, err
}
@@ -182,17 +206,34 @@ func (transport *Transport) restrictedVolumeOperation(requestPath string, reques
return transport.restrictedResourceOperation(request, resourceID, volumeName, portainer.VolumeResourceControl, false)
}
-func (transport *Transport) getVolumeResourceID(nodename, volumeID string) (string, error) {
- cli, err := transport.dockerClientFactory.CreateClient(transport.endpoint, nodename)
+func (transport *Transport) getVolumeResourceID(volumeName string) (string, error) {
+ dockerID, err := transport.getDockerID()
if err != nil {
- return "", err
+ return "", fmt.Errorf("failed fetching docker id: %w", err)
}
+ return fmt.Sprintf("%s_%s", volumeName, dockerID), nil
+}
+
+func (transport *Transport) getDockerID() (string, error) {
+ if len(transport.endpoint.Snapshots) > 0 {
+ dockerID, err := snapshot.FetchDockerID(transport.endpoint.Snapshots[0])
+ // ignore err - in case of error, just generate not from snapshot
+ if err == nil {
+ return dockerID, nil
+ }
+ }
+
+ cli := transport.dockerClient
defer cli.Close()
- volume, err := cli.VolumeInspect(context.Background(), volumeID)
+ info, err := cli.Info(context.Background())
if err != nil {
return "", err
}
- return volume.Name + volume.CreatedAt, nil
+ if info.Swarm.Cluster != nil {
+ return info.Swarm.Cluster.ID, nil
+ }
+
+ return info.ID, nil
}
diff --git a/api/internal/snapshot/snapshot.go b/api/internal/snapshot/snapshot.go
index 31b17acda..249ed168c 100644
--- a/api/internal/snapshot/snapshot.go
+++ b/api/internal/snapshot/snapshot.go
@@ -2,6 +2,7 @@ package snapshot
import (
"context"
+ "errors"
"log"
"time"
@@ -187,3 +188,27 @@ func (service *Service) snapshotEndpoints() error {
return nil
}
+
+// FetchDockerID fetches info.Swarm.Cluster.ID if endpoint is swarm and info.ID otherwise
+func FetchDockerID(snapshot portainer.DockerSnapshot) (string, error) {
+ info, done := snapshot.SnapshotRaw.Info.(map[string]interface{})
+ if !done {
+ return "", errors.New("failed getting snapshot info")
+ }
+
+ if !snapshot.Swarm {
+ return info["ID"].(string), nil
+ }
+
+ if info["Swarm"] == nil {
+ return "", errors.New("swarm endpoint is missing swarm info snapshot")
+ }
+
+ swarmInfo := info["Swarm"].(map[string]interface{})
+ if swarmInfo["Cluster"] == nil {
+ return "", errors.New("swarm endpoint is missing cluster info snapshot")
+ }
+
+ clusterInfo := swarmInfo["Cluster"].(map[string]interface{})
+ return clusterInfo["ID"].(string), nil
+}
diff --git a/app/docker/models/volume.js b/app/docker/models/volume.js
index 0de34bbc5..82ebcd0ba 100644
--- a/app/docker/models/volume.js
+++ b/app/docker/models/volume.js
@@ -13,6 +13,8 @@ export function VolumeViewModel(data) {
}
this.Mountpoint = data.Mountpoint;
+ this.ResourceId = data.ResourceID;
+
if (data.Portainer) {
if (data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
diff --git a/app/docker/views/volumes/edit/volume.html b/app/docker/views/volumes/edit/volume.html
index 167984bf3..6141cc537 100644
--- a/app/docker/views/volumes/edit/volume.html
+++ b/app/docker/views/volumes/edit/volume.html
@@ -78,7 +78,7 @@
-