Add images & configs to deploy overview #657

Merged
3wordchant merged 28 commits from feature/3wc/550-deploy-overview into main 2025-09-09 17:22:06 +00:00
16 changed files with 1029 additions and 356 deletions

View File

@ -6,14 +6,13 @@ import (
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/deploy"
"coopcloud.tech/abra/pkg/dns" "coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/i18n"
@ -129,13 +128,9 @@ checkout as-is. Recipe commit hashes are also supported as values for
log.Fatal(err) log.Fatal(err)
} }
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath) if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
for k, v := range abraShEnv {
app.Env[k] = v
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env) composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil { if err != nil {
@ -194,12 +189,34 @@ checkout as-is. Recipe commit hashes are also supported as values for
deployedVersion = deployMeta.Version deployedVersion = deployMeta.Version
} }
// Gather secrets
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app)
if err != nil {
log.Fatal(err)
}
// Gather configs
configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, app.Env, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
// Gather images
imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
// Show deploy overview
if err := internal.DeployOverview( if err := internal.DeployOverview(
3wordchant marked this conversation as resolved Outdated

Rogue newline?

Rogue newline?
app, app,
deployedVersion, deployedVersion,
toDeployVersion, toDeployVersion,
"", "",
deployWarnMessages, deployWarnMessages,
secretInfo,
configInfo,
3wordchant marked this conversation as resolved Outdated
Outdated
Review

I would move the strings.Join to the deployOverview function

I would move the strings.Join to the deployOverview function
imageInfo,
); err != nil { ); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -245,7 +262,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
}, },
} }
func getLatestVersionOrCommit(app app.App) (string, error) { func getLatestVersionOrCommit(app appPkg.App) (string, error) {
versions, err := app.Recipe.Tags() versions, err := app.Recipe.Tags()
if err != nil { if err != nil {
return "", err return "", err
@ -280,7 +297,7 @@ func validateArgsAndFlags(args []string) error {
return nil return nil
} }
func validateSecrets(cl *dockerClient.Client, app app.App) error { func validateSecrets(cl *dockerClient.Client, app appPkg.App) error {
secStats, err := secret.PollSecretsStatus(cl, app) secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil { if err != nil {
return err return err
@ -295,7 +312,7 @@ func validateSecrets(cl *dockerClient.Client, app app.App) error {
return nil return nil
} }
func getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app app.App) (string, error) { func getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app appPkg.App) (string, error) {
// Chaos mode overrides everything // Chaos mode overrides everything
if internal.Chaos { if internal.Chaos {
v, err := app.Recipe.ChaosVersion() v, err := app.Recipe.ChaosVersion()
@ -375,4 +392,12 @@ func init() {
false, false,
i18n.G("deploy latest recipe version"), i18n.G("deploy latest recipe version"),
) )
AppDeployCommand.Flags().BoolVarP(
&internal.ShowUnchanged,
i18n.G("show-unchanged"),
i18n.G("U"),
false,
i18n.G("show all configs & images, including unchanged ones"),
)
} }

View File

@ -4,11 +4,10 @@ import (
"errors" "errors"
"strings" "strings"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/deploy"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
@ -155,13 +154,9 @@ beforehand. See "abra app backup" for more.`),
log.Fatal(err) log.Fatal(err)
} }
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath) if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
for k, v := range abraShEnv {
app.Env[k] = v
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env) composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil { if err != nil {
@ -190,6 +185,24 @@ beforehand. See "abra app backup" for more.`),
} }
appPkg.SetUpdateLabel(compose, stackName, app.Env) appPkg.SetUpdateLabel(compose, stackName, app.Env)
// Gather secrets
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app)
if err != nil {
log.Fatal(err)
}
// Gather configs
configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, app.Env, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
// Gather images
imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
// NOTE(d1): no release notes implemeneted for rolling back // NOTE(d1): no release notes implemeneted for rolling back
if err := internal.DeployOverview( if err := internal.DeployOverview(
app, app,
@ -197,6 +210,9 @@ beforehand. See "abra app backup" for more.`),
chosenDowngrade, chosenDowngrade,
"", "",
downgradeWarnMessages, downgradeWarnMessages,
secretInfo,
configInfo,
imageInfo,
); err != nil { ); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -267,7 +283,7 @@ func chooseDowngrade(
// validateDownpgradeVersionArg validates the specific version. // validateDownpgradeVersionArg validates the specific version.
func validateDowngradeVersionArg( func validateDowngradeVersionArg(
specificVersion string, specificVersion string,
app app.App, app appPkg.App,
deployMeta stack.DeployMeta, deployMeta stack.DeployMeta,
) error { ) error {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version) parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
@ -346,4 +362,12 @@ func init() {
false, false,
i18n.G("disable converge logic checks"), i18n.G("disable converge logic checks"),
) )
AppRollbackCommand.Flags().BoolVarP(
&internal.ShowUnchanged,
i18n.G("show-unchanged"),
i18n.G("U"),
false,
i18n.G("show all configs & images, including unchanged ones"),
)
} }

View File

@ -71,6 +71,9 @@ Passing "--prune/-p" does not remove those volumes.`),
config.NO_DOMAIN_DEFAULT, config.NO_DOMAIN_DEFAULT,
"", "",
nil, nil,
nil,
nil,
nil,
); err != nil { ); err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -7,12 +7,11 @@ import (
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/deploy"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
@ -168,13 +167,9 @@ beforehand. See "abra app backup" for more.`),
log.Fatal(err) log.Fatal(err)
} }
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath) if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
for k, v := range abraShEnv {
app.Env[k] = v
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env) composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil { if err != nil {
@ -216,6 +211,24 @@ beforehand. See "abra app backup" for more.`),
} }
} }
// Gather secrets
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app)
if err != nil {
log.Fatal(err)
}
// Gather configs
configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, app.Env, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
// Gather images
imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
if showReleaseNotes { if showReleaseNotes {
fmt.Print(upgradeReleaseNotes) fmt.Print(upgradeReleaseNotes)
return return
@ -234,6 +247,9 @@ beforehand. See "abra app backup" for more.`),
chosenUpgrade, chosenUpgrade,
upgradeReleaseNotes, upgradeReleaseNotes,
upgradeWarnMessages, upgradeWarnMessages,
secretInfo,
configInfo,
imageInfo,
); err != nil { ); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -311,7 +327,7 @@ func chooseUpgrade(
} }
func getReleaseNotes( func getReleaseNotes(
app app.App, app appPkg.App,
versions []string, versions []string,
chosenUpgrade string, chosenUpgrade string,
deployMeta stack.DeployMeta, deployMeta stack.DeployMeta,
@ -356,7 +372,7 @@ func getReleaseNotes(
// ensureUpgradesAvailable ensures that there are available upgrades. // ensureUpgradesAvailable ensures that there are available upgrades.
func ensureUpgradesAvailable( func ensureUpgradesAvailable(
app app.App, app appPkg.App,
versions []string, versions []string,
availableUpgrades *[]string, availableUpgrades *[]string,
deployMeta stack.DeployMeta, deployMeta stack.DeployMeta,
@ -388,7 +404,7 @@ func ensureUpgradesAvailable(
// validateUpgradeVersionArg validates the specific version. // validateUpgradeVersionArg validates the specific version.
func validateUpgradeVersionArg( func validateUpgradeVersionArg(
specificVersion string, specificVersion string,
app app.App, app appPkg.App,
deployMeta stack.DeployMeta, deployMeta stack.DeployMeta,
) error { ) error {
parsedSpecificVersion, err := tagcmp.Parse(specificVersion) parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
@ -415,7 +431,7 @@ func validateUpgradeVersionArg(
// ensureDeployed ensures the app is deployed and if so, returns deployment // ensureDeployed ensures the app is deployed and if so, returns deployment
// meta info. // meta info.
func ensureDeployed(cl *dockerClient.Client, app app.App) (stack.DeployMeta, error) { func ensureDeployed(cl *dockerClient.Client, app appPkg.App) (stack.DeployMeta, error) {
log.Debug(i18n.G("checking whether %s is already deployed", app.StackName())) log.Debug(i18n.G("checking whether %s is already deployed", app.StackName()))
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName()) deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
@ -464,4 +480,12 @@ func init() {
false, false,
i18n.G("only show release notes"), i18n.G("only show release notes"),
) )
AppUpgradeCommand.Flags().BoolVarP(
&internal.ShowUnchanged,
i18n.G("show-unchanged"),
i18n.G("U"),
false,
i18n.G("show all configs & images, including unchanged ones"),
)
} }

View File

@ -19,4 +19,5 @@ var (
Minor bool Minor bool
NoDomainChecks bool NoDomainChecks bool
Patch bool Patch bool
ShowUnchanged bool
) )

View File

@ -50,6 +50,9 @@ func DeployOverview(
toDeployVersion string, toDeployVersion string,
releaseNotes string, releaseNotes string,
warnMessages []string, warnMessages []string,
secrets []string,
configs []string,
images []string,
) error { ) error {
deployConfig := "compose.yml" deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
@ -80,6 +83,24 @@ func DeployOverview(
{i18n.G("CURRENT DEPLOYMENT"), formatter.BoldDirtyDefault(deployedVersion)}, {i18n.G("CURRENT DEPLOYMENT"), formatter.BoldDirtyDefault(deployedVersion)},
{i18n.G("ENV VERSION"), formatter.BoldDirtyDefault(envVersion)}, {i18n.G("ENV VERSION"), formatter.BoldDirtyDefault(envVersion)},
{i18n.G("NEW DEPLOYMENT"), formatter.BoldDirtyDefault(toDeployVersion)}, {i18n.G("NEW DEPLOYMENT"), formatter.BoldDirtyDefault(toDeployVersion)},
{"", ""},
{i18n.G("IMAGES"), strings.Join(images, "\n")},
}
if len(secrets) > 0 {
secretsRows := [][]string{
{"", ""},
{i18n.G("SECRETS"), strings.Join(secrets, "\n")},
}
rows = append(rows, secretsRows...)
}
if len(configs) > 0 {
configsRows := [][]string{
{"", ""},
{i18n.G("CONFIGS"), strings.Join(configs, "\n")},
}
rows = append(rows, configsRows...)
} }
deployType := getDeployType(deployedVersion, toDeployVersion) deployType := getDeployType(deployedVersion, toDeployVersion)

View File

@ -11,6 +11,7 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/deploy"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
@ -335,21 +336,6 @@ func processRecipeRepoVersion(r recipe.Recipe, version string) error {
return nil return nil
} }
// mergeAbraShEnv merges abra.sh env vars into the app env vars.
func mergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error {
abraShEnv, err := envfile.ReadAbraShEnvVars(recipe.AbraShPath)
if err != nil {
return err
}
for k, v := range abraShEnv {
log.Debugf("read v:%s k: %s", v, k)
env[k] = v
}
return nil
}
// createDeployConfig merges and enriches the compose config for the deployment. // createDeployConfig merges and enriches the compose config for the deployment.
func createDeployConfig(r recipe.Recipe, stackName string, env envfile.AppEnv) (*composetypes.Config, stack.Deploy, error) { func createDeployConfig(r recipe.Recipe, stackName string, env envfile.AppEnv) (*composetypes.Config, stack.Deploy, error) {
env["STACK_NAME"] = stackName env["STACK_NAME"] = stackName
@ -444,7 +430,7 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri
return err return err
} }
if err = mergeAbraShEnv(app.Recipe, app.Env); err != nil { if err = deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
return err return err
} }

View File

@ -3,6 +3,7 @@ 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"
@ -37,3 +38,12 @@ 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")
3wordchant marked this conversation as resolved Outdated
Outdated
Review

A comment that describes how it works would be nice. Maybe with a small example

A comment that describes how it works would be nice. Maybe with a small example
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
3wordchant marked this conversation as resolved
Review

Can drop the else and just return ....

Can drop the `else` and just `return ...`.
}
return "", "", errors.New(i18n.G("can't parse version from config '%s'", fullName))
}

View File

@ -0,0 +1,89 @@
package client
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetConfigNameAndVersion(t *testing.T) {
tests := []struct {
name string
fullName string
stackName string
expected string
expectedVer string
expectError bool
}{
{
name: "valid config with version",
fullName: "myapp_database_v2",
stackName: "myapp",
expected: "database",
expectedVer: "v2",
expectError: false,
},
{
name: "valid config with numeric version",
fullName: "myapp_redis_1",
stackName: "myapp",
expected: "redis",
expectedVer: "1",
expectError: false,
},
{
name: "config without underscore in name",
fullName: "myapp_db_v1",
stackName: "myapp",
expected: "db",
expectedVer: "v1",
expectError: false,
},
{
name: "config with multiple underscores",
fullName: "myapp_my_database_v3",
stackName: "myapp",
expected: "my_database",
expectedVer: "v3",
expectError: false,
},
{
name: "invalid config - no version",
fullName: "myapp_database",
stackName: "myapp",
expectError: true,
},
{
name: "empty config name",
fullName: "myapp__v1",
stackName: "myapp",
expected: "",
expectedVer: "v1",
expectError: false,
},
{
name: "wrong stack prefix",
fullName: "otherapp_database_v1",
stackName: "myapp",
expected: "otherapp_database",
expectedVer: "v1",
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
name, version, err := GetConfigNameAndVersion(tt.fullName, tt.stackName)
if tt.expectError {
assert.Error(t, err)
assert.Empty(t, name)
assert.Empty(t, version)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, name)
assert.Equal(t, tt.expectedVer, version)
}
})
}
}

View File

@ -17,3 +17,11 @@ func StoreSecret(cl *client.Client, secretName, secretValue string) error {
return nil return nil
} }
func GetSecretNames(secrets []swarm.Secret) []string {
var secretNames []string
for _, secret := range secrets {
secretNames = append(secretNames, secret.Spec.Name)
}
return secretNames
}

58
pkg/client/secret_test.go Normal file
View File

@ -0,0 +1,58 @@
package client
import (
"testing"
"github.com/docker/docker/api/types/swarm"
"github.com/stretchr/testify/assert"
)
func TestGetSecretNames(t *testing.T) {
tests := []struct {
name string
secrets []swarm.Secret
expected []string
description string
}{
{
name: "empty secrets list",
secrets: []swarm.Secret{},
expected: nil,
description: "should return nil for empty input",
},
{
name: "single secret",
secrets: []swarm.Secret{
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "database_password"}}},
},
expected: []string{"database_password"},
description: "should return single secret name",
},
{
name: "multiple secrets",
secrets: []swarm.Secret{
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "db_password"}}},
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "api_key"}}},
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "ssl_cert"}}},
},
expected: []string{"db_password", "api_key", "ssl_cert"},
description: "should return all secret names in order",
},
{
name: "secrets with empty names",
secrets: []swarm.Secret{
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: ""}}},
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "valid_name"}}},
},
expected: []string{"", "valid_name"},
description: "should include empty names if present",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetSecretNames(tt.secrets)
assert.Equal(t, tt.expected, result, tt.description)
})
}
}

239
pkg/deploy/utils.go Normal file
View File

@ -0,0 +1,239 @@
package deploy
import (
"context"
"fmt"
"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"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret"
"github.com/distribution/reference"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types/swarm"
dockerClient "github.com/docker/docker/client"
)
// MergeAbraShEnv merges abra.sh env vars into the app env vars.
func MergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error {
abraShEnv, err := envfile.ReadAbraShEnvVars(recipe.AbraShPath)
if err != nil {
return err
}
for k, v := range abraShEnv {
log.Debugf("read v:%s k: %s", v, k)
env[k] = v
}
return 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)
if err != nil {
return nil, err
}
// List all services in the stack
services, err := cl.ServiceList(context.Background(), swarm.ServiceListOptions{
Filters: filters,
})
if err != nil {
return nil, err
}
// Collect unique config names with versions
configs := make(map[string]string)
for _, service := range services {
if service.Spec.TaskTemplate.ContainerSpec != nil {
for _, configRef := range service.Spec.TaskTemplate.ContainerSpec.Configs {
configName := configRef.ConfigName
if configName == "" {
continue
}
configBaseName, configVersion, err := client.GetConfigNameAndVersion(configName, app.StackName())
if err != nil {
log.Warn(err)
continue
}
existingConfigVersion, ok := configs[configBaseName]
if !ok {
// First time seeing this, add to map
configs[configBaseName] = configVersion
} else {
// Just make sure the versions are the same..
if existingConfigVersion != configVersion {
3wordchant marked this conversation as resolved
Review

log.Warn(i18n.G("..."))

`log.Warn(i18n.G("..."))`
log.Warnf(i18n.G("different versions for config '%s', '%s' and %s'", configBaseName, existingConfigVersion, configVersion))
}
}
}
}
}
return configs, nil
}
// GetImagesForStack retrieves all Docker images for services in a given stack.
func GetImagesForStack(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
services, err := cl.ServiceList(context.Background(), swarm.ServiceListOptions{
Filters: filters,
})
if err != nil {
return nil, err
}
// Collect unique image names with versions
images := make(map[string]string)
for _, service := range services {
if service.Spec.TaskTemplate.ContainerSpec != nil {
imageName := service.Spec.TaskTemplate.ContainerSpec.Image
imageParsed, err := reference.ParseNormalizedNamed(imageName)
if err != nil {
log.Warn(err)
continue
}
imageBaseName := reference.Path(imageParsed)
imageTag := imageParsed.(reference.NamedTagged).Tag()
existingImageVersion, ok := images[imageBaseName]
if !ok {
// First time seeing this, add to map
images[imageBaseName] = imageTag
} else {
// Just make sure the versions are the same..
if existingImageVersion != imageTag {
3wordchant marked this conversation as resolved
Review

log.Warn(i18n.G("..."))

```log.Warn(i18n.G("..."))```
log.Warnf(i18n.G("different versions for image '%s', '%s' and %s'", imageBaseName, existingImageVersion, imageTag))
}
}
}
}
return images, nil
}
func GatherSecretsForDeploy(cl *dockerClient.Client, app appPkg.App) ([]string, error) {
3wordchant marked this conversation as resolved Outdated

Rogue newline?

Rogue newline?
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil {
return nil, err
}
var secretInfo []string
// Sort secrets to ensure reproducible output
sort.Slice(secStats, func(i, j int) bool {
return secStats[i].LocalName < secStats[j].LocalName
})
for _, secStat := range secStats {
secretInfo = append(secretInfo, fmt.Sprintf("%s: %s", secStat.LocalName, secStat.Version))
}
return secretInfo, nil
}
func GatherConfigsForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composetypes.Config, abraShEnv map[string]string, showUnchanged bool) ([]string, error) {
// Get current configs from existing deployment
currentConfigs, err := GetConfigsForStack(cl, app)
if err != nil {
return nil, err
}
log.Debugf(i18n.G("deployed config names: %v", currentConfigs))
3wordchant marked this conversation as resolved Outdated

log.Debug(i18n.G("...")) (lowercase)

```log.Debug(i18n.G("..."))``` (lowercase)
// Get new configs from the compose specification
newConfigs := compose.Configs
var configInfo []string
for configName := range newConfigs {
log.Debugf(i18n.G("searching abra.sh for version for %s", configName))
3wordchant marked this conversation as resolved Outdated

log.Debug(i18n.G("...")) (lowercase)

```log.Debug(i18n.G("..."))``` (lowercase)
versionKey := strings.ToUpper(configName) + "_VERSION"
newVersion, exists := abraShEnv[versionKey]
if !exists {
log.Warnf(i18n.G("no version found for config %s", configName))
3wordchant marked this conversation as resolved Outdated

log.Warn(i18n.G("...")) (lowercase)

```log.Warn(i18n.G("..."))``` (lowercase)
configInfo = append(configInfo, i18n.G("%s: ? (missing version)", configName))
3wordchant marked this conversation as resolved Outdated

i18n.G("...")

`i18n.G("...")`
continue
}
if currentVersion, exists := currentConfigs[configName]; exists {
if currentVersion == newVersion {
if showUnchanged {
configInfo = append(configInfo, i18n.G("%s: %s (unchanged)", configName, newVersion))
3wordchant marked this conversation as resolved Outdated

i18n.G("...")

`i18n.G("...")`
}
} else {
configInfo = append(configInfo, i18n.G("%s: %s → %s", configName, currentVersion, newVersion))
}
} else {
configInfo = append(configInfo, i18n.G("%s: %s (new)", configName, newVersion))
3wordchant marked this conversation as resolved Outdated

i18n.G("...")

`i18n.G("...")`
}
}
return configInfo, nil
}
func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composetypes.Config, showUnchanged bool) ([]string, error) {
// Get current images from existing deployment
3wordchant marked this conversation as resolved
Review

Rogue newline?

Rogue newline?
currentImages, err := GetImagesForStack(cl, app)
if err != nil {
return nil, err
}
log.Debugf(i18n.G("deployed images: %v", currentImages))
3wordchant marked this conversation as resolved Outdated

log.Debug(i18n.G("...")) (lowercase)

```log.Debug(i18n.G("..."))``` (lowercase)
// Proposed new images from the compose files
newImages := make(map[string]string)
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
}
existingImageVersion, ok := newImages[imageBaseName]
if !ok {
// First time seeing this, add to map
newImages[imageBaseName] = imageTag
} else {
// Just make sure the versions are the same..
if existingImageVersion != imageTag {
log.Warnf(i18n.G("different versions for image '%s', '%s' and %s'", imageBaseName, existingImageVersion, imageTag))
}
3wordchant marked this conversation as resolved Outdated

log.Warn(i18n.G("..."))

```log.Warn(i18n.G("..."))```
}
}
log.Debugf(i18n.G("proposed images: %v", newImages))
3wordchant marked this conversation as resolved Outdated

log.Debug(i18n.G("...")) (lowercase)

```log.Debug(i18n.G("..."))``` (lowercase)
var imageInfo []string
for newImageName, newImageVersion := range newImages {
if currentVersion, exists := currentImages[newImageName]; exists {
if currentVersion == newImageVersion {
if showUnchanged {
imageInfo = append(imageInfo, i18n.G("%s: %s (unchanged)", formatter.StripTagMeta(newImageName), newImageVersion))
}
3wordchant marked this conversation as resolved Outdated

i18n.G("...")

`i18n.G("...")`
} else {
imageInfo = append(imageInfo, i18n.G("%s: %s → %s", formatter.StripTagMeta(newImageName), currentVersion, newImageVersion))
}
} else {
imageInfo = append(imageInfo, i18n.G("%s: %s (new)", formatter.StripTagMeta(newImageName), newImageVersion))
}
3wordchant marked this conversation as resolved Outdated

i18n.G("...")

`i18n.G("...")`
}
return imageInfo, nil
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -280,7 +280,7 @@ type secretStatus struct {
type secretStatuses []secretStatus type secretStatuses []secretStatus
// PollSecretsStatus checks status of secrets by comparing the local recipe // PollSecretsStatus checks status of secrets by comparing the local recipe
// config and deploymend server state. // config and deployed server state.
func PollSecretsStatus(cl *dockerClient.Client, app appPkg.App) (secretStatuses, error) { func PollSecretsStatus(cl *dockerClient.Client, app appPkg.App) (secretStatuses, error) {
var secStats secretStatuses var secStats secretStatuses
@ -306,7 +306,7 @@ func PollSecretsStatus(cl *dockerClient.Client, app appPkg.App) (secretStatuses,
remoteSecretNames := make(map[string]bool) remoteSecretNames := make(map[string]bool)
for _, cont := range secretList { for _, cont := range secretList {
remoteSecretNames[cont.Spec.Annotations.Name] = true remoteSecretNames[cont.Spec.Name] = true
} }
for secretName, val := range secretsConfig { for secretName, val := range secretsConfig {

View File

@ -41,6 +41,8 @@ teardown(){
assert_output --partial 'CURRENT DEPLOYMENT N/A' assert_output --partial 'CURRENT DEPLOYMENT N/A'
assert_output --partial 'ENV VERSION N/A' assert_output --partial 'ENV VERSION N/A'
assert_output --partial "NEW DEPLOYMENT ${latestRelease}" assert_output --partial "NEW DEPLOYMENT ${latestRelease}"
assert_output --partial "IMAGES nginx: ${latestRelease##*+} (new)"
assert_output --partial "CONFIGS test_conf: v1 (new)"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \ run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
@ -58,12 +60,35 @@ teardown(){
assert_output --partial "CURRENT DEPLOYMENT N/A" assert_output --partial "CURRENT DEPLOYMENT N/A"
assert_output --partial "ENV VERSION ${latestRelease}" assert_output --partial "ENV VERSION ${latestRelease}"
assert_output --partial "NEW DEPLOYMENT ${latestRelease}" assert_output --partial "NEW DEPLOYMENT ${latestRelease}"
assert_output --partial "IMAGES nginx: ${latestRelease##*+} (new)"
assert_output --partial "CONFIGS test_conf: v1 (new)"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \ run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success assert_success
} }
# bats test_tags=slow
@test "show changed config version on re-deploy" {
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_success
run sed -i 's/TEST_CONF_VERSION=v1/TEST_CONF_VERSION=v2/' \
"$ABRA_DIR/recipes/$TEST_RECIPE/abra.sh" \
assert_success
cat "$ABRA_DIR/recipes/$TEST_RECIPE/abra.sh"
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --force --chaos
assert_success
assert_output --partial "CONFIGS test_conf: v1 → v2"
_checkout_recipe
}
# bats test_tags=slow # bats test_tags=slow
@test "deploy, re-deploy, choose env version" { @test "deploy, re-deploy, choose env version" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" \ run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" \