forked from toolshed/abra
		
	
		
			
				
	
	
		
			317 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			317 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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(i18n.G("read v:%s k: %s", v, k))
 | |
| 		env[k] = v
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // GetEntityNameAndVersion parses a full 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(i18n.G("current secrets: %v", currentSecrets))
 | |
| 
 | |
| 	newSecrets, err := secret.PollSecretsStatus(cl, app)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	log.Debugf(i18n.G("new secrets: %v", newSecrets))
 | |
| 
 | |
| 	// 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
 | |
| }
 |