All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			See #483
		
			
				
	
	
		
			255 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			255 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package recipe
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io/ioutil"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 
 | |
| 	"coopcloud.tech/abra/pkg/formatter"
 | |
| 	"coopcloud.tech/abra/pkg/i18n"
 | |
| 	"coopcloud.tech/abra/pkg/log"
 | |
| 	"coopcloud.tech/abra/pkg/upstream/stack"
 | |
| 	loader "coopcloud.tech/abra/pkg/upstream/stack"
 | |
| 	"github.com/distribution/reference"
 | |
| 	composetypes "github.com/docker/cli/cli/compose/types"
 | |
| )
 | |
| 
 | |
| // GetComposeFiles gets the list of compose files for an app (or recipe if you
 | |
| // don't already have an app) which should be merged into a composetypes.Config
 | |
| // while respecting the COMPOSE_FILE env var.
 | |
| func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) {
 | |
| 	composeFileEnvVar, ok := appEnv["COMPOSE_FILE"]
 | |
| 	if !ok {
 | |
| 		if err := ensurePathExists(r.ComposePath); err != nil {
 | |
| 			return []string{}, err
 | |
| 		}
 | |
| 		log.Debug(i18n.G("no COMPOSE_FILE detected, loading default: %s", r.ComposePath))
 | |
| 		return []string{r.ComposePath}, nil
 | |
| 	}
 | |
| 
 | |
| 	if !strings.Contains(composeFileEnvVar, ":") {
 | |
| 		path := fmt.Sprintf("%s/%s", r.Dir, composeFileEnvVar)
 | |
| 		if err := ensurePathExists(path); err != nil {
 | |
| 			return []string{}, err
 | |
| 		}
 | |
| 		log.Debug(i18n.G("COMPOSE_FILE detected, loading %s", path))
 | |
| 		return []string{path}, nil
 | |
| 	}
 | |
| 
 | |
| 	var composeFiles []string
 | |
| 
 | |
| 	numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1
 | |
| 	envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles)
 | |
