package deploy import ( "context" "errors" "sort" "strings" appPkg "coopcloud.tech/abra/pkg/app" "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 } // 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. 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 := GetEntityNameAndVersion(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(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 { 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, showUnchanged bool) ([]string, error) { // Get current secrets from existing deployment currentSecrets, err := GetSecretsForStack(cl, app) if err != nil { return nil, err } 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.Slice(newSecrets, func(i, j int) bool { return newSecrets[i].LocalName < newSecrets[j].LocalName }) 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 } 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)) // 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)) versionKey := strings.ToUpper(configName) + "_VERSION" newVersion, exists := abraShEnv[versionKey] if !exists { log.Warnf(i18n.G("no version found for config %s", configName)) configInfo = append(configInfo, i18n.G("%s: ? (missing version)", configName)) continue } if currentVersion, exists := currentConfigs[configName]; exists { if currentVersion == newVersion { if showUnchanged { configInfo = append(configInfo, i18n.G("%s: %s (unchanged)", configName, newVersion)) } } else { configInfo = append(configInfo, i18n.G("%s: %s → %s", configName, currentVersion, newVersion)) } } else { configInfo = append(configInfo, i18n.G("%s: %s (new)", configName, newVersion)) } } return configInfo, nil } func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composetypes.Config, showUnchanged bool) ([]string, error) { // Get current images from existing deployment currentImages, err := GetImagesForStack(cl, app) if err != nil { return nil, err } log.Debugf(i18n.G("deployed images: %v", currentImages)) // Proposed new images from the compose files newImages := make(map[string]string) for _, service := range compose.Services { imageParsed, err := reference.ParseNormalizedNamed(service.Image) 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 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)) } } } log.Debugf(i18n.G("proposed images: %v", newImages)) 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)) } } 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)) } } return imageInfo, nil }