Compare commits
19 Commits
main
...
feature/3w
Author | SHA1 | Date | |
---|---|---|---|
cd34b64089 | |||
bcc208d06a | |||
02473f8fbb | |||
380a47ea4a | |||
ec2791c6f6 | |||
eaac5a1396 | |||
1edc64fdf6 | |||
a4714dda9f | |||
82b9371790 | |||
cf9fd715e9 | |||
6a3c79cb3b | |||
4896b7c782 | |||
f60c1c328c | |||
e8b5b92599 | |||
e3db5e357c | |||
99d2291045 | |||
86f324d4b6 | |||
4dd96e313b | |||
d84c1d24ba |
@ -6,14 +6,13 @@ import (
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/envfile"
|
||||
"coopcloud.tech/abra/pkg/secret"
|
||||
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/deploy"
|
||||
"coopcloud.tech/abra/pkg/dns"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
@ -126,13 +125,9 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
|
||||
if err != nil {
|
||||
if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for k, v := range abraShEnv {
|
||||
app.Env[k] = v
|
||||
}
|
||||
|
||||
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||
if err != nil {
|
||||
@ -191,12 +186,35 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
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)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Gather images
|
||||
imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Show deploy overview
|
||||
|
||||
if err := internal.DeployOverview(
|
||||
app,
|
||||
deployedVersion,
|
||||
toDeployVersion,
|
||||
"",
|
||||
deployWarnMessages,
|
||||
strings.Join(secretInfo, "\n"),
|
||||
strings.Join(configInfo, "\n"),
|
||||
strings.Join(imageInfo, "\n"),
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -242,7 +260,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()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -277,7 +295,7 @@ func validateArgsAndFlags(args []string) error {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -292,7 +310,7 @@ func validateSecrets(cl *dockerClient.Client, app app.App) error {
|
||||
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
|
||||
if internal.Chaos {
|
||||
v, err := app.Recipe.ChaosVersion()
|
||||
|
@ -4,11 +4,10 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/app"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/envfile"
|
||||
"coopcloud.tech/abra/pkg/deploy"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/lint"
|
||||
@ -155,13 +154,9 @@ beforehand. See "abra app backup" for more.`),
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
|
||||
if err != nil {
|
||||
if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for k, v := range abraShEnv {
|
||||
app.Env[k] = v
|
||||
}
|
||||
|
||||
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||
if err != nil {
|
||||
@ -190,6 +185,24 @@ beforehand. See "abra app backup" for more.`),
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Gather images
|
||||
imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// NOTE(d1): no release notes implemeneted for rolling back
|
||||
if err := internal.DeployOverview(
|
||||
app,
|
||||
@ -197,6 +210,9 @@ beforehand. See "abra app backup" for more.`),
|
||||
chosenDowngrade,
|
||||
"",
|
||||
downgradeWarnMessages,
|
||||
strings.Join(secretInfo, "\n"),
|
||||
strings.Join(configInfo, "\n"),
|
||||
strings.Join(imageInfo, "\n"),
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -267,7 +283,7 @@ func chooseDowngrade(
|
||||
// validateDownpgradeVersionArg validates the specific version.
|
||||
func validateDowngradeVersionArg(
|
||||
specificVersion string,
|
||||
app app.App,
|
||||
app appPkg.App,
|
||||
deployMeta stack.DeployMeta,
|
||||
) error {
|
||||
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
|
||||
|
@ -71,6 +71,9 @@ Passing "--prune/-p" does not remove those volumes.`),
|
||||
config.NO_DOMAIN_DEFAULT,
|
||||
"",
|
||||
nil,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -7,12 +7,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/app"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/envfile"
|
||||
"coopcloud.tech/abra/pkg/deploy"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/lint"
|
||||
@ -168,13 +167,9 @@ beforehand. See "abra app backup" for more.`),
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
|
||||
if err != nil {
|
||||
if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for k, v := range abraShEnv {
|
||||
app.Env[k] = v
|
||||
}
|
||||
|
||||
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||
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)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Gather images
|
||||
imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if showReleaseNotes {
|
||||
fmt.Print(upgradeReleaseNotes)
|
||||
return
|
||||
@ -234,6 +247,9 @@ beforehand. See "abra app backup" for more.`),
|
||||
chosenUpgrade,
|
||||
upgradeReleaseNotes,
|
||||
upgradeWarnMessages,
|
||||
strings.Join(secretInfo, "\n"),
|
||||
strings.Join(configInfo, "\n"),
|
||||
strings.Join(imageInfo, "\n"),
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -311,7 +327,7 @@ func chooseUpgrade(
|
||||
}
|
||||
|
||||
func getReleaseNotes(
|
||||
app app.App,
|
||||
app appPkg.App,
|
||||
versions []string,
|
||||
chosenUpgrade string,
|
||||
deployMeta stack.DeployMeta,
|
||||
@ -356,7 +372,7 @@ func getReleaseNotes(
|
||||
|
||||
// ensureUpgradesAvailable ensures that there are available upgrades.
|
||||
func ensureUpgradesAvailable(
|
||||
app app.App,
|
||||
app appPkg.App,
|
||||
versions []string,
|
||||
availableUpgrades *[]string,
|
||||
deployMeta stack.DeployMeta,
|
||||
@ -388,7 +404,7 @@ func ensureUpgradesAvailable(
|
||||
// validateUpgradeVersionArg validates the specific version.
|
||||
func validateUpgradeVersionArg(
|
||||
specificVersion string,
|
||||
app app.App,
|
||||
app appPkg.App,
|
||||
deployMeta stack.DeployMeta,
|
||||
) error {
|
||||
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
|
||||
@ -415,7 +431,7 @@ func validateUpgradeVersionArg(
|
||||
|
||||
// ensureDeployed ensures the app is deployed and if so, returns deployment
|
||||
// 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()))
|
||||
|
||||
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
||||
|
@ -50,6 +50,9 @@ func DeployOverview(
|
||||
toDeployVersion string,
|
||||
releaseNotes string,
|
||||
warnMessages []string,
|
||||
secrets string,
|
||||
configs string,
|
||||
images string,
|
||||
) error {
|
||||
deployConfig := "compose.yml"
|
||||
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
|
||||
@ -80,6 +83,24 @@ func DeployOverview(
|
||||
{i18n.G("CURRENT DEPLOYMENT"), formatter.BoldDirtyDefault(deployedVersion)},
|
||||
{i18n.G("ENV VERSION"), formatter.BoldDirtyDefault(envVersion)},
|
||||
{i18n.G("NEW DEPLOYMENT"), formatter.BoldDirtyDefault(toDeployVersion)},
|
||||
{"", ""},
|
||||
{i18n.G("IMAGES"), images},
|
||||
}
|
||||
|
||||
if len(secrets) > 0 {
|
||||
secretsRows := [][]string{
|
||||
{"", ""},
|
||||
{i18n.G("SECRETS"), secrets},
|
||||
}
|
||||
rows = append(rows, secretsRows...)
|
||||
}
|
||||
|
||||
if len(configs) > 0 {
|
||||
configsRows := [][]string{
|
||||
{"", ""},
|
||||
{i18n.G("CONFIGS"), configs},
|
||||
}
|
||||
rows = append(rows, configsRows...)
|
||||
}
|
||||
|
||||
deployType := getDeployType(deployedVersion, toDeployVersion)
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/deploy"
|
||||
"coopcloud.tech/abra/pkg/envfile"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/lint"
|
||||
@ -335,21 +336,6 @@ func processRecipeRepoVersion(r recipe.Recipe, version string) error {
|
||||
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.
|
||||
func createDeployConfig(r recipe.Recipe, stackName string, env envfile.AppEnv) (*composetypes.Config, stack.Deploy, error) {
|
||||
env["STACK_NAME"] = stackName
|
||||
@ -444,7 +430,7 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri
|
||||
return err
|
||||
}
|
||||
|
||||
if err = mergeAbraShEnv(app.Recipe, app.Env); err != nil {
|
||||
if err = deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package client
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@ -37,3 +38,12 @@ func RemoveConfigs(cl *client.Client, ctx context.Context, configNames []string,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
} else {
|
||||
return "", "", errors.New(i18n.G("can't parse version from config '%s'", fullName))
|
||||
}
|
||||
}
|
||||
|
89
pkg/client/configs_test.go
Normal file
89
pkg/client/configs_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -17,3 +17,11 @@ func StoreSecret(cl *client.Client, secretName, secretValue string) error {
|
||||
|
||||
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
58
pkg/client/secret_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
243
pkg/deploy/utils.go
Normal file
243
pkg/deploy/utils.go
Normal file
@ -0,0 +1,243 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/envfile"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/secret"
|
||||
|
||||
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 {
|
||||
log.Warnf("different versions for config '%s', '%s' and %s'", configBaseName, existingConfigVersion, configVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func GetImageNameAndTag(imageName string) (string, string, error) {
|
||||
imageParts := regexp.MustCompile("^([^:]*):([^@]*)@?").FindSubmatch([]byte(imageName))
|
||||
|
||||
if len(imageParts) == 0 {
|
||||
return "", "", errors.New("can't determine image version for image '%s'")
|
||||
}
|
||||
|
||||
imageBaseName := string(imageParts[1])
|
||||
imageTag := string(imageParts[2])
|
||||
|
||||
return imageBaseName, imageTag, 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
|
||||
|
||||
imageBaseName, imageTag, err := GetImageNameAndTag(imageName)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
continue
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Warnf("different versions for image '%s', '%s' and %s'", imageBaseName, existingImageVersion, imageTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return images, nil
|
||||
}
|
||||
|
||||
func GatherSecretsForDeploy(cl *dockerClient.Client, app appPkg.App) ([]string, error) {
|
||||
|
||||
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) ([]string, error) {
|
||||
// Get current configs from existing deployment
|
||||
currentConfigs, err := GetConfigsForStack(cl, app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Deployed config names: %v", currentConfigs)
|
||||
|
||||
// Get new configs from the compose specification
|
||||
newConfigs := compose.Configs
|
||||
|
||||
var configInfo []string
|
||||
for configName := range newConfigs {
|
||||
log.Debugf("Searching abra.sh for version for %s", configName)
|
||||
versionKey := strings.ToUpper(configName) + "_VERSION"
|
||||
newVersion, exists := abraShEnv[versionKey]
|
||||
if !exists {
|
||||
log.Warnf("No version found for config %s", configName)
|
||||
configInfo = append(configInfo, fmt.Sprintf("%s: ? (missing version)", configName))
|
||||
continue
|
||||
}
|
||||
|
||||
if currentVersion, exists := currentConfigs[configName]; exists {
|
||||
if currentVersion == newVersion {
|
||||
configInfo = append(configInfo, fmt.Sprintf("%s: %s (unchanged)", configName, newVersion))
|
||||
} else {
|
||||
configInfo = append(configInfo, fmt.Sprintf("%s: %s → %s", configName, currentVersion, newVersion))
|
||||
}
|
||||
} else {
|
||||
configInfo = append(configInfo, fmt.Sprintf("%s: %s (new)", configName, newVersion))
|
||||
}
|
||||
}
|
||||
|
||||
return configInfo, nil
|
||||
}
|
||||
|
||||
func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composetypes.Config) ([]string, error) {
|
||||
|
||||
// Get current images from existing deployment
|
||||
currentImages, err := GetImagesForStack(cl, app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Deployed images: %v", currentImages)
|
||||
|
||||
// Proposed new images from the compose files
|
||||
newImages := make(map[string]string)
|
||||
|
||||
for _, service := range compose.Services {
|
||||
imageBaseName, imageTag, err := GetImageNameAndTag(service.Image)
|
||||
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("different versions for image '%s', '%s' and %s'", imageBaseName, existingImageVersion, imageTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Debugf("Proposed images: %v", newImages)
|
||||
|
||||
var imageInfo []string
|
||||
for newImageName, newImageVersion := range newImages {
|
||||
if currentVersion, exists := currentImages[newImageName]; exists {
|
||||
if currentVersion == newImageVersion {
|
||||
imageInfo = append(imageInfo, fmt.Sprintf("%s: %s (unchanged)", newImageName, newImageVersion))
|
||||
} else {
|
||||
imageInfo = append(imageInfo, fmt.Sprintf("%s: %s → %s", newImageName, currentVersion, newImageVersion))
|
||||
}
|
||||
} else {
|
||||
imageInfo = append(imageInfo, fmt.Sprintf("%s: %s (new)", newImageName, newImageVersion))
|
||||
}
|
||||
}
|
||||
|
||||
return imageInfo, nil
|
||||
}
|
87
pkg/deploy/utils_test.go
Normal file
87
pkg/deploy/utils_test.go
Normal file
@ -0,0 +1,87 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetImageNameAndTag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
imageName string
|
||||
expectedName string
|
||||
expectedTag string
|
||||
expectError bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "standard image with tag",
|
||||
imageName: "nginx:1.23",
|
||||
expectedName: "nginx",
|
||||
expectedTag: "1.23",
|
||||
expectError: false,
|
||||
description: "should parse standard image name with tag",
|
||||
},
|
||||
{
|
||||
name: "image with digest",
|
||||
imageName: "nginx:1.23@sha256:abc123",
|
||||
expectedName: "nginx",
|
||||
expectedTag: "1.23",
|
||||
expectError: false,
|
||||
description: "should parse image with digest, ignoring digest part",
|
||||
},
|
||||
{
|
||||
name: "image with latest tag",
|
||||
imageName: "redis:latest",
|
||||
expectedName: "redis",
|
||||
expectedTag: "latest",
|
||||
expectError: false,
|
||||
description: "should parse image with latest tag",
|
||||
},
|
||||
{
|
||||
name: "image with numeric tag",
|
||||
imageName: "postgres:14",
|
||||
expectedName: "postgres",
|
||||
expectedTag: "14",
|
||||
expectError: false,
|
||||
description: "should parse image with numeric tag",
|
||||
},
|
||||
{
|
||||
name: "image with complex name",
|
||||
imageName: "registry.example.com/myapp/api:v1.2.3",
|
||||
expectedName: "registry.example.com/myapp/api",
|
||||
expectedTag: "v1.2.3",
|
||||
expectError: false,
|
||||
description: "should parse image with registry prefix and complex name",
|
||||
},
|
||||
{
|
||||
name: "image without tag",
|
||||
imageName: "nginx",
|
||||
expectError: true,
|
||||
description: "should error when no tag present",
|
||||
},
|
||||
{
|
||||
name: "empty image name",
|
||||
imageName: "",
|
||||
expectError: true,
|
||||
description: "should error on empty image name",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
name, tag, err := GetImageNameAndTag(tt.imageName)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, name)
|
||||
assert.Empty(t, tag)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedName, name)
|
||||
assert.Equal(t, tt.expectedTag, tag)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -280,7 +280,7 @@ type secretStatus struct {
|
||||
type secretStatuses []secretStatus
|
||||
|
||||
// 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) {
|
||||
var secStats secretStatuses
|
||||
|
||||
@ -306,7 +306,7 @@ func PollSecretsStatus(cl *dockerClient.Client, app appPkg.App) (secretStatuses,
|
||||
|
||||
remoteSecretNames := make(map[string]bool)
|
||||
for _, cont := range secretList {
|
||||
remoteSecretNames[cont.Spec.Annotations.Name] = true
|
||||
remoteSecretNames[cont.Spec.Name] = true
|
||||
}
|
||||
|
||||
for secretName, val := range secretsConfig {
|
||||
|
Reference in New Issue
Block a user