Allow digests in docker images #957
@@ -129,6 +129,9 @@ interface.`),
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// digest pinned images shared between services are only checked once
|
||||
digestChecked := make(map[string]bool)
|
||||
|
||||
for _, service := range config.Services {
|
||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||
if err != nil {
|
||||
@@ -178,6 +181,13 @@ interface.`),
|
||||
|
||||
sort.Sort(tagcmp.ByTagDesc(compatible))
|
||||
|
||||
if canonical, ok := img.(reference.Canonical); ok && !digestChecked[service.Image] {
|
||||
digestChecked[service.Image] = true
|
||||
if err := refreshDigest(recipe, img, image, canonical.Digest().String()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(compatible) == 0 && !allTags {
|
||||
log.Info(i18n.G("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
|
||||
continue // skip on to the next tag and don't update any compose files
|
||||
@@ -364,6 +374,47 @@ interface.`),
|
||||
},
|
||||
}
|
||||
|
||||
// refreshDigest checks whether the current tag of a digest pinned image was
|
||||
// re-published upstream with a new digest and offers to update the pin.
|
||||
func refreshDigest(recipe recipePkg.Recipe, img reference.Named, image, digest string) error {
|
||||
tag := img.(reference.NamedTagged).Tag()
|
||||
|
||||
registryDigest, err := client.GetRegistryDigest(img, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if registryDigest == digest {
|
||||
log.Debug(i18n.G("digest of %s:%s is up to date", image, tag))
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Warn(i18n.G("%s:%s was re-published upstream (digest %s → %s)", image, tag, digest, registryDigest))
|
||||
|
||||
update := false
|
||||
if !internal.NoInput {
|
||||
prompt := &survey.Confirm{
|
||||
Message: i18n.G("update digest of %s:%s?", image, tag),
|
||||
Default: true,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &update); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !update {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := recipe.UpdateTag(image, tag); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info(i18n.G("digest updated for %s:%s", image, tag))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
allTags bool
|
||||
createCommit bool
|
||||
|
||||
@@ -21,6 +21,7 @@ require (
|
||||
github.com/leonelquinteros/gotext v1.8.0
|
||||
github.com/moby/sys/signal v0.7.1
|
||||
github.com/moby/term v0.5.2
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/schollz/progressbar/v3 v3.19.1
|
||||
github.com/spf13/cobra v1.10.1
|
||||
@@ -105,7 +106,6 @@ require (
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runc v1.1.13 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.7.0 // indirect
|
||||
|
||||
+52
-1
@@ -4,18 +4,35 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"github.com/containers/image/docker"
|
||||
"github.com/containers/image/manifest"
|
||||
"github.com/containers/image/types"
|
||||
"github.com/distribution/reference"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// NOTE: containers/image does not request OCI image indexes by default.
|
||||
// Many multi-arch images (e.g. on Docker Hub) are published as OCI indexes
|
||||
// and the registry refuses to serve them without the matching Accept header.
|
||||
if !slices.Contains(manifest.DefaultRequestedManifestMIMETypes, imgspecv1.MediaTypeImageIndex) {
|
||||
manifest.DefaultRequestedManifestMIMETypes = append(
|
||||
manifest.DefaultRequestedManifestMIMETypes,
|
||||
imgspecv1.MediaTypeImageIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// GetRegistryTags retrieves all tags of an image from a container registry.
|
||||
func GetRegistryTags(img reference.Named) ([]string, error) {
|
||||
var tags []string
|
||||
|
||||
ref, err := docker.ParseReference(fmt.Sprintf("//%s", img))
|
||||
// NOTE: strip tag and digest, containers/image refuses references
|
||||
// which have both (e.g. nextcloud:34.0.5-fpm@sha256:...)
|
||||
ref, err := docker.ParseReference(fmt.Sprintf("//%s", reference.TrimNamed(img)))
|
||||
if err != nil {
|
||||
return tags, errors.New(i18n.G("failed to parse image %s, saw: %s", img, err.Error()))
|
||||
}
|
||||
@@ -28,3 +45,37 @@ func GetRegistryTags(img reference.Named) ([]string, error) {
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// GetRegistryDigest retrieves the manifest digest of an image tag from a
|
||||
// container registry. For multi-arch images, this is the digest of the image
|
||||
// index, which is what Docker resolves "image:tag" to.
|
||||
func GetRegistryDigest(img reference.Named, tag string) (string, error) {
|
||||
tagged, err := reference.WithTag(reference.TrimNamed(img), tag)
|
||||
if err != nil {
|
||||
return "", errors.New(i18n.G("failed to parse image %s, saw: %s", img, err.Error()))
|
||||
}
|
||||
|
||||
ref, err := docker.ParseReference(fmt.Sprintf("//%s", tagged))
|
||||
if err != nil {
|
||||
return "", errors.New(i18n.G("failed to parse image %s, saw: %s", tagged, err.Error()))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
src, err := ref.NewImageSource(ctx, &types.SystemContext{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
manifestBytes, _, err := src.GetManifest(ctx, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
digest, err := manifest.Digest(manifestBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return digest.String(), nil
|
||||
}
|
||||
|
||||
+87
-62
@@ -7,13 +7,13 @@
|
||||
msgid ""
|
||||
msgstr "Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: EMAIL\n"
|
||||
"POT-Creation-Date: 2026-10-14 10:32-0400\n"
|
||||
"POT-Creation-Date: 2026-10-22 21:25+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: ./cli/app/cp.go:38
|
||||
@@ -219,7 +219,7 @@ msgstr ""
|
||||
msgid "%s created (version: %s)"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:335
|
||||
#: ./cli/recipe/upgrade.go:345
|
||||
#, c-format
|
||||
msgid "%s currently has these unstaged changes 👇"
|
||||
msgstr ""
|
||||
@@ -304,7 +304,7 @@ msgstr ""
|
||||
msgid "%s has no secrets to generate, skipping..."
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:107
|
||||
#: ./pkg/recipe/compose.go:109
|
||||
#, c-format
|
||||
msgid "%s has no version label? try running \"abra recipe sync %s\" first?"
|
||||
msgstr ""
|
||||
@@ -324,12 +324,12 @@ msgstr ""
|
||||
msgid "%s is already fetched"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:233
|
||||
#: ./pkg/recipe/compose.go:277
|
||||
#, c-format
|
||||
msgid "%s is already set, nothing to do?"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:72
|
||||
#: ./pkg/recipe/compose.go:74
|
||||
#, c-format
|
||||
msgid "%s is missing a compose.yml or compose.*.yml file?"
|
||||
msgstr ""
|
||||
@@ -379,7 +379,7 @@ msgstr ""
|
||||
msgid "%s must first be deployed on %s before moving"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:150
|
||||
#: ./cli/recipe/upgrade.go:153
|
||||
#, c-format
|
||||
msgid "%s not considered semver-like"
|
||||
msgstr ""
|
||||
@@ -549,6 +549,11 @@ msgstr ""
|
||||
msgid "%s: waiting %d seconds before next retry"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:392
|
||||
#, c-format
|
||||
msgid "%s:%s was re-published upstream (digest %s → %s)"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/app/upgrade.go:425
|
||||
#, c-format
|
||||
msgid "'%s' is not a known version"
|
||||
@@ -638,17 +643,17 @@ msgstr ""
|
||||
msgid "CHAOS DEPLOY"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:47
|
||||
#: ./pkg/recipe/compose.go:49
|
||||
#, c-format
|
||||
msgid "COMPOSE_FILE (=\"%s\") parsing failed?"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:58
|
||||
#: ./pkg/recipe/compose.go:60
|
||||
#, c-format
|
||||
msgid "COMPOSE_FILE detected (%s), loading %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:38
|
||||
#: ./pkg/recipe/compose.go:40
|
||||
#, c-format
|
||||
msgid "COMPOSE_FILE detected, loading %s"
|
||||
msgstr ""
|
||||
@@ -1687,7 +1692,7 @@ msgctxt "recipe fetch"
|
||||
msgid "a"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:408
|
||||
#: ./cli/recipe/upgrade.go:459
|
||||
msgctxt "recipe upgrade"
|
||||
msgid "a"
|
||||
msgstr ""
|
||||
@@ -1737,7 +1742,7 @@ msgstr ""
|
||||
msgid "accepts at most 2 args with --local/-l"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:249
|
||||
#: ./pkg/recipe/compose.go:293
|
||||
#, c-format
|
||||
msgid "add '- \"%s\"' manually to the 'app' service in %s"
|
||||
msgstr ""
|
||||
@@ -1780,7 +1785,7 @@ msgstr ""
|
||||
msgid "all-services"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:407
|
||||
#: ./cli/recipe/upgrade.go:458
|
||||
msgid "all-tags"
|
||||
msgstr ""
|
||||
|
||||
@@ -1919,7 +1924,7 @@ msgstr ""
|
||||
msgid "c"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:416
|
||||
#: ./cli/recipe/upgrade.go:467
|
||||
msgctxt "recipe upgrade"
|
||||
msgid "c"
|
||||
msgstr ""
|
||||
@@ -1928,7 +1933,7 @@ msgstr ""
|
||||
msgid "can not insert from file and read from stdin"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:323
|
||||
#: ./cli/recipe/upgrade.go:333
|
||||
#, c-format
|
||||
msgid "can upgrade service: %s, image: %s, tag: %s ::"
|
||||
msgstr ""
|
||||
@@ -2108,7 +2113,7 @@ msgstr ""
|
||||
msgid "chore: publish new catalogue release changes"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:352
|
||||
#: ./cli/recipe/upgrade.go:362
|
||||
msgid "chore: update image tags"
|
||||
msgstr ""
|
||||
|
||||
@@ -2170,19 +2175,19 @@ msgstr ""
|
||||
msgid "commandconn: starting %s with %v"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:415
|
||||
#: ./cli/recipe/upgrade.go:466
|
||||
msgid "commit"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:418
|
||||
#: ./cli/recipe/upgrade.go:469
|
||||
msgid "commit changes"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:342
|
||||
#: ./cli/recipe/upgrade.go:352
|
||||
msgid "commit changes?"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:356
|
||||
#: ./cli/recipe/upgrade.go:366
|
||||
#, c-format
|
||||
msgid "committed changes as '%s'"
|
||||
msgstr ""
|
||||
@@ -2230,12 +2235,12 @@ msgstr ""
|
||||
msgid "connection timed out for %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:191
|
||||
#: ./pkg/recipe/compose.go:235
|
||||
#, c-format
|
||||
msgid "considering %s config(s) for label update"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:123
|
||||
#: ./pkg/recipe/compose.go:132
|
||||
#, c-format
|
||||
msgid "considering %s config(s) for tag update"
|
||||
msgstr ""
|
||||
@@ -2258,7 +2263,7 @@ msgstr ""
|
||||
msgid "context lacks Docker endpoint"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:229
|
||||
#: ./pkg/recipe/compose.go:273
|
||||
#, c-format
|
||||
msgid "coop-cloud.${STACK_NAME}.version=%s"
|
||||
msgstr ""
|
||||
@@ -2465,12 +2470,12 @@ msgstr ""
|
||||
msgid "detected ABRA_CI=1"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:204
|
||||
#: ./cli/recipe/upgrade.go:214
|
||||
#, c-format
|
||||
msgid "detected compatible upgradable tags %s for %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:177
|
||||
#: ./cli/recipe/upgrade.go:180
|
||||
#, c-format
|
||||
msgid "detected potential upgradable tags %s for %s"
|
||||
msgstr ""
|
||||
@@ -2509,6 +2514,16 @@ msgstr ""
|
||||
msgid "different versions for secret '%s', '%s' and %s'"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:388
|
||||
#, c-format
|
||||
msgid "digest of %s:%s is up to date"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:413
|
||||
#, c-format
|
||||
msgid "digest updated for %s:%s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/recipe.go:202
|
||||
#, c-format
|
||||
msgid "dir: %s, "
|
||||
@@ -2658,7 +2673,7 @@ msgstr ""
|
||||
msgid "ensuring env version %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:285
|
||||
#: ./cli/recipe/upgrade.go:295
|
||||
msgid "enter / return to confirm, choose 'skip' to not upgrade this tag, vim mode is enabled"
|
||||
msgstr ""
|
||||
|
||||
@@ -2839,7 +2854,7 @@ msgstr ""
|
||||
msgid "failed to migrate app config: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/client/registry.go:20
|
||||
#: ./pkg/client/registry.go:37 ./pkg/client/registry.go:55 ./pkg/client/registry.go:60
|
||||
#, c-format
|
||||
msgid "failed to parse image %s, saw: %s"
|
||||
msgstr ""
|
||||
@@ -3291,15 +3306,15 @@ msgstr ""
|
||||
msgid "including VOLUMES=%v in backupbot exec invocation"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/release.go:634 ./cli/recipe/upgrade.go:378
|
||||
#: ./cli/recipe/release.go:634 ./cli/recipe/upgrade.go:429
|
||||
msgid "increase the major part of the version"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/release.go:642 ./cli/recipe/upgrade.go:386
|
||||
#: ./cli/recipe/release.go:642 ./cli/recipe/upgrade.go:437
|
||||
msgid "increase the minor part of the version"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/release.go:650 ./cli/recipe/upgrade.go:394
|
||||
#: ./cli/recipe/release.go:650 ./cli/recipe/upgrade.go:445
|
||||
msgid "increase the patch part of the version"
|
||||
msgstr ""
|
||||
|
||||
@@ -3464,7 +3479,7 @@ msgstr ""
|
||||
msgid "list [flags]"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:410
|
||||
#: ./cli/recipe/upgrade.go:461
|
||||
msgid "list all tags, not just upgrades"
|
||||
msgstr ""
|
||||
|
||||
@@ -3534,11 +3549,11 @@ msgstr ""
|
||||
#. with no spaces in between
|
||||
#. translators: `abra man` aliases. use a comma separated list of aliases
|
||||
#. with no spaces in between
|
||||
#: ./cli/app/list.go:319 ./cli/app/move.go:34 ./cli/app/ps.go:205 ./cli/app/secret.go:553 ./cli/app/secret.go:649 ./cli/recipe/list.go:104 ./cli/recipe/upgrade.go:400 ./cli/recipe/version.go:139 ./cli/run.go:152 ./cli/server/list.go:106
|
||||
#: ./cli/app/list.go:319 ./cli/app/move.go:34 ./cli/app/ps.go:205 ./cli/app/secret.go:553 ./cli/app/secret.go:649 ./cli/recipe/list.go:104 ./cli/recipe/upgrade.go:451 ./cli/recipe/version.go:139 ./cli/run.go:152 ./cli/server/list.go:106
|
||||
msgid "m"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/app/list.go:318 ./cli/app/ps.go:204 ./cli/app/secret.go:552 ./cli/app/secret.go:648 ./cli/recipe/list.go:103 ./cli/recipe/upgrade.go:399 ./cli/recipe/version.go:138 ./cli/server/list.go:105
|
||||
#: ./cli/app/list.go:318 ./cli/app/ps.go:204 ./cli/app/secret.go:552 ./cli/app/secret.go:648 ./cli/recipe/list.go:103 ./cli/recipe/upgrade.go:450 ./cli/recipe/version.go:138 ./cli/server/list.go:105
|
||||
msgid "machine"
|
||||
msgstr ""
|
||||
|
||||
@@ -3547,7 +3562,7 @@ msgstr ""
|
||||
msgid "main app service version for %s is empty?"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/internal/recipe.go:48 ./cli/internal/recipe.go:66 ./cli/internal/recipe.go:80 ./cli/recipe/release.go:631 ./cli/recipe/upgrade.go:375
|
||||
#: ./cli/internal/recipe.go:48 ./cli/internal/recipe.go:66 ./cli/internal/recipe.go:80 ./cli/recipe/release.go:631 ./cli/recipe/upgrade.go:426
|
||||
msgid "major"
|
||||
msgstr ""
|
||||
|
||||
@@ -3576,7 +3591,7 @@ msgstr ""
|
||||
msgid "migrating app config from %s to %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/internal/recipe.go:48 ./cli/internal/recipe.go:68 ./cli/internal/recipe.go:82 ./cli/recipe/release.go:639 ./cli/recipe/upgrade.go:383
|
||||
#: ./cli/internal/recipe.go:48 ./cli/internal/recipe.go:68 ./cli/internal/recipe.go:82 ./cli/recipe/release.go:639 ./cli/recipe/upgrade.go:434
|
||||
msgid "minor"
|
||||
msgstr ""
|
||||
|
||||
@@ -3705,7 +3720,7 @@ msgstr ""
|
||||
msgid "no .git directory in %s?"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:29
|
||||
#: ./pkg/recipe/compose.go:31
|
||||
#, c-format
|
||||
msgid "no COMPOSE_FILE detected, loading default: %s"
|
||||
msgstr ""
|
||||
@@ -3731,7 +3746,7 @@ msgstr ""
|
||||
msgid "no changes discovered in %s, nothing to publish?"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:361
|
||||
#: ./cli/recipe/upgrade.go:371
|
||||
msgid "no changes, skip creating commit"
|
||||
msgstr ""
|
||||
|
||||
@@ -3756,7 +3771,7 @@ msgstr ""
|
||||
msgid "no domain provided"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:248
|
||||
#: ./pkg/recipe/compose.go:292
|
||||
msgid "no existing label found, automagic insertion not supported yet"
|
||||
msgstr ""
|
||||
|
||||
@@ -3765,7 +3780,7 @@ msgstr ""
|
||||
msgid "no git tags found for %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:182
|
||||
#: ./cli/recipe/upgrade.go:192
|
||||
#, c-format
|
||||
msgid "no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)"
|
||||
msgstr ""
|
||||
@@ -3897,12 +3912,12 @@ msgstr ""
|
||||
msgid "not requesting a remote TTY"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:305
|
||||
#: ./cli/recipe/upgrade.go:315
|
||||
#, c-format
|
||||
msgid "not upgrading %s, skipping as requested"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:244
|
||||
#: ./cli/recipe/upgrade.go:254
|
||||
#, c-format
|
||||
msgid "not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants"
|
||||
msgstr ""
|
||||
@@ -3956,12 +3971,12 @@ msgstr ""
|
||||
msgid "p"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:163
|
||||
#: ./cli/recipe/upgrade.go:166
|
||||
#, c-format
|
||||
msgid "parsed %s for %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:159
|
||||
#: ./pkg/recipe/compose.go:178
|
||||
#, c-format
|
||||
msgid "parsed %s from %s"
|
||||
msgstr ""
|
||||
@@ -3999,7 +4014,7 @@ msgstr ""
|
||||
msgid "pass command not found on $PATH, is it installed?"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/internal/recipe.go:48 ./cli/internal/recipe.go:70 ./cli/internal/recipe.go:84 ./cli/recipe/release.go:647 ./cli/recipe/upgrade.go:391
|
||||
#: ./cli/internal/recipe.go:48 ./cli/internal/recipe.go:70 ./cli/internal/recipe.go:84 ./cli/recipe/release.go:647 ./cli/recipe/upgrade.go:442
|
||||
msgid "patch"
|
||||
msgstr ""
|
||||
|
||||
@@ -4053,7 +4068,7 @@ msgstr ""
|
||||
msgid "prefer offline & filesystem access"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/app/list.go:321 ./cli/app/ps.go:207 ./cli/app/secret.go:555 ./cli/app/secret.go:651 ./cli/recipe/list.go:106 ./cli/recipe/upgrade.go:402 ./cli/recipe/version.go:141 ./cli/server/list.go:108
|
||||
#: ./cli/app/list.go:321 ./cli/app/ps.go:207 ./cli/app/secret.go:555 ./cli/app/secret.go:651 ./cli/recipe/list.go:106 ./cli/recipe/upgrade.go:453 ./cli/recipe/version.go:141 ./cli/server/list.go:108
|
||||
msgid "print machine-readable output"
|
||||
msgstr ""
|
||||
|
||||
@@ -4436,7 +4451,7 @@ msgstr ""
|
||||
msgid "retrieved %s as target container on %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:59
|
||||
#: ./pkg/recipe/compose.go:61
|
||||
#, c-format
|
||||
msgid "retrieved %s configs for %s"
|
||||
msgstr ""
|
||||
@@ -4446,7 +4461,7 @@ msgstr ""
|
||||
msgid "retrieved %s for %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:144
|
||||
#: ./cli/recipe/upgrade.go:147
|
||||
#, c-format
|
||||
msgid "retrieved %s from remote registry for %s"
|
||||
msgstr ""
|
||||
@@ -4723,12 +4738,12 @@ msgstr ""
|
||||
msgid "serverAdd: cleanUp: unable to list files in %s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:227
|
||||
#: ./cli/recipe/upgrade.go:237
|
||||
#, c-format
|
||||
msgid "service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:223
|
||||
#: ./cli/recipe/upgrade.go:233
|
||||
#, c-format
|
||||
msgid "service %s, image %s pinned to %s, no compatible upgrade found"
|
||||
msgstr ""
|
||||
@@ -5007,7 +5022,7 @@ msgstr ""
|
||||
msgid "sync [flags]"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:243
|
||||
#: ./pkg/recipe/compose.go:287
|
||||
#, c-format
|
||||
msgid "synced label %s to service %s"
|
||||
msgstr ""
|
||||
@@ -5025,7 +5040,7 @@ msgstr ""
|
||||
msgid "tag at commit %s is unannotated or otherwise broken"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:301
|
||||
#: ./cli/recipe/upgrade.go:311
|
||||
#, c-format
|
||||
msgid "tag upgraded from %s to %s for %s"
|
||||
msgstr ""
|
||||
@@ -5212,7 +5227,7 @@ msgstr ""
|
||||
msgid "unable to determine server of app %s, please pass --server/-s"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:252
|
||||
#: ./cli/recipe/upgrade.go:262
|
||||
#, c-format
|
||||
msgid "unable to determine versioning semantics of %s, listing all tags"
|
||||
msgstr ""
|
||||
@@ -5286,12 +5301,12 @@ msgstr ""
|
||||
msgid "unable to open worktree of %s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:159
|
||||
#: ./cli/recipe/upgrade.go:162
|
||||
#, c-format
|
||||
msgid "unable to parse %s, error was: %s, skipping upgrade for %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:153
|
||||
#: ./pkg/recipe/compose.go:167
|
||||
#, c-format
|
||||
msgid "unable to parse %s, skipping"
|
||||
msgstr ""
|
||||
@@ -5331,7 +5346,7 @@ msgstr ""
|
||||
msgid "unable to read remotes in %s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:153
|
||||
#: ./cli/recipe/upgrade.go:156
|
||||
#, c-format
|
||||
msgid "unable to read tag for image %s, is it missing? skipping upgrade for %s"
|
||||
msgstr ""
|
||||
@@ -5381,6 +5396,11 @@ msgstr ""
|
||||
msgid "unable to retrieve container for %s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:193
|
||||
#, c-format
|
||||
msgid "unable to retrieve digest for %s: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/app/list.go:157
|
||||
#, c-format
|
||||
msgid "unable to retrieve tags for %s: %s"
|
||||
@@ -5513,12 +5533,17 @@ msgstr ""
|
||||
msgid "up"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:397
|
||||
#, c-format
|
||||
msgid "update digest of %s:%s?"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/upstream/stack/stack.go:477
|
||||
#, c-format
|
||||
msgid "updating %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/compose.go:171 ./pkg/recipe/compose.go:237
|
||||
#: ./pkg/recipe/compose.go:213 ./pkg/recipe/compose.go:281
|
||||
#, c-format
|
||||
msgid "updating %s to %s in %s"
|
||||
msgstr ""
|
||||
@@ -5543,17 +5568,17 @@ msgstr ""
|
||||
msgid "upgrade [flags]"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:248
|
||||
#: ./cli/recipe/upgrade.go:258
|
||||
#, c-format
|
||||
msgid "upgrade to which tag? (service: %s, image: %s, tag: %s)"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:254
|
||||
#: ./cli/recipe/upgrade.go:264
|
||||
#, c-format
|
||||
msgid "upgrade to which tag? (service: %s, tag: %s)"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/upgrade.go:221
|
||||
#: ./cli/recipe/upgrade.go:231
|
||||
#, c-format
|
||||
msgid "upgrading service %s from %s to %s (pinned tag: %s)"
|
||||
msgstr ""
|
||||
@@ -5806,11 +5831,11 @@ msgstr ""
|
||||
msgid "writing recipe version failed: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/release.go:632 ./cli/recipe/upgrade.go:376
|
||||
#: ./cli/recipe/release.go:632 ./cli/recipe/upgrade.go:427
|
||||
msgid "x"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/release.go:640 ./cli/recipe/upgrade.go:384
|
||||
#: ./cli/recipe/release.go:640 ./cli/recipe/upgrade.go:435
|
||||
msgid "y"
|
||||
msgstr ""
|
||||
|
||||
@@ -5822,7 +5847,7 @@ msgstr ""
|
||||
msgid "you can only use one version flag: --major, --minor or --patch"
|
||||
msgstr ""
|
||||
|
||||
#: ./cli/recipe/release.go:648 ./cli/recipe/upgrade.go:392
|
||||
#: ./cli/recipe/release.go:648 ./cli/recipe/upgrade.go:443
|
||||
msgid "z"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+49
-5
@@ -6,8 +6,10 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
@@ -110,7 +112,14 @@ func (r Recipe) GetVersionLabelLocal() (string, error) {
|
||||
return label, nil
|
||||
}
|
||||
|
||||
// getRegistryDigest is a variable so that it can be stubbed in tests.
|
||||
var getRegistryDigest = client.GetRegistryDigest
|
||||
|
||||
// UpdateTag updates an image tag in-place on file system local compose files.
|
||||
// Images which are pinned to a digest (e.g. "nextcloud:34.0.5-fpm@sha256:...")
|
||||
// have their digest updated to the one of the new tag, as retrieved from the
|
||||
// container registry. Passing the current tag of a digest pinned image
|
||||
// refreshes the digest, e.g. when upstream re-published the same tag.
|
||||
func (r Recipe) UpdateTag(image, tag string) (bool, error) {
|
||||
fullPattern := fmt.Sprintf("%s/compose**yml", r.Dir)
|
||||
image = formatter.StripTagMeta(image)
|
||||
@@ -122,6 +131,11 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
|
||||
|
||||
log.Debug(i18n.G("considering %s config(s) for tag update", strings.Join(composeFiles, ", ")))
|
||||
|
||||
var (
|
||||
updated bool
|
||||
newDigest string
|
||||
)
|
||||
|
||||
for _, composeFile := range composeFiles {
|
||||
opts := stack.Deploy{Composefiles: []string{composeFile}}
|
||||
|
||||
@@ -140,7 +154,7 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
|
||||
continue // may be a compose.$optional.yml file
|
||||
}
|
||||
|
||||
img, _ := reference.ParseNormalizedNamed(service.Image)
|
||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -154,30 +168,60 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
var composeDigest string
|
||||
if canonical, ok := img.(reference.Canonical); ok {
|
||||
composeDigest = canonical.Digest().String()
|
||||
}
|
||||
|
||||
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)
|
||||
bytes, err := os.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)
|
||||
|
||||
if composeDigest != "" {
|
||||
if newDigest == "" {
|
||||
newDigest, err = getRegistryDigest(img, tag)
|
||||
if err != nil {
|
||||
return false, errors.New(i18n.G("unable to retrieve digest for %s: %s", new, err))
|
||||
}
|
||||
}
|
||||
|
||||
old = fmt.Sprintf("%s@%s", old, composeDigest)
|
||||
new = fmt.Sprintf("%s@%s", new, newDigest)
|
||||
}
|
||||
|
||||
if old == new {
|
||||
continue
|
||||
}
|
||||
|
||||
// NOTE: only match the full image reference, so that "foo:1.2" does
|
||||
// not match "foo:1.2.3" or the "foo:1.2" part of "foo:1.2@sha256:..."
|
||||
pattern := regexp.MustCompile(regexp.QuoteMeta(old) + `([^@\w.\-]|$)`)
|
||||
replaced := pattern.ReplaceAllString(string(bytes), new+"${1}")
|
||||
if replaced == string(bytes) {
|
||||
continue
|
||||
}
|
||||
|
||||
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 {
|
||||
if err := os.WriteFile(compose.Filename, []byte(replaced), 0o764); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
// UpdateLabel updates a label in-place on file system local compose files.
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
oldDigest = "sha256:1111111111111111111111111111111111111111111111111111111111111111"
|
||||
newDigest = "sha256:2222222222222222222222222222222222222222222222222222222222222222"
|
||||
)
|
||||
|
||||
func setupUpdateTagRecipe(t *testing.T, compose string) (Recipe, string) {
|
||||
t.Helper()
|
||||
|
||||
dir := t.TempDir()
|
||||
composePath := path.Join(dir, "compose.yml")
|
||||
envPath := path.Join(dir, ".env.sample")
|
||||
|
||||
assert.NoError(t, os.WriteFile(composePath, []byte(compose), 0o644))
|
||||
assert.NoError(t, os.WriteFile(envPath, []byte("TYPE=foo\n"), 0o644))
|
||||
|
||||
return Recipe{
|
||||
Name: "foo",
|
||||
Dir: dir,
|
||||
ComposePath: composePath,
|
||||
SampleEnvPath: envPath,
|
||||
}, composePath
|
||||
}
|
||||
|
||||
func stubRegistryDigest(t *testing.T, digest string) *[]string {
|
||||
t.Helper()
|
||||
|
||||
var calls []string
|
||||
orig := getRegistryDigest
|
||||
getRegistryDigest = func(img reference.Named, tag string) (string, error) {
|
||||
calls = append(calls, reference.Path(img)+":"+tag)
|
||||
return digest, nil
|
||||
}
|
||||
t.Cleanup(func() { getRegistryDigest = orig })
|
||||
|
||||
return &calls
|
||||
}
|
||||
|
||||
func TestUpdateTag(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
compose string
|
||||
image string
|
||||
tag string
|
||||
expected string
|
||||
updated bool
|
||||
lookups int
|
||||
}{
|
||||
{
|
||||
name: "plain tag",
|
||||
compose: "version: \"3.8\"\nservices:\n app:\n image: nginx:1.31.7\n",
|
||||
image: "nginx",
|
||||
tag: "1.31.8",
|
||||
expected: "version: \"3.8\"\nservices:\n app:\n image: nginx:1.31.8\n",
|
||||
updated: true,
|
||||
},
|
||||
{
|
||||
name: "shorter tag does not mangle longer tag",
|
||||
compose: "version: \"3.8\"\nservices:\n app:\n image: nginx:1.31\n web:\n image: nginx:1.31.7\n",
|
||||
image: "nginx",
|
||||
tag: "1.32",
|
||||
expected: "version: \"3.8\"\nservices:\n app:\n image: nginx:1.32\n web:\n image: nginx:1.32\n",
|
||||
updated: true,
|
||||
},
|
||||
{
|
||||
name: "digest is updated with tag",
|
||||
compose: "version: \"3.8\"\nservices:\n app:\n image: nextcloud:34.0.5-fpm@" + oldDigest + "\n",
|
||||
image: "nextcloud",
|
||||
tag: "34.0.6-fpm",
|
||||
expected: "version: \"3.8\"\nservices:\n app:\n image: nextcloud:34.0.6-fpm@" + newDigest + "\n",
|
||||
updated: true,
|
||||
lookups: 1,
|
||||
},
|
||||
{
|
||||
name: "digest is refreshed for same tag",
|
||||
compose: "version: \"3.8\"\nservices:\n app:\n image: \"nextcloud:34.0.5-fpm@" + oldDigest + "\"\n",
|
||||
image: "nextcloud",
|
||||
tag: "34.0.5-fpm",
|
||||
expected: "version: \"3.8\"\nservices:\n app:\n image: \"nextcloud:34.0.5-fpm@" + newDigest + "\"\n",
|
||||
updated: true,
|
||||
lookups: 1,
|
||||
},
|
||||
{
|
||||
name: "same tag without digest is a no-op",
|
||||
compose: "version: \"3.8\"\nservices:\n app:\n image: nextcloud:34.0.5-fpm\n",
|
||||
image: "nextcloud",
|
||||
tag: "34.0.5-fpm",
|
||||
expected: "version: \"3.8\"\nservices:\n app:\n image: nextcloud:34.0.5-fpm\n",
|
||||
updated: false,
|
||||
},
|
||||
{
|
||||
name: "pinned and unpinned services sharing an image",
|
||||
compose: "version: \"3.8\"\nservices:\n app:\n image: nextcloud:34.0.5-fpm@" + oldDigest + "\n cron:\n image: nextcloud:34.0.5-fpm\n",
|
||||
image: "nextcloud",
|
||||
tag: "34.0.6-fpm",
|
||||
expected: "version: \"3.8\"\nservices:\n app:\n image: nextcloud:34.0.6-fpm@" + newDigest + "\n cron:\n image: nextcloud:34.0.6-fpm\n",
|
||||
updated: true,
|
||||
lookups: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r, composePath := setupUpdateTagRecipe(t, tc.compose)
|
||||
calls := stubRegistryDigest(t, newDigest)
|
||||
|
||||
updated, err := r.UpdateTag(tc.image, tc.tag)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.updated, updated)
|
||||
assert.Len(t, *calls, tc.lookups)
|
||||
|
||||
content, err := os.ReadFile(composePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, string(content))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user