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 @@ - +