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/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 { log.Warnf("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("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, showUnchanged bool) ([]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 { if (showUnchanged) { 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, showUnchanged bool) ([]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 { 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("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 { if showUnchanged { imageInfo = append(imageInfo, fmt.Sprintf("%s: %s (unchanged)", formatter.StripTagMeta(newImageName), newImageVersion)) } } else { imageInfo = append(imageInfo, fmt.Sprintf("%s: %s → %s", formatter.StripTagMeta(newImageName), currentVersion, newImageVersion)) } } else { imageInfo = append(imageInfo, fmt.Sprintf("%s: %s (new)", formatter.StripTagMeta(newImageName), newImageVersion)) } } return imageInfo, nil }