feat(docker/volumes): change how volume resource id is calculated (#5067)

[EE-494]
pull/5308/head
Chaim Lev-Ari 2021-07-19 10:43:49 +03:00 committed by GitHub
parent 72117693fb
commit db16299aab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 187 additions and 31 deletions

View File

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

View File

@ -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()

View File

@ -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)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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);

View File

@ -78,7 +78,7 @@
</div>
<!-- access-control-panel -->
<por-access-control-panel ng-if="volume" resource-id="volume.Id + volume.CreatedAt" resource-control="volume.ResourceControl" resource-type="'volume'"> </por-access-control-panel>
<por-access-control-panel ng-if="volume" resource-id="volume.ResourceId" resource-control="volume.ResourceControl" resource-type="'volume'"> </por-access-control-panel>
<!-- !access-control-panel -->
<div class="row" ng-if="!(volume.Options | emptyobject)">