forked from toolshed/abra
		
	feat: show proposed secret version changes during deploy
This commit is contained in:
		| @ -192,7 +192,7 @@ checkout as-is. Recipe commit hashes are also supported as values for | ||||
| 		} | ||||
|  | ||||
| 		// Gather secrets | ||||
| 		secretInfo, err := deploy.GatherSecretsForDeploy(cl, app) | ||||
| 		secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged) | ||||
| 		if err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| @ -186,7 +186,7 @@ beforehand. See "abra app backup" for more.`), | ||||
| 		appPkg.SetUpdateLabel(compose, stackName, app.Env) | ||||
|  | ||||
| 		// Gather secrets | ||||
| 		secretInfo, err := deploy.GatherSecretsForDeploy(cl, app) | ||||
| 		secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged) | ||||
| 		if err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| @ -212,7 +212,7 @@ beforehand. See "abra app backup" for more.`), | ||||
| 		} | ||||
|  | ||||
| 		// Gather secrets | ||||
| 		secretInfo, err := deploy.GatherSecretsForDeploy(cl, app) | ||||
| 		secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged) | ||||
| 		if err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| @ -3,7 +3,6 @@ package client | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"strings" | ||||
|  | ||||
| 	"coopcloud.tech/abra/pkg/i18n" | ||||
| 	"github.com/docker/docker/api/types/filters" | ||||
| @ -38,12 +37,3 @@ func RemoveConfigs(cl *client.Client, ctx context.Context, configNames []string, | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // GetConfigNameAndVersion parses a full config name like `app_example_com_someconf_v1` to extract name and version, ("someconf", "v1") | ||||
| func GetConfigNameAndVersion(fullName string, stackName string) (string, string, error) { | ||||
| 	name := strings.TrimPrefix(fullName, stackName+"_") | ||||
| 	if lastUnderscore := strings.LastIndex(name, "_"); lastUnderscore != -1 { | ||||
| 		return name[0:lastUnderscore], name[lastUnderscore+1:], nil | ||||
| 	} | ||||
| 	return "", "", errors.New(i18n.G("can't parse version from config '%s'", fullName)) | ||||
| } | ||||
|  | ||||
| @ -2,12 +2,11 @@ package deploy | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"errors" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
|  | ||||
| 	appPkg "coopcloud.tech/abra/pkg/app" | ||||
| 	"coopcloud.tech/abra/pkg/client" | ||||
| 	"coopcloud.tech/abra/pkg/envfile" | ||||
| 	"coopcloud.tech/abra/pkg/formatter" | ||||
| 	"coopcloud.tech/abra/pkg/i18n" | ||||
| @ -36,6 +35,61 @@ func MergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // GetEntityNameAndVersion parses a full config name like `app_example_com_someconf_v1` to extract name and version, ("someconf", "v1") | ||||
| func GetEntityNameAndVersion(fullName string, stackName string) (string, string, error) { | ||||
| 	name := strings.TrimPrefix(fullName, stackName+"_") | ||||
| 	if lastUnderscore := strings.LastIndex(name, "_"); lastUnderscore != -1 { | ||||
| 		return name[0:lastUnderscore], name[lastUnderscore+1:], nil | ||||
| 	} | ||||
| 	return "", "", errors.New(i18n.G("can't parse version from '%s'", fullName)) | ||||
| } | ||||
|  | ||||
| func GetSecretsForStack(cl *dockerClient.Client, app appPkg.App) (map[string]string, error) { | ||||
| 	filters, err := app.Filters(false, false) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// List all services in the stack | ||||
| 	// NOTE: we could do cl.SecretList, but we want to know which secrets are actually attached | ||||
| 	services, err := cl.ServiceList(context.Background(), swarm.ServiceListOptions{ | ||||
| 		Filters: filters, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	secrets := make(map[string]string) | ||||
|  | ||||
| 	for _, service := range services { | ||||
| 		if service.Spec.TaskTemplate.ContainerSpec.Secrets != nil { | ||||
| 			for _, secretRef := range service.Spec.TaskTemplate.ContainerSpec.Secrets { | ||||
| 				secretName := secretRef.SecretName | ||||
| 				if secretName == "" { | ||||
| 					continue | ||||
| 				} | ||||
| 				secretBaseName, secretVersion, err := GetEntityNameAndVersion(secretName, app.StackName()) | ||||
| 				if err != nil { | ||||
| 					log.Warn(err) | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| 				existingSecretVersion, exists := secrets[secretBaseName] | ||||
| 				if !exists { | ||||
| 					// First time seeing this, add to map | ||||
| 					secrets[secretBaseName] = secretVersion | ||||
| 				} else { | ||||
| 					// Just make sure the versions are the same.. | ||||
| 					if existingSecretVersion != secretVersion { | ||||
| 						log.Warnf(i18n.G("different versions for secret '%s', '%s' and %s'", secretBaseName, existingSecretVersion, secretVersion)) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return secrets, nil | ||||
| } | ||||
|  | ||||
| // GetConfigsForStack retrieves all Docker configs attached to services in a given stack. | ||||
| func GetConfigsForStack(cl *dockerClient.Client, app appPkg.App) (map[string]string, error) { | ||||
| 	filters, err := app.Filters(false, false) | ||||
| @ -60,7 +114,7 @@ func GetConfigsForStack(cl *dockerClient.Client, app appPkg.App) (map[string]str | ||||
| 				if configName == "" { | ||||
| 					continue | ||||
| 				} | ||||
| 				configBaseName, configVersion, err := client.GetConfigNameAndVersion(configName, app.StackName()) | ||||
| 				configBaseName, configVersion, err := GetEntityNameAndVersion(configName, app.StackName()) | ||||
| 				if err != nil { | ||||
| 					log.Warn(err) | ||||
| 					continue | ||||
| @ -129,21 +183,41 @@ func GetImagesForStack(cl *dockerClient.Client, app appPkg.App) (map[string]stri | ||||
| 	return images, nil | ||||
| } | ||||
|  | ||||
| func GatherSecretsForDeploy(cl *dockerClient.Client, app appPkg.App) ([]string, error) { | ||||
| 	secStats, err := secret.PollSecretsStatus(cl, app) | ||||
| func GatherSecretsForDeploy(cl *dockerClient.Client, app appPkg.App, showUnchanged bool) ([]string, error) { | ||||
| 	// Get current secrets from existing deployment | ||||
| 	currentSecrets, err := GetSecretsForStack(cl, app) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	var secretInfo []string | ||||
| 	log.Debugf("current secrets: %v", currentSecrets) | ||||
|  | ||||
| 	newSecrets, err := secret.PollSecretsStatus(cl, app) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Sort secrets to ensure reproducible output | ||||
| 	sort.Slice(secStats, func(i, j int) bool { | ||||
| 		return secStats[i].LocalName < secStats[j].LocalName | ||||
| 	sort.Slice(newSecrets, func(i, j int) bool { | ||||
| 		return newSecrets[i].LocalName < newSecrets[j].LocalName | ||||
| 	}) | ||||
| 	for _, secStat := range secStats { | ||||
| 		secretInfo = append(secretInfo, fmt.Sprintf("%s: %s", secStat.LocalName, secStat.Version)) | ||||
|  | ||||
| 	var secretInfo []string | ||||
|  | ||||
| 	for _, newSecret := range newSecrets { | ||||
| 		if currentVersion, exists := currentSecrets[newSecret.LocalName]; exists { | ||||
| 			if currentVersion == newSecret.Version { | ||||
| 				if showUnchanged { | ||||
| 					secretInfo = append(secretInfo, i18n.G("%s: %s (unchanged)", newSecret.LocalName, newSecret.Version)) | ||||
| 				} | ||||
| 			} else { | ||||
| 				secretInfo = append(secretInfo, i18n.G("%s: %s → %s", newSecret.LocalName, currentVersion, newSecret.Version)) | ||||
| 			} | ||||
| 		} else { | ||||
| 			secretInfo = append(secretInfo, i18n.G("%s: %s (new)", newSecret.LocalName, newSecret.Version)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return secretInfo, nil | ||||
| } | ||||
|  | ||||
| @ -200,13 +274,14 @@ func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *com | ||||
|  | ||||
| 	for _, service := range compose.Services { | ||||
| 		imageParsed, err := reference.ParseNormalizedNamed(service.Image) | ||||
| 		imageBaseName := reference.Path(imageParsed) | ||||
| 		imageTag := imageParsed.(reference.NamedTagged).Tag() | ||||
|  | ||||
| 		if err != nil { | ||||
| 			log.Warn(err) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		imageBaseName := reference.Path(imageParsed) | ||||
| 		imageTag := imageParsed.(reference.NamedTagged).Tag() | ||||
|  | ||||
| 		existingImageVersion, ok := newImages[imageBaseName] | ||||
| 		if !ok { | ||||
| 			// First time seeing this, add to map | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| package client | ||||
| package deploy | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| @ -6,7 +6,7 @@ import ( | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestGetConfigNameAndVersion(t *testing.T) { | ||||
| func TestGetEntityNameAndVersion(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name        string | ||||
| 		fullName    string | ||||
| @ -73,7 +73,7 @@ func TestGetConfigNameAndVersion(t *testing.T) { | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			name, version, err := GetConfigNameAndVersion(tt.fullName, tt.stackName) | ||||
| 			name, version, err := GetEntityNameAndVersion(tt.fullName, tt.stackName) | ||||
| 
 | ||||
| 			if tt.expectError { | ||||
| 				assert.Error(t, err) | ||||
		Reference in New Issue
	
	Block a user