forked from toolshed/abra
		
	
		
			
				
	
	
		
			245 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			245 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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
 | |
| }
 |