feat: show proposed secret version changes during deploy #661
@ -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"
|
||||
@ -29,13 +28,68 @@ func MergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error {
|
||||
}
|
||||
|
||||
for k, v := range abraShEnv {
|
||||
log.Debugf("read v:%s k: %s", v, k)
|
||||
log.Debugf(i18n.G("read v:%s k: %s", v, k))
|
||||
env[k] = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEntityNameAndVersion parses a full 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,43 @@ 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(i18n.G("current secrets: %v", currentSecrets))
|
||||
|
||||
newSecrets, err := secret.PollSecretsStatus(cl, app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf(i18n.G("new secrets: %v", newSecrets))
|
||||
|
||||
// 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 +276,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)
|
@ -7,7 +7,7 @@
|
||||
msgid ""
|
||||
msgstr "Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: EMAIL\n"
|
||||
"POT-Creation-Date: 2025-09-09 15:59-0400\n"
|
||||
"POT-Creation-Date: 2025-09-09 17:36-0400\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -441,7 +441,7 @@ msgstr ""
|
||||
msgid "%s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:182 ./pkg/deploy/utils.go:234
|
||||
#: ./pkg/deploy/utils.go:219 ./pkg/deploy/utils.go:258 ./pkg/deploy/utils.go:311
|
||||
#, c-format
|
||||
msgid "%s: %s (new)"
|
||||
msgstr ""
|
||||
@ -451,12 +451,12 @@ msgstr ""
|
||||
msgid "%s: %s (retries: %v, healthcheck: %s)"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:176 ./pkg/deploy/utils.go:228
|
||||
#: ./pkg/deploy/utils.go:213 ./pkg/deploy/utils.go:252 ./pkg/deploy/utils.go:305
|
||||
#, c-format
|
||||
msgid "%s: %s (unchanged)"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:179 ./pkg/deploy/utils.go:231
|
||||
#: ./pkg/deploy/utils.go:216 ./pkg/deploy/utils.go:255 ./pkg/deploy/utils.go:308
|
||||
#, c-format
|
||||
msgid "%s: %s → %s"
|
||||
msgstr ""
|
||||
@ -466,7 +466,7 @@ msgstr ""
|
||||
msgid "%s: %s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:169
|
||||
#: ./pkg/deploy/utils.go:245
|
||||
#, c-format
|
||||
msgid "%s: ? (missing version)"
|
||||
msgstr ""
|
||||
@ -1917,9 +1917,9 @@ msgstr ""
|
||||
msgid "can't copy dir to file"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/client/configs.go:48
|
||||
#: ./pkg/deploy/utils.go:44
|
||||
#, c-format
|
||||
msgid "can't parse version from config '%s'"
|
||||
msgid "can't parse version from '%s'"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/internal/validate.go:35
|
||||
@ -2154,7 +2154,7 @@ msgstr ""
|
||||
msgid "compose: %s, "
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/client/configs.go:36
|
||||
#: ./pkg/client/configs.go:35
|
||||
#, c-format
|
||||
msgid "conf %s: %s"
|
||||
msgstr ""
|
||||
@ -2329,6 +2329,11 @@ msgstr ""
|
||||
msgid "current deployment '%s' is not a known version for %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:193
|
||||
#, c-format
|
||||
msgid "current secrets: %v"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/release.go:532
|
||||
#, c-format
|
||||
msgid "current: %s, new: %s, correct?"
|
||||
@ -2387,12 +2392,12 @@ msgstr ""
|
||||
msgid "deploy timed out 🟠"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:157
|
||||
#: ./pkg/deploy/utils.go:233
|
||||
#, c-format
|
||||
msgid "deployed config names: %v"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:196
|
||||
#: ./pkg/deploy/utils.go:272
|
||||
#, c-format
|
||||
msgid "deployed images: %v"
|
||||
msgstr ""
|
||||
@ -2447,16 +2452,21 @@ msgstr ""
|
||||
msgid "diff <recipe> [flags]"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:76
|
||||
#: ./pkg/deploy/utils.go:130
|
||||
#, c-format
|
||||
msgid "different versions for config '%s', '%s' and %s'"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:123 ./pkg/deploy/utils.go:217
|
||||
#: ./pkg/deploy/utils.go:177 ./pkg/deploy/utils.go:294
|
||||
#, c-format
|
||||
msgid "different versions for image '%s', '%s' and %s'"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:84
|
||||
#, c-format
|
||||
msgid "different versions for secret '%s', '%s' and %s'"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/recipe.go:202
|
||||
#, c-format
|
||||
msgid "dir: %s, "
|
||||
@ -3641,6 +3651,11 @@ msgstr ""
|
||||
msgid "new release published: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:200
|
||||
#, c-format
|
||||
msgid "new secrets: %v"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/git/read.go:88
|
||||
#, c-format
|
||||
msgid "no %s exists, not reading any global gitignore config"
|
||||
@ -3811,7 +3826,7 @@ msgstr ""
|
||||
msgid "no version bump type specififed?"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:168
|
||||
#: ./pkg/deploy/utils.go:244
|
||||
#, c-format
|
||||
msgid "no version found for config %s"
|
||||
msgstr ""
|
||||
@ -4027,7 +4042,7 @@ msgstr ""
|
||||
msgid "processing %s for %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:221
|
||||
#: ./pkg/deploy/utils.go:298
|
||||
#, c-format
|
||||
msgid "proposed images: %v"
|
||||
msgstr ""
|
||||
@ -4122,6 +4137,11 @@ msgstr ""
|
||||
msgid "read recipe catalogue from file system cache in %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:31
|
||||
#, c-format
|
||||
msgid "read v:%s k: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/ui/deploy.go:86
|
||||
#, c-format
|
||||
msgid "reader: %v, "
|
||||
@ -4545,7 +4565,7 @@ msgstr ""
|
||||
msgid "satisfied"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/deploy/utils.go:164
|
||||
#: ./pkg/deploy/utils.go:240
|
||||
#, c-format
|
||||
msgid "searching abra.sh for version for %s"
|
||||
msgstr ""
|
||||
|
Reference in New Issue
Block a user