From c67fc57902ef78a6d86b22f0c566cf9eb6776a8f Mon Sep 17 00:00:00 2001 From: 3wc <3wc@doesthisthing.work> Date: Tue, 9 Sep 2025 17:34:06 -0400 Subject: [PATCH] feat: show proposed secret version changes during deploy --- cli/app/deploy.go | 2 +- cli/app/rollback.go | 2 +- cli/app/upgrade.go | 2 +- pkg/client/configs.go | 10 -- pkg/deploy/utils.go | 101 +++++++++++++++--- .../configs_test.go => deploy/utils_test.go} | 6 +- 6 files changed, 94 insertions(+), 29 deletions(-) rename pkg/{client/configs_test.go => deploy/utils_test.go} (93%) diff --git a/cli/app/deploy.go b/cli/app/deploy.go index 8cb630c8..3b412f12 100644 --- a/cli/app/deploy.go +++ b/cli/app/deploy.go @@ -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) } diff --git a/cli/app/rollback.go b/cli/app/rollback.go index dfff8807..298297c2 100644 --- a/cli/app/rollback.go +++ b/cli/app/rollback.go @@ -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) } diff --git a/cli/app/upgrade.go b/cli/app/upgrade.go index 718ea6de..1231b0b9 100644 --- a/cli/app/upgrade.go +++ b/cli/app/upgrade.go @@ -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) } diff --git a/pkg/client/configs.go b/pkg/client/configs.go index 406a32ad..2a2a0eeb 100644 --- a/pkg/client/configs.go +++ b/pkg/client/configs.go @@ -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)) -} diff --git a/pkg/deploy/utils.go b/pkg/deploy/utils.go index bfe333a2..0fceb13b 100644 --- a/pkg/deploy/utils.go +++ b/pkg/deploy/utils.go @@ -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 diff --git a/pkg/client/configs_test.go b/pkg/deploy/utils_test.go similarity index 93% rename from pkg/client/configs_test.go rename to pkg/deploy/utils_test.go index 51e8a6b5..db842cd1 100644 --- a/pkg/client/configs_test.go +++ b/pkg/deploy/utils_test.go @@ -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)