package recipe import ( "fmt" "io/ioutil" "os" "path/filepath" "strings" "coopcloud.tech/abra/pkg/formatter" "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 Recipe2) 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.Debugf("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.Debugf("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, fmt.Errorf("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.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", ")) log.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), r.Name) return composeFiles, nil } // UpdateTag updates an image tag in-place on file system local compose files. func (r Recipe2) 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.Debugf("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.Debugf("unable to parse %s, skipping", img) continue } composeImage := formatter.StripTagMeta(reference.Path(img)) log.Debugf("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.Debugf("updating %s to %s in %s", old, new, compose.Filename) if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil { return false, err } } } } return false, nil } // UpdateLabel updates a label in-place on file system local compose files. func (r Recipe2) 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.Debugf("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 := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value) replacedBytes := strings.Replace(string(bytes), old, label, -1) if old == label { log.Warnf("%s is already set, nothing to do?", label) return nil } log.Debugf("updating %s to %s in %s", old, label, compose.Filename) if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil { return err } log.Infof("synced label %s to service %s", label, serviceName) } } if !discovered { log.Warn("no existing label found, automagic insertion not supported yet") log.Fatalf("add '- \"%s\"' manually to the 'app' service in %s", label, composeFile) } } return nil }