| 	if len(envVars) != numComposeFiles {
 | |
| 		return composeFiles, errors.New(i18n.G("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar))
 | |
| 	}
 | |
| 
 | |
| 	for _, file := range envVars {
 | |
| 		path := fmt.Sprintf("%s/%s", r.Dir, file)
 | |
| 		if err := ensurePathExists(path); err != nil {
 | |
| 			return composeFiles, err
 | |
| 		}
 | |
| 		composeFiles = append(composeFiles, path)
 | |
| 	}
 | |
| 
 | |
| 	log.Debug(i18n.G("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", ")))
 | |
| 	log.Debug(i18n.G("retrieved %s configs for %s", strings.Join(composeFiles, ", "), r.Name))
 | |
| 
 | |
| 	return composeFiles, nil
 | |
| }
 | |
| 
 | |
| func (r Recipe) GetComposeConfig(env map[string]string) (*composetypes.Config, error) {
 | |
| 	pattern := fmt.Sprintf("%s/compose**yml", r.Dir)
 | |
| 	composeFiles, err := filepath.Glob(pattern)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	if len(composeFiles) == 0 {
 | |
| 		return nil, errors.New(i18n.G("%s is missing a compose.yml or compose.*.yml file?", r.Name))
 | |
| 	}
 | |
| 
 | |
| 	if env == nil {
 | |
| 		env, err = r.SampleEnv()
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	opts := stack.Deploy{Composefiles: composeFiles}
 | |
| 	config, err := loader.LoadComposefile(opts, env)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return config, nil
 | |
| }
 | |
| 
 | |
| // GetVersionLabelLocal retrieves the version label on the local recipe config
 | |
| func (r Recipe) GetVersionLabelLocal() (string, error) {
 | |
| 	var label string
 | |
| 	config, err := r.GetComposeConfig(nil)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	for _, service := range config.Services {
 | |
| 		for label, value := range service.Deploy.Labels {
 | |
| 			if strings.HasPrefix(label, "coop-cloud") && strings.Contains(label, "version") {
 | |
| 				return value, nil
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if label == "" {
 | |
| 		return label, errors.New(i18n.G("%s has no version label? try running \"abra recipe sync %s\" first?", r.Name, r.Name))
 | |
| 	}
 | |
| 
 | |
| 	return label, nil
 | |
| }
 | |
| 
 | |
| // UpdateTag updates an image tag in-place on file system local compose files.
 | |
| func (r Recipe) UpdateTag(image, tag string) (bool, error) {
 | |
| 	fullPattern := fmt.Sprintf("%s/compose**yml", r.Dir)
 | |
| 	image = formatter.StripTagMeta(image)
 | |
| 
 | |
| 	composeFiles, err := filepath.Glob(fullPattern)
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 
 | |
| 	log.Debug(i18n.G("considering %s config(s) for tag update", strings.Join(composeFiles, ", ")))
 | |
| 
 | |
| 	for _, composeFile := range composeFiles {
 | |
| 		opts := stack.Deploy{Composefiles: []string{composeFile}}
 | |
| 
 | |
| 		sampleEnv, err := r.SampleEnv()
 | |
| 		if err != nil {
 | |
| 			return false, err
 | |
| 		}
 | |
| 
 | |
| 		compose, err := loader.LoadComposefile(opts, sampleEnv)
 | |
| 		if err != nil {
 | |
| 			return false, err
 | |
| 		}
 | |
| 
 | |
| 		for _, service := range compose.Services {
 | |
| 			if service.Image == "" {
 | |
| 				continue // may be a compose.$optional.yml file
 | |
| 			}
 | |
| 
 | |
| 			img, _ := reference.ParseNormalizedNamed(service.Image)
 | |
| 			if err != nil {
 | |
| 				return false, err
 | |
| 			}
 | |
| 
 | |
| 			var composeTag string
 | |
| 			switch img.(type) {
 | |
| 			case reference.NamedTagged:
 | |
| 				composeTag = img.(reference.NamedTagged).Tag()
 | |
| 			default:
 | |
| 				log.Debug(i18n.G("unable to parse %s, skipping", img))
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			composeImage := formatter.StripTagMeta(reference.Path(img))
 | |
| 
 | |
| 			log.Debug(i18n.G("parsed %s from %s", composeTag, service.Image))
 | |
| 
 | |
| 			if image == composeImage {
 | |
| 				bytes, err := ioutil.ReadFile(composeFile)
 | |
| 				if err != nil {
 | |
| 					return false, err
 | |
| 				}
 | |
| 
 | |
| 				old := fmt.Sprintf("%s:%s", composeImage, composeTag)
 | |
| 				new := fmt.Sprintf("%s:%s", composeImage, tag)
 | |
| 				replacedBytes := strings.Replace(string(bytes), old, new, -1)
 | |
| 
 | |
| 				log.Debug(i18n.G("updating %s to %s in %s", old, new, compose.Filename))
 | |
| 
 | |
| 				if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
 | |
| 					return false, err
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return false, nil
 | |
| }
 | |
| 
 | |
| // UpdateLabel updates a label in-place on file system local compose files.
 | |
| func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
 | |
| 	fullPattern := fmt.Sprintf("%s/%s", r.Dir, pattern)
 | |
| 	composeFiles, err := filepath.Glob(fullPattern)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	log.Debug(i18n.G("considering %s config(s) for label update", strings.Join(composeFiles, ", ")))
 | |
| 
 | |
| 	for _, composeFile := range composeFiles {
 | |
| 		opts := stack.Deploy{Composefiles: []string{composeFile}}
 | |
| 
 | |
| 		sampleEnv, err := r.SampleEnv()
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		compose, err := loader.LoadComposefile(opts, sampleEnv)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		serviceExists := false
 | |
| 		var service composetypes.ServiceConfig
 | |
| 		for _, s := range compose.Services {
 | |
| 			if s.Name == serviceName {
 | |
| 				service = s
 | |
| 				serviceExists = true
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if !serviceExists {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		discovered := false
 | |
| 		for oldLabel, value := range service.Deploy.Labels {
 | |
| 			if strings.HasPrefix(oldLabel, "coop-cloud") && strings.Contains(oldLabel, "version") {
 | |
| 				discovered = true
 | |
| 
 | |
| 				bytes, err := ioutil.ReadFile(composeFile)
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 
 | |
| 				old := i18n.G("coop-cloud.${STACK_NAME}.version=%s", value)
 | |
| 				replacedBytes := strings.Replace(string(bytes), old, label, -1)
 | |
| 
 | |
| 				if old == label {
 | |
| 					log.Warnf(i18n.G("%s is already set, nothing to do?", label))
 | |
| 					return nil
 | |
| 				}
 | |
| 
 | |
| 				log.Debug(i18n.G("updating %s to %s in %s", old, label, compose.Filename))
 | |
| 
 | |
| 				if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 
 | |
| 				log.Infof(i18n.G("synced label %s to service %s", label, serviceName))
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if !discovered {
 | |
| 			log.Warn(i18n.G("no existing label found, automagic insertion not supported yet"))
 | |
| 			log.Fatal(i18n.G("add '- \"%s\"' manually to the 'app' service in %s", label, composeFile))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 |