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
|
|
}
|