diff --git a/cli/internal/cli.go b/cli/internal/cli.go index 1d32b475..1b07a475 100644 --- a/cli/internal/cli.go +++ b/cli/internal/cli.go @@ -19,5 +19,6 @@ var ( Minor bool NoDomainChecks bool Patch bool + PinDigests bool ShowUnchanged bool ) diff --git a/cli/recipe/upgrade.go b/cli/recipe/upgrade.go index 3cac573c..a7e6d6eb 100644 --- a/cli/recipe/upgrade.go +++ b/cli/recipe/upgrade.go @@ -293,12 +293,31 @@ interface.`), } } if upgradeTag != "skip" { - ok, err := recipe.UpdateTag(image, upgradeTag) + var ok bool + var err error + var resolvedDigest string + + if internal.PinDigests { + digest, dErr := client.GetImageDigest(img, upgradeTag) + if dErr != nil { + log.Fatal(i18n.G("failed to resolve digest for %s:%s: %s", image, upgradeTag, dErr.Error())) + } + resolvedDigest = digest + + ok, err = recipe.UpdatePinnedTag(image, upgradeTag, digest) + } else { + ok, err = recipe.UpdateTag(image, upgradeTag) + } + if err != nil { log.Fatal(err) } if ok { - log.Info(i18n.G("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)) + if internal.PinDigests { + log.Info(i18n.G("tag upgraded and pinned from %s to %s@%s for %s", tag.String(), upgradeTag, resolvedDigest, image)) + } else { + log.Info(i18n.G("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)) + } } } else { if !internal.NoInput { @@ -417,4 +436,12 @@ func init() { false, i18n.G("commit changes"), ) + + RecipeUpgradeCommand.Flags().BoolVarP( + &internal.PinDigests, + i18n.G("pindigests"), + i18n.GC("p", "pin container image by digest"), + false, + i18n.G("pin the container image version by manifest digest (image:tag@sha256:...)"), + ) } diff --git a/pkg/recipe/compose.go b/pkg/recipe/compose.go index f90c5aaa..6e39dc9e 100644 --- a/pkg/recipe/compose.go +++ b/pkg/recipe/compose.go @@ -122,6 +122,7 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) { log.Debug(i18n.G("considering %s config(s) for tag update", strings.Join(composeFiles, ", "))) + updated := false for _, composeFile := range composeFiles { opts := stack.Deploy{Composefiles: []string{composeFile}} @@ -166,6 +167,10 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) { old := fmt.Sprintf("%s:%s", composeImage, composeTag) new := fmt.Sprintf("%s:%s", composeImage, tag) + if old == new { + continue + } + replacedBytes := strings.Replace(string(bytes), old, new, -1) log.Debug(i18n.G("updating %s to %s in %s", old, new, compose.Filename)) @@ -173,11 +178,72 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) { if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil { return false, err } + updated = true } } } - return false, nil + return updated, nil +} + +// UpdatePinnedTag updates an image reference to a pinned version (image:tag@sha256:...) in-place on local compose files. +func (r Recipe) UpdatePinnedTag(image, tag, digest string) (bool, error) { + glob := fmt.Sprintf("%s/compose**yml", r.Dir) + image = formatter.StripTagMeta(image) + + composeFiles, err := filepath.Glob(glob) + if err != nil { + return false, err + } + + updated := false + 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 + } + + img, _ := reference.ParseNormalizedNamed(formatter.StripDigest(service.Image)) + composeImage := formatter.StripTagMeta(reference.Path(img)) + + if image == composeImage { + bytes, err := ioutil.ReadFile(composeFile) + if err != nil { + return false, err + } + + old := service.Image + new := fmt.Sprintf("%s:%s@%s", composeImage, tag, digest) + + if old == new { + continue + } + + replacedBytes := strings.Replace(string(bytes), old, new, -1) + + log.Debug(i18n.G("updating pinned reference %s to %s in %s", old, new, compose.Filename)) + + if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil { + return false, err + } + updated = true + } + } + } + + return updated, nil } // UpdateLabel updates a label in-place on file system local compose files.