Compare commits

...
4 Commits
Author SHA1 Message Date
sixsmith 4181daac82 run make-i18n 2026-09-29 21:11:56 -04:00
sixsmith 9475381e23 use compose loader for image updates, add shared image test
Look up the service image with the compose loader instead of parsing
the yaml separately, and only rewrite image lines which match it
exactly. Read images back via GetComposeConfig in the tests, drop
testify from them, and add an integration test for pinning services
which use variants of the same image.
2026-09-29 21:11:56 -04:00
sixsmith 7b5c2f4723 run make-i18n 2026-09-29 20:55:40 -04:00
sixsmith 3ba7470a55 fix digest pinning issues in recipe upgrade
Update the image of a single service instead of every service using
the same image name, only rewriting the tag and digest of its image
value so the registry host and the rest of the file are kept as is.

- skip images which use variables, warn to update them manually
- error on images which fail to parse instead of panicking
- drop a stale digest when upgrading a pinned image without
  --pindigests, since it points at the image of the old tag
- error when the registry does not return a digest
- use docker cli credentials when fetching the digest, so private
  registries work after docker login

Add tests for UpdatePinnedTag, UpdateTag and
GetImageMultiplatformDigest.
2026-09-29 20:55:40 -04:00
8 changed files with 624 additions and 227 deletions
+2 -2
View File
@@ -304,9 +304,9 @@ interface.`),
}
resolvedDigest = digest
ok, err = recipe.UpdatePinnedTag(image, upgradeTag, digest)
ok, err = recipe.UpdatePinnedTag(service.Name, upgradeTag, digest)
} else {
ok, err = recipe.UpdateTag(image, upgradeTag)
ok, err = recipe.UpdateTag(service.Name, upgradeTag)
}
if err != nil {
+12 -5
View File
@@ -13,8 +13,6 @@ import (
"github.com/regclient/regclient/types/ref"
)
var rc = regclient.New()
// GetRegistryTags retrieves all tags of an image from a container registry.
func GetRegistryTags(img reference.Named) ([]string, error) {
var tags []string
@@ -41,13 +39,22 @@ func GetRegistryTags(img reference.Named) ([]string, error) {
func GetImageMultiplatformDigest(img reference.Named, tag string) (string, error) {
r, err := ref.New(fmt.Sprintf("%s:%s", img.Name(), tag))
if err != nil {
return "", fmt.Errorf("parsing reference %s:%s: %w", img.Name(), tag, err)
return "", errors.New(i18n.G("failed to parse image %s:%s, saw: %s", img.Name(), tag, err.Error()))
}
// Use the credentials of the docker cli, which enables using a private
// registry by running docker login on the machine that abra is executed.
rc := regclient.New(regclient.WithDockerCreds())
m, err := rc.ManifestHead(context.Background(), r)
if err != nil {
return "", fmt.Errorf("fetching manifest for %s:%s: %w", img.Name(), tag, err)
return "", errors.New(i18n.G("failed to fetch manifest for %s:%s, saw: %s", img.Name(), tag, err.Error()))
}
return m.GetDescriptor().Digest.String(), nil
digest := m.GetDescriptor().Digest.String()
if digest == "" {
return "", errors.New(i18n.G("registry did not return a digest for %s:%s", img.Name(), tag))
}
return digest, nil
}
+50
View File
@@ -0,0 +1,50 @@
package client
import (
"regexp"
"testing"
"github.com/distribution/reference"
)
func TestGetImageMultiplatformDigest(t *testing.T) {
img, err := reference.ParseNormalizedNamed("nginx")
if err != nil {
t.Fatal(err)
}
digest, err := GetImageMultiplatformDigest(img, "1.21.6")
if err != nil {
t.Fatal(err)
}
if !regexp.MustCompile(`^sha256:[a-f0-9]{64}$`).MatchString(digest) {
t.Fatalf("unexpected digest %s", digest)
}
again, err := GetImageMultiplatformDigest(img, "1.21.6")
if err != nil {
t.Fatal(err)
}
if again != digest {
t.Errorf("got %s on second lookup, want %s", again, digest)
}
other, err := GetImageMultiplatformDigest(img, "1.21.5")
if err != nil {
t.Fatal(err)
}
if other == digest {
t.Errorf("expected different digests for 1.21.5 and 1.21.6, got %s", digest)
}
}
func TestGetImageMultiplatformDigestMissingTag(t *testing.T) {
img, err := reference.ParseNormalizedNamed("nginx")
if err != nil {
t.Fatal(err)
}
if _, err := GetImageMultiplatformDigest(img, "this-tag-does-not-exist"); err == nil {
t.Fatal("expected an error for a missing tag")
}
}
+82 -59
View File
@@ -7,7 +7,7 @@
msgid ""
msgstr "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-09-10 00:13-0400\n"
"POT-Creation-Date: 2026-09-29 21:11-0400\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"
@@ -304,7 +304,7 @@ msgstr ""
msgid "%s has no secrets to generate, skipping..."
msgstr ""
#: ./pkg/recipe/compose.go:107
#: ./pkg/recipe/compose.go:108
#, 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:299
#: ./pkg/recipe/compose.go:270
#, c-format
msgid "%s is already set, nothing to do?"
msgstr ""
#: ./pkg/recipe/compose.go:72
#: ./pkg/recipe/compose.go:73
#, c-format
msgid "%s is missing a compose.yml or compose.*.yml file?"
msgstr ""
@@ -568,12 +568,12 @@ msgstr ""
msgid "--local detected, running %s on local work station"
msgstr ""
#: ./pkg/lint/recipe.go:261 ./pkg/recipe/files.go:18
#: ./pkg/lint/recipe.go:268 ./pkg/recipe/files.go:18
#, c-format
msgid ".env.sample for %s couldn't be read: %s"
msgstr ""
#: ./pkg/lint/recipe.go:133
#: ./pkg/lint/recipe.go:140
msgid ".env.sample provided"
msgstr ""
@@ -638,17 +638,17 @@ msgstr ""
msgid "CHAOS DEPLOY"
msgstr ""
#: ./pkg/recipe/compose.go:47
#: ./pkg/recipe/compose.go:48
#, c-format
msgid "COMPOSE_FILE (=\"%s\") parsing failed?"
msgstr ""
#: ./pkg/recipe/compose.go:58
#: ./pkg/recipe/compose.go:59
#, c-format
msgid "COMPOSE_FILE detected (%s), loading %s"
msgstr ""
#: ./pkg/recipe/compose.go:38
#: ./pkg/recipe/compose.go:39
#, c-format
msgid "COMPOSE_FILE detected, loading %s"
msgstr ""
@@ -1644,7 +1644,7 @@ msgid "\n"
"\n"
msgstr ""
#: ./pkg/lint/recipe.go:203
#: ./pkg/lint/recipe.go:210
#, c-format
msgid "\n"
"lint %s: %s"
@@ -1737,7 +1737,7 @@ msgstr ""
msgid "accepts at most 2 args with --local/-l"
msgstr ""
#: ./pkg/recipe/compose.go:315
#: ./pkg/recipe/compose.go:286
#, c-format
msgid "add '- \"%s\"' manually to the 'app' service in %s"
msgstr ""
@@ -1764,11 +1764,15 @@ msgstr ""
msgid "all containers did really go away"
msgstr ""
#: ./pkg/lint/recipe.go:131
msgid "all images use a digest"
msgstr ""
#: ./pkg/lint/recipe.go:82
msgid "all images use a tag"
msgstr ""
#: ./pkg/lint/recipe.go:162
#: ./pkg/lint/recipe.go:169
msgid "all services have images"
msgstr ""
@@ -2221,7 +2225,7 @@ msgstr ""
msgid "config not found: %s"
msgstr ""
#: ./pkg/lint/recipe.go:169
#: ./pkg/lint/recipe.go:176
msgid "config version are vendored"
msgstr ""
@@ -2230,14 +2234,14 @@ msgstr ""
msgid "connection timed out for %s"
msgstr ""
#: ./pkg/recipe/compose.go:257
#: ./pkg/recipe/compose.go:144
#, c-format
msgid "considering %s config(s) for label update"
msgid "considering %s config(s) for image update"
msgstr ""
#: ./pkg/recipe/compose.go:123
#: ./pkg/recipe/compose.go:228
#, c-format
msgid "considering %s config(s) for tag update"
msgid "considering %s config(s) for label update"
msgstr ""
#: ./cli/app/undeploy.go:141 ./cli/server/prune.go:53
@@ -2258,7 +2262,7 @@ msgstr ""
msgid "context lacks Docker endpoint"
msgstr ""
#: ./pkg/recipe/compose.go:295
#: ./pkg/recipe/compose.go:266
#, c-format
msgid "coop-cloud.${STACK_NAME}.version=%s"
msgstr ""
@@ -2303,7 +2307,7 @@ msgstr ""
msgid "create <domain> [flags]"
msgstr ""
#: ./pkg/lint/recipe.go:134
#: ./pkg/lint/recipe.go:141
msgid "create an example .env.sample"
msgstr ""
@@ -2418,7 +2422,7 @@ msgstr ""
msgid "deploy in progress 🟠"
msgstr ""
#: ./pkg/lint/recipe.go:147
#: ./pkg/lint/recipe.go:154
msgid "deploy labels stanza present"
msgstr ""
@@ -2644,7 +2648,7 @@ msgstr ""
msgid "ensure 'version: \"3.8\"' in compose configs"
msgstr ""
#: ./pkg/lint/recipe.go:163
#: ./pkg/lint/recipe.go:170
msgid "ensure \"image: ...\" set on all services"
msgstr ""
@@ -2682,7 +2686,7 @@ msgstr ""
msgid "err: %v, "
msgstr ""
#: ./cli/recipe/lint.go:82 ./cli/recipe/lint.go:144 ./pkg/lint/recipe.go:132 ./pkg/lint/recipe.go:139 ./pkg/lint/recipe.go:146 ./pkg/lint/recipe.go:153 ./pkg/lint/recipe.go:161 ./pkg/lint/recipe.go:168 ./pkg/lint/recipe.go:175
#: ./cli/recipe/lint.go:82 ./cli/recipe/lint.go:144 ./pkg/lint/recipe.go:139 ./pkg/lint/recipe.go:146 ./pkg/lint/recipe.go:153 ./pkg/lint/recipe.go:160 ./pkg/lint/recipe.go:168 ./pkg/lint/recipe.go:175 ./pkg/lint/recipe.go:182
msgid "error"
msgstr ""
@@ -2801,6 +2805,11 @@ msgstr ""
msgid "failed to create volume %s on %s: %s"
msgstr ""
#: ./pkg/client/registry.go:51
#, c-format
msgid "failed to fetch manifest for %s:%s, saw: %s"
msgstr ""
#: ./pkg/secret/secret.go:104
#, c-format
msgid "failed to generate random bytes: %w"
@@ -2839,11 +2848,16 @@ msgstr ""
msgid "failed to migrate app config: %s"
msgstr ""
#: ./pkg/client/registry.go:24
#: ./pkg/client/registry.go:22
#, c-format
msgid "failed to parse image %s, saw: %s"
msgstr ""
#: ./pkg/client/registry.go:42
#, c-format
msgid "failed to parse image %s:%s, saw: %s"
msgstr ""
#: ./pkg/recipe/git.go:437
#, c-format
msgid "failed to parse image for %s in %s: %s"
@@ -3216,11 +3230,11 @@ msgstr ""
msgid "images pruned: %d; space reclaimed: %s"
msgstr ""
#: ./pkg/lint/recipe.go:148
#: ./pkg/lint/recipe.go:155
msgid "include \"deploy: labels: ...\" stanza"
msgstr ""
#: ./pkg/lint/recipe.go:155
#: ./pkg/lint/recipe.go:162
msgid "include \"traefik.enable=true\" deploy label"
msgstr ""
@@ -3374,7 +3388,7 @@ msgstr ""
msgid "invalid detach escape keys, using default: %s"
msgstr ""
#: ./pkg/lint/recipe.go:504
#: ./pkg/lint/recipe.go:533
msgid "invalid lightweight tag detected"
msgstr ""
@@ -3435,12 +3449,12 @@ msgstr ""
msgid "lint <recipe> [flags]"
msgstr ""
#: ./pkg/lint/recipe.go:187
#: ./pkg/lint/recipe.go:194
#, c-format
msgid "linting for critical errors in %s configs"
msgstr ""
#: ./pkg/lint/recipe.go:215
#: ./pkg/lint/recipe.go:222
#, c-format
msgid "linting successful, %s is well configured"
msgstr ""
@@ -3632,7 +3646,7 @@ msgstr ""
msgid "name"
msgstr ""
#: ./pkg/lint/recipe.go:141
#: ./pkg/lint/recipe.go:148
msgid "name a servce 'app'"
msgstr ""
@@ -3710,7 +3724,7 @@ msgstr ""
msgid "no .git directory in %s?"
msgstr ""
#: ./pkg/recipe/compose.go:29
#: ./pkg/recipe/compose.go:30
#, c-format
msgid "no COMPOSE_FILE detected, loading default: %s"
msgstr ""
@@ -3761,7 +3775,7 @@ msgstr ""
msgid "no domain provided"
msgstr ""
#: ./pkg/recipe/compose.go:314
#: ./pkg/recipe/compose.go:285
msgid "no existing label found, automagic insertion not supported yet"
msgstr ""
@@ -3933,11 +3947,11 @@ msgstr ""
msgid "one of <src>/<dest> arguments must take $SERVICE:$PATH form"
msgstr ""
#: ./pkg/lint/recipe.go:140
#: ./pkg/lint/recipe.go:147
msgid "one service named 'app'"
msgstr ""
#: ./pkg/lint/recipe.go:176
#: ./pkg/lint/recipe.go:183
msgid "only annotated tags used for recipe version"
msgstr ""
@@ -3971,11 +3985,6 @@ msgstr ""
msgid "parsed %s for %s"
msgstr ""
#: ./pkg/recipe/compose.go:160
#, c-format
msgid "parsed %s from %s"
msgstr ""
#: ./cli/app/cmd.go:134 ./cli/app/cmd.go:180
#, c-format
msgid "parsed following command arguments: %s"
@@ -4224,7 +4233,7 @@ msgstr ""
msgid "recipe %s does not exist?"
msgstr ""
#: ./pkg/lint/recipe.go:212
#: ./pkg/lint/recipe.go:219
#, c-format
msgid "recipe '%s' failed lint checks:\n"
"%s"
@@ -4248,6 +4257,11 @@ msgstr ""
msgid "ref"
msgstr ""
#: ./pkg/client/registry.go:56
#, c-format
msgid "registry did not return a digest for %s:%s"
msgstr ""
#. translators: `recipe release` command
#: ./cli/recipe/release.go:35
msgid "release <recipe> [version] [flags]"
@@ -4346,6 +4360,11 @@ msgstr ""
msgid "removing configs failed: %s"
msgstr ""
#: ./pkg/recipe/compose.go:120
#, c-format
msgid "removing digest from %s for %s, use --pindigests to pin the new tag"
msgstr ""
#: ./pkg/upstream/stack/remove.go:148
#, c-format
msgid "removing network %s"
@@ -4371,7 +4390,7 @@ msgstr ""
msgid "removing volumes failed: %s"
msgstr ""
#: ./pkg/lint/recipe.go:177
#: ./pkg/lint/recipe.go:184
msgid "replace lightweight tag with annotated tag"
msgstr ""
@@ -4454,7 +4473,7 @@ msgstr ""
msgid "retrieved %s as target container on %s"
msgstr ""
#: ./pkg/recipe/compose.go:59
#: ./pkg/recipe/compose.go:60
#, c-format
msgid "retrieved %s configs for %s"
msgstr ""
@@ -4625,7 +4644,7 @@ msgstr ""
msgid "secret %s is > %d chars when combined with %s"
msgstr ""
#: ./pkg/lint/recipe.go:481
#: ./pkg/lint/recipe.go:510
#, c-format
msgid "secret %s is longer than 12 characters"
msgstr ""
@@ -5025,7 +5044,7 @@ msgstr ""
msgid "sync [flags]"
msgstr ""
#: ./pkg/recipe/compose.go:309
#: ./pkg/recipe/compose.go:280
#, c-format
msgid "synced label %s to service %s"
msgstr ""
@@ -5122,7 +5141,7 @@ msgstr ""
msgid "toggle non-interactive mode"
msgstr ""
#: ./pkg/lint/recipe.go:154
#: ./pkg/lint/recipe.go:161
msgid "traefik routing enabled"
msgstr ""
@@ -5250,6 +5269,11 @@ msgstr ""
msgid "unable to fetch tags in %s: %s"
msgstr ""
#: ./pkg/recipe/compose.go:181
#, c-format
msgid "unable to find image %s for %s in %s, please update it manually"
msgstr ""
#: ./cli/app/move.go:301
#, c-format
msgid "unable to get container matching %s: %s"
@@ -5270,7 +5294,7 @@ msgstr ""
msgid "unable to inspect container for %s: %s"
msgstr ""
#: ./pkg/lint/recipe.go:496
#: ./pkg/lint/recipe.go:525
#, c-format
msgid "unable to list local tags for %s"
msgstr ""
@@ -5284,7 +5308,7 @@ msgstr ""
msgid "unable to look up server context for %s: %s"
msgstr ""
#: ./cli/recipe/fetch.go:77 ./pkg/git/read.go:26 ./pkg/lint/recipe.go:491 ./pkg/recipe/git.go:243
#: ./cli/recipe/fetch.go:77 ./pkg/git/read.go:26 ./pkg/lint/recipe.go:520 ./pkg/recipe/git.go:243
#, c-format
msgid "unable to open %s: %s"
msgstr ""
@@ -5314,11 +5338,6 @@ msgstr ""
msgid "unable to parse %s, error was: %s, skipping upgrade for %s"
msgstr ""
#: ./pkg/recipe/compose.go:154
#, c-format
msgid "unable to parse %s, skipping"
msgstr ""
#: ./cli/app/list.go:167
#, c-format
msgid "unable to parse %s, skipping as upgrade option"
@@ -5334,6 +5353,11 @@ msgstr ""
msgid "unable to parse deploy version: %s"
msgstr ""
#: ./pkg/recipe/compose.go:204
#, c-format
msgid "unable to parse image %s: %s"
msgstr ""
#: ./cli/run.go:176
#, c-format
msgid "unable to proceed, %s does not exist?"
@@ -5541,16 +5565,11 @@ msgstr ""
msgid "updating %s"
msgstr ""
#: ./pkg/recipe/compose.go:176 ./pkg/recipe/compose.go:303
#: ./pkg/recipe/compose.go:187 ./pkg/recipe/compose.go:274
#, c-format
msgid "updating %s to %s in %s"
msgstr ""
#: ./pkg/recipe/compose.go:236
#, c-format
msgid "updating pinned reference %s to %s in %s"
msgstr ""
#. translators: `abra upgrade` command for autocompletion
#: ./cli/run.go:103
msgid "upgrade"
@@ -5590,6 +5609,10 @@ msgstr ""
msgid "upload your recipe to git.coopcloud.tech/coop-cloud/..."
msgstr ""
#: ./pkg/lint/recipe.go:132
msgid "use a digest for all images"
msgstr ""
#: ./pkg/lint/recipe.go:83
msgid "use a tag for all images"
msgstr ""
@@ -5640,7 +5663,7 @@ msgstr ""
msgid "validated %s as server argument"
msgstr ""
#: ./pkg/lint/recipe.go:170
#: ./pkg/lint/recipe.go:177
msgid "vendor config versions in an abra.sh"
msgstr ""
@@ -5791,7 +5814,7 @@ msgstr ""
msgid "waiting for %d task(s) to reach terminal state"
msgstr ""
#: ./pkg/lint/recipe.go:21 ./pkg/lint/recipe.go:67 ./pkg/lint/recipe.go:74 ./pkg/lint/recipe.go:81 ./pkg/lint/recipe.go:88 ./pkg/lint/recipe.go:95 ./pkg/lint/recipe.go:102 ./pkg/lint/recipe.go:109 ./pkg/lint/recipe.go:116 ./pkg/lint/recipe.go:123
#: ./pkg/lint/recipe.go:21 ./pkg/lint/recipe.go:67 ./pkg/lint/recipe.go:74 ./pkg/lint/recipe.go:81 ./pkg/lint/recipe.go:88 ./pkg/lint/recipe.go:95 ./pkg/lint/recipe.go:102 ./pkg/lint/recipe.go:109 ./pkg/lint/recipe.go:116 ./pkg/lint/recipe.go:123 ./pkg/lint/recipe.go:130
msgid "warn"
msgstr ""
Binary file not shown.
+106 -65
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-09-10 00:13-0400\n"
"POT-Creation-Date: 2026-09-29 21:11-0400\n"
"PO-Revision-Date: 2026-09-01 22:35+0000\n"
"Last-Translator: chasqui <chasqui@cryptolab.net>\n"
"Language-Team: Spanish <https://translate.coopcloud.tech/projects/co-op-"
@@ -438,7 +438,7 @@ msgstr "¿%s no tiene versiones publicadas?"
msgid "%s has no secrets to generate, skipping..."
msgstr "%s no tiene secretos para generar, omitiendo..."
#: pkg/recipe/compose.go:107
#: pkg/recipe/compose.go:108
#, c-format
msgid "%s has no version label? try running \"abra recipe sync %s\" first?"
msgstr ""
@@ -460,12 +460,12 @@ msgstr "%s ya está desplegado"
msgid "%s is already fetched"
msgstr "%s ya se ha obtenido"
#: pkg/recipe/compose.go:299
#: pkg/recipe/compose.go:270
#, c-format
msgid "%s is already set, nothing to do?"
msgstr "%s ya está establecido, ¿nada qué hacer?"
#: pkg/recipe/compose.go:72
#: pkg/recipe/compose.go:73
#, c-format
msgid "%s is missing a compose.yml or compose.*.yml file?"
msgstr "¿%s no tiene un archivo compose.yml o compose.*.yml?"
@@ -715,12 +715,12 @@ msgstr ""
msgid "--local detected, running %s on local work station"
msgstr "--local detectado, ejecutando %s en la estación de trabajo local"
#: pkg/lint/recipe.go:261 pkg/recipe/files.go:18
#: pkg/lint/recipe.go:268 pkg/recipe/files.go:18
#, c-format
msgid ".env.sample for %s couldn't be read: %s"
msgstr "archivo de entorno env.sample para %s no pudo leerse: %s"
#: pkg/lint/recipe.go:133
#: pkg/lint/recipe.go:140
msgid ".env.sample provided"
msgstr "Se proporcionó .env.sample"
@@ -831,17 +831,17 @@ msgstr "🌀 CAOS"
msgid "CHAOS DEPLOY"
msgstr "DESPLIEGUE CAÓTICO"
#: pkg/recipe/compose.go:47
#: pkg/recipe/compose.go:48
#, c-format
msgid "COMPOSE_FILE (=\"%s\") parsing failed?"
msgstr "COMPOSE_FILE (=\"%s\") ¿Fallo el análisis?"
#: pkg/recipe/compose.go:58
#: pkg/recipe/compose.go:59
#, c-format
msgid "COMPOSE_FILE detected (%s), loading %s"
msgstr "COMPOSE_FILE detectado (%s), cargando %s"
#: pkg/recipe/compose.go:38
#: pkg/recipe/compose.go:39
#, c-format
msgid "COMPOSE_FILE detected, loading %s"
msgstr "COMPOSE_FILE detectado, cargando %s"
@@ -2387,7 +2387,7 @@ msgstr ""
"\"nada de qué preocuparse\".\n"
"\n"
#: pkg/lint/recipe.go:203
#: pkg/lint/recipe.go:210
#, c-format
msgid ""
"\n"
@@ -2481,7 +2481,7 @@ msgstr ""
msgid "accepts at most 2 args with --local/-l"
msgstr "acepta como máximo 2 argumentos con --local/-l"
#: pkg/recipe/compose.go:315
#: pkg/recipe/compose.go:286
#, c-format
msgid "add '- \"%s\"' manually to the 'app' service in %s"
msgstr "agrega '- \"%s\"' manualmente al servicio de la 'app' en %s"
@@ -2509,11 +2509,16 @@ msgstr "todos"
msgid "all containers did really go away"
msgstr "todos los contenedores realmente desaparecieron"
#: pkg/lint/recipe.go:131
#, fuzzy
msgid "all images use a digest"
msgstr "todas las imágenes usan una etiqueta"
#: pkg/lint/recipe.go:82
msgid "all images use a tag"
msgstr "todas las imágenes usan una etiqueta"
#: pkg/lint/recipe.go:162
#: pkg/lint/recipe.go:169
msgid "all services have images"
msgstr "todos los servicios tienen imágenes"
@@ -3003,7 +3008,7 @@ msgstr "archivo de configuración cargado desde: %s"
msgid "config not found: %s"
msgstr "configuración no encontrada: %s"
#: pkg/lint/recipe.go:169
#: pkg/lint/recipe.go:176
msgid "config version are vendored"
msgstr "la versión de configuración está vendida"
@@ -3012,19 +3017,19 @@ msgstr "la versión de configuración está vendida"
msgid "connection timed out for %s"
msgstr "tiempo de conexión agotado para %s"
#: pkg/recipe/compose.go:257
#: pkg/recipe/compose.go:144
#, fuzzy, c-format
msgid "considering %s config(s) for image update"
msgstr ""
"considerando la(s) configuración(es) %s para la actualización de etiquetas "
"(tags)"
#: pkg/recipe/compose.go:228
#, c-format
msgid "considering %s config(s) for label update"
msgstr ""
"considerando la(s) configuración(es) %s para la actualización de etiquetas"
#: pkg/recipe/compose.go:123
#, c-format
msgid "considering %s config(s) for tag update"
msgstr ""
"considerando la(s) configuración(es) %s para la actualización de etiquetas "
"(tags)"
#: cli/app/undeploy.go:141 cli/server/prune.go:53
#, c-format
msgid "containers pruned: %d; space reclaimed: %s"
@@ -3043,7 +3048,7 @@ msgstr "el contexto para %s ya existe"
msgid "context lacks Docker endpoint"
msgstr "el contexto no tiene punto final de Docker"
#: pkg/recipe/compose.go:295
#: pkg/recipe/compose.go:266
#, c-format
msgid "coop-cloud.${STACK_NAME}.version=%s"
msgstr ""
@@ -3088,7 +3093,7 @@ msgstr "copiar <aplicacion> <origen> <destino> [opciones]"
msgid "create <domain> [flags]"
msgstr "crear <aplicacion> [opciones]"
#: pkg/lint/recipe.go:134
#: pkg/lint/recipe.go:141
msgid "create an example .env.sample"
msgstr "crear un ejemplo de .env.sample"
@@ -3204,7 +3209,7 @@ msgstr "argh! falló el despliegue 🛑"
msgid "deploy in progress 🟠"
msgstr "despliegue en proceso 🟠"
#: pkg/lint/recipe.go:147
#: pkg/lint/recipe.go:154
msgid "deploy labels stanza present"
msgstr "stanza de etiquetas de despliegue presente"
@@ -3435,7 +3440,7 @@ msgid "ensure 'version: \"3.8\"' in compose configs"
msgstr ""
"asegúrate que 'version: \"3.8\"' esté en las configuraciones de compose"
#: pkg/lint/recipe.go:163
#: pkg/lint/recipe.go:170
msgid "ensure \"image: ...\" set on all services"
msgstr "asegúrate que \"image: ...\" esté establecido en todos los servicios"
@@ -3477,9 +3482,9 @@ msgstr "archivo de entorno (env) para %s tiene problemas: %s"
msgid "err: %v, "
msgstr "error: %v, "
#: cli/recipe/lint.go:82 cli/recipe/lint.go:144 pkg/lint/recipe.go:132
#: pkg/lint/recipe.go:139 pkg/lint/recipe.go:146 pkg/lint/recipe.go:153
#: pkg/lint/recipe.go:161 pkg/lint/recipe.go:168 pkg/lint/recipe.go:175
#: cli/recipe/lint.go:82 cli/recipe/lint.go:144 pkg/lint/recipe.go:139
#: pkg/lint/recipe.go:146 pkg/lint/recipe.go:153 pkg/lint/recipe.go:160
#: pkg/lint/recipe.go:168 pkg/lint/recipe.go:175 pkg/lint/recipe.go:182
msgid "error"
msgstr "error"
@@ -3602,6 +3607,11 @@ msgstr "🛑 argh! error al crear secreto %s"
msgid "failed to create volume %s on %s: %s"
msgstr "🛑 error al crear el volumen %s en %s: %s"
#: pkg/client/registry.go:51
#, fuzzy, c-format
msgid "failed to fetch manifest for %s:%s, saw: %s"
msgstr "🛑 error al analizar la imagen %s; en: %s: %s"
#: pkg/secret/secret.go:104
#, c-format
msgid "failed to generate random bytes: %w"
@@ -3640,11 +3650,16 @@ msgstr "🛑 error al coincidir con el servicio seleccionado"
msgid "failed to migrate app config: %s"
msgstr "🛑 error al migrar la configuración de la aplicación: %s"
#: pkg/client/registry.go:24
#: pkg/client/registry.go:22
#, c-format
msgid "failed to parse image %s, saw: %s"
msgstr "🛑 error al analizar la imagen %s; detectado: %s"
#: pkg/client/registry.go:42
#, fuzzy, c-format
msgid "failed to parse image %s:%s, saw: %s"
msgstr "🛑 error al analizar la imagen %s; detectado: %s"
#: pkg/recipe/git.go:437
#, c-format
msgid "failed to parse image for %s in %s: %s"
@@ -4027,11 +4042,11 @@ msgstr "Ignorar cambios no confirmados en recetas"
msgid "images pruned: %d; space reclaimed: %s"
msgstr "imágenes descartadas: %d; espacio recuperado: %s"
#: pkg/lint/recipe.go:148
#: pkg/lint/recipe.go:155
msgid "include \"deploy: labels: ...\" stanza"
msgstr "incluir el bloque \"deploy: labels: ...\" stanza"
#: pkg/lint/recipe.go:155
#: pkg/lint/recipe.go:162
msgid "include \"traefik.enable=true\" deploy label"
msgstr "incluir la etiqueta de despliegue \"traefik.enable=true\""
@@ -4196,7 +4211,7 @@ msgstr ""
"teclas de escape de separación inválidas, utilizando el valor "
"predeterminado: %s"
#: pkg/lint/recipe.go:504
#: pkg/lint/recipe.go:533
msgid "invalid lightweight tag detected"
msgstr "etiqueta ligera inválida detectada"
@@ -4258,12 +4273,12 @@ msgstr ""
msgid "lint <recipe> [flags]"
msgstr "validar <receta> [opciones]"
#: pkg/lint/recipe.go:187
#: pkg/lint/recipe.go:194
#, c-format
msgid "linting for critical errors in %s configs"
msgstr "buscando errores críticos en las configuraciones de %s"
#: pkg/lint/recipe.go:215
#: pkg/lint/recipe.go:222
#, c-format
msgid "linting successful, %s is well configured"
msgstr "linting exitoso, %s está bien configurado"
@@ -4468,7 +4483,7 @@ msgstr ""
msgid "name"
msgstr "nombre"
#: pkg/lint/recipe.go:141
#: pkg/lint/recipe.go:148
msgid "name a servce 'app'"
msgstr "nombrar un servicio 'app'"
@@ -4557,7 +4572,7 @@ msgstr "no se encontró etiqueta %s para %s"
msgid "no .git directory in %s?"
msgstr "¿no hay un directorio .git en %s?"
#: pkg/recipe/compose.go:29
#: pkg/recipe/compose.go:30
#, c-format
msgid "no COMPOSE_FILE detected, loading default: %s"
msgstr "no se detectó COMPOSE_FILE, cargando el predeterminado: %s"
@@ -4608,7 +4623,7 @@ msgstr "no se encontraron contenedores que coincidan con el filtro %v?"
msgid "no domain provided"
msgstr "no se proporcionó dominio"
#: pkg/recipe/compose.go:314
#: pkg/recipe/compose.go:285
msgid "no existing label found, automagic insertion not supported yet"
msgstr ""
"no se encontró ninguna etiqueta existente, la inserción automágica no es "
@@ -4792,11 +4807,11 @@ msgstr ""
msgid "one of <src>/<dest> arguments must take $SERVICE:$PATH form"
msgstr "uno de los argumentos <src>/<dest> debe tener la forma $SERVICE:$PATH"
#: pkg/lint/recipe.go:140
#: pkg/lint/recipe.go:147
msgid "one service named 'app'"
msgstr "un servicio llamado 'app'"
#: pkg/lint/recipe.go:176
#: pkg/lint/recipe.go:183
msgid "only annotated tags used for recipe version"
msgstr "solo se usaron etiquetas anotadas para la versión de la receta"
@@ -4834,11 +4849,6 @@ msgstr "arriba"
msgid "parsed %s for %s"
msgstr "parseado %s para %s"
#: pkg/recipe/compose.go:160
#, c-format
msgid "parsed %s from %s"
msgstr "parseado %s de %s"
#: cli/app/cmd.go:134 cli/app/cmd.go:180
#, c-format
msgid "parsed following command arguments: %s"
@@ -5099,7 +5109,7 @@ msgstr "receta"
msgid "recipe %s does not exist?"
msgstr "¿la receta %s no existe?"
#: pkg/lint/recipe.go:212
#: pkg/lint/recipe.go:219
#, c-format
msgid ""
"recipe '%s' failed lint checks:\n"
@@ -5126,6 +5136,11 @@ msgstr "reducir la longitud de los nombres secretos a 12 caracteres"
msgid "ref"
msgstr ""
#: pkg/client/registry.go:56
#, fuzzy, c-format
msgid "registry did not return a digest for %s:%s"
msgstr "🛑 error al analizar la imagen %s; en: %s: %s"
#. translators: `recipe release` command
#: cli/recipe/release.go:35
msgid "release <recipe> [version] [flags]"
@@ -5224,6 +5239,11 @@ msgstr "eliminando configuración %s"
msgid "removing configs failed: %s"
msgstr "eliminación de configuraciones fallida: %s"
#: pkg/recipe/compose.go:120
#, c-format
msgid "removing digest from %s for %s, use --pindigests to pin the new tag"
msgstr ""
#: pkg/upstream/stack/remove.go:148
#, c-format
msgid "removing network %s"
@@ -5249,7 +5269,7 @@ msgstr "eliminación de volumen %s fallida: %s"
msgid "removing volumes failed: %s"
msgstr "error al eliminar volúmenes: %s"
#: pkg/lint/recipe.go:177
#: pkg/lint/recipe.go:184
msgid "replace lightweight tag with annotated tag"
msgstr "reemplazar etiqueta ligera por etiqueta anotada"
@@ -5332,7 +5352,7 @@ msgstr "recuperado %s como servicio con respaldo (backup) habilitado"
msgid "retrieved %s as target container on %s"
msgstr "recuperado %s como contenedor destino en %s"
#: pkg/recipe/compose.go:59
#: pkg/recipe/compose.go:60
#, c-format
msgid "retrieved %s configs for %s"
msgstr "recuperado %s configuración(es) para %s"
@@ -5511,7 +5531,7 @@ msgstr "buscando abra.sh para la versión de %s"
msgid "secret %s is > %d chars when combined with %s"
msgstr "el secreto %s tiene > %d caracteres cuando se combina con %s"
#: pkg/lint/recipe.go:481
#: pkg/lint/recipe.go:510
#, c-format
msgid "secret %s is longer than 12 characters"
msgstr "el secreto %s es más largo que 12 caracteres"
@@ -5930,7 +5950,7 @@ msgstr "¿Modo swarm no habilitado en el servidor local?"
msgid "sync [flags]"
msgstr "sincronizar [opciones]"
#: pkg/recipe/compose.go:309
#: pkg/recipe/compose.go:280
#, c-format
msgid "synced label %s to service %s"
msgstr "etiqueta sincronizada %s con el servicio %s"
@@ -6028,7 +6048,7 @@ msgstr "las opciones tmpfs son incompatibles con el tipo volume"
msgid "toggle non-interactive mode"
msgstr "activar modo no interactivo"
#: pkg/lint/recipe.go:154
#: pkg/lint/recipe.go:161
msgid "traefik routing enabled"
msgstr "enrutamiento de traefik habilitado"
@@ -6161,6 +6181,11 @@ msgstr "no se pudo detectar el remoto SSH para %s"
msgid "unable to fetch tags in %s: %s"
msgstr "no se pudieron obtener las etiquetas en %s: %s"
#: pkg/recipe/compose.go:181
#, c-format
msgid "unable to find image %s for %s in %s, please update it manually"
msgstr ""
#: cli/app/move.go:301
#, c-format
msgid "unable to get container matching %s: %s"
@@ -6181,7 +6206,7 @@ msgstr "no se pudo hacer un soft reset en %s: %s"
msgid "unable to inspect container for %s: %s"
msgstr "no se pudo inspeccionar el contenedor para %s: %s"
#: pkg/lint/recipe.go:496
#: pkg/lint/recipe.go:525
#, c-format
msgid "unable to list local tags for %s"
msgstr "no se pudieron listar las etiquetas locales para %s"
@@ -6195,7 +6220,7 @@ msgstr "no se pudo localizar el comando git; no se puede mostrar el diff"
msgid "unable to look up server context for %s: %s"
msgstr "no se pudo obtener el contexto del servidor para %s: %s"
#: cli/recipe/fetch.go:77 pkg/git/read.go:26 pkg/lint/recipe.go:491
#: cli/recipe/fetch.go:77 pkg/git/read.go:26 pkg/lint/recipe.go:520
#: pkg/recipe/git.go:243
#, c-format
msgid "unable to open %s: %s"
@@ -6227,11 +6252,6 @@ msgid "unable to parse %s, error was: %s, skipping upgrade for %s"
msgstr ""
"no se pudo analizar %s, el error fue: %s, omitiendo la actualización para %s"
#: pkg/recipe/compose.go:154
#, c-format
msgid "unable to parse %s, skipping"
msgstr "no se pudo parsear %s; se omitirá"
#: cli/app/list.go:167
#, c-format
msgid "unable to parse %s, skipping as upgrade option"
@@ -6247,6 +6267,11 @@ msgstr "no se pudo parsear el valor '%s' como bool: %s"
msgid "unable to parse deploy version: %s"
msgstr "no se puede analizar la versión de despliegue: %s"
#: pkg/recipe/compose.go:204
#, fuzzy, c-format
msgid "unable to parse image %s: %s"
msgstr "no se pudo crear %s: %s"
#: cli/run.go:176
#, c-format
msgid "unable to proceed, %s does not exist?"
@@ -6461,16 +6486,11 @@ msgstr "arriba"
msgid "updating %s"
msgstr "actualizando %s"
#: pkg/recipe/compose.go:176 pkg/recipe/compose.go:303
#: pkg/recipe/compose.go:187 pkg/recipe/compose.go:274
#, c-format
msgid "updating %s to %s in %s"
msgstr "actualizando %s a %s en %s"
#: pkg/recipe/compose.go:236
#, fuzzy, c-format
msgid "updating pinned reference %s to %s in %s"
msgstr "actualizando %s a %s en %s"
#. translators: `abra upgrade` command for autocompletion
#: cli/run.go:103
msgid "upgrade"
@@ -6510,6 +6530,11 @@ msgstr "actualizando el servicio %s de %s a %s (etiqueta fijada: %s)"
msgid "upload your recipe to git.coopcloud.tech/coop-cloud/..."
msgstr "sube tu receta a git.coopcloud.tech/coop-cloud/..."
#: pkg/lint/recipe.go:132
#, fuzzy
msgid "use a digest for all images"
msgstr "usar una etiqueta para todas las imágenes"
#: pkg/lint/recipe.go:83
msgid "use a tag for all images"
msgstr "usar una etiqueta para todas las imágenes"
@@ -6561,7 +6586,7 @@ msgstr "validado %s como argumento de receta"
msgid "validated %s as server argument"
msgstr "validado %s como argumento de servidor"
#: pkg/lint/recipe.go:170
#: pkg/lint/recipe.go:177
msgid "vendor config versions in an abra.sh"
msgstr "versiones de configuración del proveedor en un abra.sh"
@@ -6718,7 +6743,7 @@ msgstr "esperando a que %d tarea(s) alcancen un estado terminal"
#: pkg/lint/recipe.go:21 pkg/lint/recipe.go:67 pkg/lint/recipe.go:74
#: pkg/lint/recipe.go:81 pkg/lint/recipe.go:88 pkg/lint/recipe.go:95
#: pkg/lint/recipe.go:102 pkg/lint/recipe.go:109 pkg/lint/recipe.go:116
#: pkg/lint/recipe.go:123
#: pkg/lint/recipe.go:123 pkg/lint/recipe.go:130
msgid "warn"
msgstr "advertencia"
@@ -6792,6 +6817,22 @@ msgstr ""
msgid "{name: %s, "
msgstr "{nombre: %s, "
#, fuzzy, c-format
#~ msgid "unable to parse %s: %s"
#~ msgstr "no se pudo abrir %s: %s"
#, c-format
#~ msgid "parsed %s from %s"
#~ msgstr "parseado %s de %s"
#, c-format
#~ msgid "unable to parse %s, skipping"
#~ msgstr "no se pudo parsear %s; se omitirá"
#, fuzzy, c-format
#~ msgid "updating pinned reference %s to %s in %s"
#~ msgstr "actualizando %s a %s en %s"
#~ msgid "removed freshly created commit"
#~ msgstr "eliminado commit recién creado"
+67 -96
View File
@@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"coopcloud.tech/abra/pkg/formatter"
@@ -110,140 +111,110 @@ func (r Recipe) GetVersionLabelLocal() (string, error) {
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)
// UpdateTag updates the image tag of a service in-place on file system local
// compose files. An existing digest is dropped, since it would still point at
// the image of the old tag.
func (r Recipe) UpdateTag(serviceName, tag string) (bool, error) {
return r.updateServiceImage(serviceName, func(image string) (string, error) {
if strings.Contains(image, "@") {
log.Warn(i18n.G("removing digest from %s for %s, use --pindigests to pin the new tag", image, serviceName))
}
return setImageTag(image, tag, "")
})
}
composeFiles, err := filepath.Glob(fullPattern)
// UpdatePinnedTag updates the image of a service to a pinned version
// (image:tag@sha256:...) in-place on file system local compose files.
func (r Recipe) UpdatePinnedTag(serviceName, tag, digest string) (bool, error) {
return r.updateServiceImage(serviceName, func(image string) (string, error) {
return setImageTag(image, tag, digest)
})
}
// updateServiceImage rewrites the image of a service in all local compose
// files which define it. Only image lines matching the current image exactly
// are touched, so other services using a variant of the same image (e.g.
// nginx:1.21.0-alpine) are left as is.
func (r Recipe) updateServiceImage(serviceName string, rewrite func(image string) (string, error)) (bool, error) {
composeFiles, err := filepath.Glob(fmt.Sprintf("%s/compose**yml", r.Dir))
if err != nil {
return false, err
}
log.Debug(i18n.G("considering %s config(s) for tag update", strings.Join(composeFiles, ", ")))
log.Debug(i18n.G("considering %s config(s) for image update", strings.Join(composeFiles, ", ")))
sampleEnv, err := r.SampleEnv()
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 == "" {
if service.Name != serviceName || service.Image == "" {
continue // may be a compose.$optional.yml file
}
img, _ := reference.ParseNormalizedNamed(service.Image)
old := service.Image
new, err := rewrite(old)
if err != nil {
return false, err
}
if old == new {
continue
}
bytes, err := os.ReadFile(composeFile)
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))
imageLine := regexp.MustCompile(`(?m)^(\s*image:\s*["']?)` + regexp.QuoteMeta(old) + `(["']?\s*(#.*)?)$`)
if !imageLine.Match(bytes) {
// the image is set via a variable, e.g. nginx:${VERSION}
log.Warn(i18n.G("unable to find image %s for %s in %s, please update it manually", old, serviceName, composeFile))
continue
}
composeImage := formatter.StripTagMeta(reference.Path(img))
replacedBytes := imageLine.ReplaceAllString(string(bytes), "${1}"+strings.ReplaceAll(new, "$", "$$")+"${2}")
log.Debug(i18n.G("parsed %s from %s", composeTag, service.Image))
log.Debug(i18n.G("updating %s to %s in %s", old, new, composeFile))
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)
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))
if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
return false, err
}
updated = true
if err := os.WriteFile(composeFile, []byte(replacedBytes), 0o764); err != nil {
return false, err
}
updated = true
}
}
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
// setImageTag replaces the tag and digest of an image while keeping the image
// name as written (e.g. including the registry host). An empty digest leaves
// the image unpinned.
func setImageTag(image, tag, digest string) (string, error) {
if _, err := reference.ParseNormalizedNamed(image); err != nil {
return "", errors.New(i18n.G("unable to parse image %s: %s", image, 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.RemoveSha(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
}
}
name := formatter.RemoveSha(image)
if i := strings.LastIndex(name, ":"); i > strings.LastIndex(name, "/") {
name = name[:i] // strip the tag, but not a registry port
}
return updated, nil
new := fmt.Sprintf("%s:%s", name, tag)
if digest != "" {
new = fmt.Sprintf("%s@%s", new, digest)
}
return new, nil
}
// UpdateLabel updates a label in-place on file system local compose files.
+305
View File
@@ -0,0 +1,305 @@
package recipe
import (
"os"
"path"
"regexp"
"testing"
"coopcloud.tech/abra/pkg/client"
"github.com/distribution/reference"
)
const (
testDigest = "sha256:2bcabc23b45489fb0885d69a06ba1d648aeda973fae7bb981bafbb884165e514"
testNewDigest = "sha256:908dc23e643a1447dbfb2e189ed268bfde6a51a5bf9a34d3dd3440a24f58ccf7"
testAlpineDigest = "sha256:a74534e76ee1121d418fa7394ca930eb67440deda413848bc67c68138535b989"
)
func writeTestRecipe(t *testing.T, compose, sampleEnv string) Recipe {
t.Helper()
dir := t.TempDir()
r := Recipe{
Name: "test_recipe",
Dir: dir,
ComposePath: path.Join(dir, "compose.yml"),
SampleEnvPath: path.Join(dir, ".env.sample"),
}
if err := os.WriteFile(r.ComposePath, []byte(compose), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(r.SampleEnvPath, []byte("TYPE=test_recipe\n"+sampleEnv), 0o644); err != nil {
t.Fatal(err)
}
return r
}
// composeWithImages returns a compose file with the services "app" and "db"
// using the given images, in that order.
func composeWithImages(images ...string) string {
out := "---\nversion: \"3.8\"\n\nservices:\n"
names := []string{"app", "db"}
for i, image := range images {
out += " " + names[i] + ":\n image: " + image + "\n"
}
return out
}
// serviceImages returns the image of each service, as loaded from the recipe
// compose files.
func serviceImages(t *testing.T, r Recipe) map[string]string {
t.Helper()
config, err := r.GetComposeConfig(nil)
if err != nil {
t.Fatal(err)
}
images := make(map[string]string)
for _, service := range config.Services {
images[service.Name] = service.Image
}
return images
}
func assertImages(t *testing.T, r Recipe, want map[string]string) {
t.Helper()
got := serviceImages(t, r)
if len(got) != len(want) {
t.Fatalf("got services %v, want %v", got, want)
}
for service, image := range want {
if got[service] != image {
t.Errorf("service %s: got image %s, want %s", service, got[service], image)
}
}
}
func TestUpdatePinnedTag(t *testing.T) {
tests := []struct {
name string
images []string
sampleEnv string
service string
tag string
digest string
wantUpdated bool
wantImages map[string]string
}{
{
name: "pin unpinned image",
images: []string{"nginx:1.21.0"},
service: "app",
tag: "1.21.6",
digest: testDigest,
wantUpdated: true,
wantImages: map[string]string{"app": "nginx:1.21.6@" + testDigest},
},
{
name: "pin same tag",
images: []string{"nginx:1.21.6"},
service: "app",
tag: "1.21.6",
digest: testDigest,
wantUpdated: true,
wantImages: map[string]string{"app": "nginx:1.21.6@" + testDigest},
},
{
name: "replace existing digest",
images: []string{"nginx:1.21.6@" + testDigest},
service: "app",
tag: "1.31.6",
digest: testNewDigest,
wantUpdated: true,
wantImages: map[string]string{"app": "nginx:1.31.6@" + testNewDigest},
},
{
name: "already pinned",
images: []string{"nginx:1.21.6@" + testDigest},
service: "app",
tag: "1.21.6",
digest: testDigest,
wantUpdated: false,
wantImages: map[string]string{"app": "nginx:1.21.6@" + testDigest},
},
{
name: "only the given service is pinned",
images: []string{"nginx:1.21.0", "postgres:15"},
service: "app",
tag: "1.21.6",
digest: testDigest,
wantUpdated: true,
wantImages: map[string]string{"app": "nginx:1.21.6@" + testDigest, "db": "postgres:15"},
},
{
name: "same image in another service is left alone",
images: []string{"nginx:1.21.0", "nginx:1.21.0-alpine"},
service: "app",
tag: "1.21.6",
digest: testDigest,
wantUpdated: true,
wantImages: map[string]string{"app": "nginx:1.21.6@" + testDigest, "db": "nginx:1.21.0-alpine"},
},
{
name: "keep registry host",
images: []string{"ghcr.io/foo/bar:1.0"},
service: "app",
tag: "1.1",
digest: testDigest,
wantUpdated: true,
wantImages: map[string]string{"app": "ghcr.io/foo/bar:1.1@" + testDigest},
},
{
name: "keep registry port",
images: []string{"localhost:5000/foo/bar:1.0"},
service: "app",
tag: "1.1",
digest: testDigest,
wantUpdated: true,
wantImages: map[string]string{"app": "localhost:5000/foo/bar:1.1@" + testDigest},
},
{
name: "quoted image",
images: []string{`"nginx:1.21.0"`},
service: "app",
tag: "1.21.6",
digest: testDigest,
wantUpdated: true,
wantImages: map[string]string{"app": "nginx:1.21.6@" + testDigest},
},
{
name: "skip image with variable",
images: []string{"nginx:${NGINX_VERSION}"},
sampleEnv: "NGINX_VERSION=1.21.0\n",
service: "app",
tag: "1.21.6",
digest: testDigest,
wantUpdated: false,
wantImages: map[string]string{"app": "nginx:1.21.0"},
},
{
name: "no matching service",
images: []string{"nginx:1.21.0"},
service: "db",
tag: "1.21.6",
digest: testDigest,
wantUpdated: false,
wantImages: map[string]string{"app": "nginx:1.21.0"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
r := writeTestRecipe(t, composeWithImages(test.images...), test.sampleEnv)
updated, err := r.UpdatePinnedTag(test.service, test.tag, test.digest)
if err != nil {
t.Fatal(err)
}
if updated != test.wantUpdated {
t.Errorf("got updated %v, want %v", updated, test.wantUpdated)
}
assertImages(t, r, test.wantImages)
})
}
}
func TestUpdatePinnedTagInvalidImage(t *testing.T) {
r := writeTestRecipe(t, composeWithImages("Nginx:1.21.0"), "")
if _, err := r.UpdatePinnedTag("app", "1.21.6", testDigest); err == nil {
t.Fatal("expected an error for an invalid image")
}
assertImages(t, r, map[string]string{"app": "Nginx:1.21.0"})
}
func TestUpdateTag(t *testing.T) {
r := writeTestRecipe(t, composeWithImages("nginx:1.21.0", "nginx:1.21.0-alpine"), "")
updated, err := r.UpdateTag("app", "1.21.6")
if err != nil {
t.Fatal(err)
}
if !updated {
t.Error("expected image to be updated")
}
assertImages(t, r, map[string]string{"app": "nginx:1.21.6", "db": "nginx:1.21.0-alpine"})
updated, err = r.UpdateTag("app", "1.21.6")
if err != nil {
t.Fatal(err)
}
if updated {
t.Error("expected no update for the same tag")
}
}
// An existing digest points at the old image, keeping it after a tag
// upgrade would silently deploy the previous version under the new tag.
func TestUpdateTagDropsStaleDigest(t *testing.T) {
r := writeTestRecipe(t, composeWithImages("nginx:1.21.6@"+testDigest), "")
updated, err := r.UpdateTag("app", "1.31.6")
if err != nil {
t.Fatal(err)
}
if !updated {
t.Error("expected image to be updated")
}
assertImages(t, r, map[string]string{"app": "nginx:1.31.6"})
}
// Services using variants of the same image must each be pinned to the
// digest of their own tag, in whichever order they are upgraded.
func TestUpdatePinnedTagSharedImageIntegration(t *testing.T) {
img, err := reference.ParseNormalizedNamed("nginx")
if err != nil {
t.Fatal(err)
}
upgradeTags := map[string]string{"app": "1.21.6", "db": "1.21.6-alpine"}
digests := make(map[string]string)
for service, tag := range upgradeTags {
digest, err := client.GetImageMultiplatformDigest(img, tag)
if err != nil {
t.Fatal(err)
}
if !regexp.MustCompile(`^sha256:[a-f0-9]{64}$`).MatchString(digest) {
t.Fatalf("unexpected digest %s for %s", digest, tag)
}
digests[service] = digest
}
if digests["app"] == digests["db"] {
t.Fatalf("expected different digests for %v, got %s", upgradeTags, digests["app"])
}
for _, order := range [][]string{{"app", "db"}, {"db", "app"}} {
t.Run(order[0]+" first", func(t *testing.T) {
r := writeTestRecipe(t, composeWithImages("nginx:1.21.0", "nginx:1.21.0-alpine"), "")
for _, service := range order {
updated, err := r.UpdatePinnedTag(service, upgradeTags[service], digests[service])
if err != nil {
t.Fatal(err)
}
if !updated {
t.Errorf("expected %s to be updated", service)
}
}
assertImages(t, r, map[string]string{
"app": "nginx:1.21.6@" + digests["app"],
"db": "nginx:1.21.6-alpine@" + digests["db"],
})
})
}
}