feat: show proposed secret version changes during deploy

This commit is contained in:
3wc
2025-09-09 17:34:06 -04:00
parent 07cafd371c
commit c67fc57902
6 changed files with 94 additions and 29 deletions

View File

@ -192,7 +192,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
} }
// Gather secrets // Gather secrets
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app) secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -186,7 +186,7 @@ beforehand. See "abra app backup" for more.`),
appPkg.SetUpdateLabel(compose, stackName, app.Env) appPkg.SetUpdateLabel(compose, stackName, app.Env)
// Gather secrets // Gather secrets
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app) secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -212,7 +212,7 @@ beforehand. See "abra app backup" for more.`),
} }
// Gather secrets // Gather secrets
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app) secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -3,7 +3,6 @@ package client
import ( import (
"context" "context"
"errors" "errors"
"strings"
"coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/i18n"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -38,12 +37,3 @@ func RemoveConfigs(cl *client.Client, ctx context.Context, configNames []string,
} }
return nil 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))
}

View File

@ -2,12 +2,11 @@ package deploy
import ( import (
"context" "context"
"fmt" "errors"
"sort" "sort"
"strings" "strings"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/i18n"
@ -36,6 +35,61 @@ func MergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error {
return nil 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. // GetConfigsForStack retrieves all Docker configs attached to services in a given stack.
func GetConfigsForStack(cl *dockerClient.Client, app appPkg.App) (map[string]string, error) { func GetConfigsForStack(cl *dockerClient.Client, app appPkg.App) (map[string]string, error) {
filters, err := app.Filters(false, false) filters, err := app.Filters(false, false)
@ -60,7 +114,7 @@ func GetConfigsForStack(cl *dockerClient.Client, app appPkg.App) (map[string]str
if configName == "" { if configName == "" {
continue continue
} }
configBaseName, configVersion, err := client.GetConfigNameAndVersion(configName, app.StackName()) configBaseName, configVersion, err := GetEntityNameAndVersion(configName, app.StackName())
if err != nil { if err != nil {
log.Warn(err) log.Warn(err)
continue continue
@ -129,21 +183,41 @@ func GetImagesForStack(cl *dockerClient.Client, app appPkg.App) (map[string]stri
return images, nil return images, nil
} }
func GatherSecretsForDeploy(cl *dockerClient.Client, app appPkg.App) ([]string, error) { func GatherSecretsForDeploy(cl *dockerClient.Client, app appPkg.App, showUnchanged bool) ([]string, error) {
secStats, err := secret.PollSecretsStatus(cl, app) // Get current secrets from existing deployment
currentSecrets, err := GetSecretsForStack(cl, app)
if err != nil { if err != nil {
return nil, err 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 secrets to ensure reproducible output
sort.Slice(secStats, func(i, j int) bool { sort.Slice(newSecrets, func(i, j int) bool {
return secStats[i].LocalName < secStats[j].LocalName 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 return secretInfo, nil
} }
@ -200,13 +274,14 @@ func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *com
for _, service := range compose.Services { for _, service := range compose.Services {
imageParsed, err := reference.ParseNormalizedNamed(service.Image) imageParsed, err := reference.ParseNormalizedNamed(service.Image)
imageBaseName := reference.Path(imageParsed)
imageTag := imageParsed.(reference.NamedTagged).Tag()
if err != nil { if err != nil {
log.Warn(err) log.Warn(err)
continue continue
} }
imageBaseName := reference.Path(imageParsed)
imageTag := imageParsed.(reference.NamedTagged).Tag()
existingImageVersion, ok := newImages[imageBaseName] existingImageVersion, ok := newImages[imageBaseName]
if !ok { if !ok {
// First time seeing this, add to map // First time seeing this, add to map

View File

@ -1,4 +1,4 @@
package client package deploy
import ( import (
"testing" "testing"
@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestGetConfigNameAndVersion(t *testing.T) { func TestGetEntityNameAndVersion(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
fullName string fullName string
@ -73,7 +73,7 @@ func TestGetConfigNameAndVersion(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { if tt.expectError {
assert.Error(t, err) assert.Error(t, err)