feat: show proposed secret version changes during deploy
This commit is contained in:
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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))
|
|
||||||
}
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
Reference in New Issue
Block a user