WIP: address digest pinning review comments

This commit is contained in:
2026-10-04 23:19:21 -04:00
parent 452cf1a468
commit 5fea6e0815
10 changed files with 63 additions and 45 deletions
+1
View File
@@ -7,3 +7,4 @@
/bin
dist/
tests/integration/.bats
.profile/
+1 -1
View File
@@ -343,7 +343,7 @@ func GetImageVersions(recipe recipePkg.Recipe) (map[string]string, error) {
continue
}
services[path] = formatter.ImageVersion(img)
services[path] = formatter.GetTagAndDigest(img)
}
if missingTag {
+8 -15
View File
@@ -209,9 +209,7 @@ interface.`),
log.Debug(i18n.G("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name))
_, ok := servicePins[service.Name]
if upgradeTag != "" {
// No newer tag: keep the current target for pinning.
} else if versionsPresent && ok {
if upgradeTag == "" && versionsPresent && ok {
pinnedTag := servicePins[service.Name].version
if tag.IsLessThan(pinnedTag) {
pinnedTagString := pinnedTag.String()
@@ -233,7 +231,7 @@ interface.`),
log.Fatal(i18n.G("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String()))
continue
}
} else {
} else if upgradeTag == "" {
if bumpType != 0 {
for _, upTag := range compatible {
upElement, err := tag.UpgradeDelta(upTag)
@@ -302,26 +300,21 @@ interface.`),
}
}
if upgradeTag != "skip" {
pin := shouldPin(service.Image, internal.PinDigests)
var ok bool
var err error
var resolvedDigest string
if pin {
digest, dErr := client.GetImageMultiplatformDigest(img, upgradeTag)
if dErr != nil {
log.Fatal(i18n.G("failed to resolve digest for %s:%s: %s", image, upgradeTag, dErr.Error()))
if shouldPin(service.Image, internal.PinDigests) {
resolvedDigest, err = client.GetImageMultiplatformDigest(img, upgradeTag)
if err != nil {
log.Fatal(i18n.G("failed to resolve digest for %s:%s: %s", image, upgradeTag, err.Error()))
}
resolvedDigest = digest
}
ok, err = recipe.UpdateTag(service.Image, upgradeTag, resolvedDigest)
ok, err := recipe.UpdateTag(service.Image, upgradeTag, resolvedDigest)
if err != nil {
log.Fatal(err)
}
if ok {
if pin {
if shouldPin(service.Image, 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))
+6 -5
View File
@@ -37,12 +37,13 @@ func GetRegistryTags(img reference.Named) ([]string, error) {
// See: https://docs.docker.com/build/building/multi-platform/#why-multi-platform-builds
// and: https://docs.docker.com/dhi/explore/security-concepts/digests/#multi-platform-images-and-manifests
func GetImageMultiplatformDigest(img reference.Named, tag string) (string, error) {
// Drop any digest. A reference with both a tag and a digest is fetched by
// digest, which would pin the new tag to the old image.
name := reference.TrimNamed(img).Name()
r, err := ref.New(fmt.Sprintf("%s:%s", name, tag))
tagged, err := reference.WithTag(reference.TrimNamed(img), tag)
if err != nil {
return "", errors.New(i18n.G("failed to parse image %s:%s, saw: %s", name, tag, err.Error()))
return "", errors.New(i18n.G("failed to parse image %s:%s, saw: %s", img.Name(), tag, err.Error()))
}
r, err := ref.New(tagged.String())
if err != nil {
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
+13
View File
@@ -38,4 +38,17 @@ func TestGetImageMultiplatformDigest(t *testing.T) {
if older == newer {
t.Errorf("expected different digests for 1.21.5 and 1.21.6, got %s", newer)
}
// A previous pin must not override the requested tag.
pinned, err := reference.ParseNormalizedNamed("nginx:1.21.5@" + older)
if err != nil {
t.Fatal(err)
}
digest, err := GetImageMultiplatformDigest(pinned, "1.21.6")
if err != nil {
t.Fatal(err)
}
if digest != newer {
t.Errorf("got digest %s, want %s for the new tag", digest, newer)
}
}
+2 -2
View File
@@ -171,7 +171,7 @@ func GetImagesForStack(cl *dockerClient.Client, app appPkg.App) (map[string]stri
images[imageBaseName] = ""
continue
}
imageTag := formatter.ImageVersion(namedTag)
imageTag := formatter.GetTagAndDigest(namedTag)
existingImageVersion, ok := images[imageBaseName]
if !ok {
@@ -294,7 +294,7 @@ func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *com
newImages[imageBaseName] = ""
continue
}
imageTag := formatter.ImageVersion(namedTag)
imageTag := formatter.GetTagAndDigest(namedTag)
existingImageVersion, ok := newImages[imageBaseName]
if !ok {
+2 -2
View File
@@ -41,8 +41,8 @@ func RemoveSha(str string) string {
return strings.Split(str, "@")[0]
}
// ImageVersion returns the tag and any digest for comparison and display.
func ImageVersion(image reference.Named) string {
// GetTagAndDigest returns the tag and any digest for comparison and display.
func GetTagAndDigest(image reference.Named) string {
var version string
if tagged, ok := image.(reference.NamedTagged); ok {
version = tagged.Tag()
+2 -2
View File
@@ -18,7 +18,7 @@ func TestBoldDirtyDefault(t *testing.T) {
assert.Equal(t, "foo", BoldDirtyDefault("foo"))
}
func TestImageVersion(t *testing.T) {
func TestGetTagAndDigest(t *testing.T) {
digest := "sha256:908dc23e643a1447dbfb2e189ed268bfde6a51a5bf9a34d3dd3440a24f58ccf7"
for _, tc := range []struct{ image, want string }{
{"nginx:1.21.6", "1.21.6"},
@@ -30,7 +30,7 @@ func TestImageVersion(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if got := ImageVersion(img); got != tc.want {
if got := GetTagAndDigest(img); got != tc.want {
t.Errorf("%s: got %s, want %s", tc.image, got, tc.want)
}
}
+3 -8
View File
@@ -16,7 +16,6 @@ import (
loader "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/distribution/reference"
composetypes "github.com/docker/cli/cli/compose/types"
godigest "github.com/opencontainers/go-digest"
)
// GetComposeFiles gets the list of compose files for an app (or recipe if you
@@ -181,19 +180,15 @@ func (r Recipe) UpdateTag(image, tag, digest string) (bool, error) {
if err != nil {
return false, err
}
var target reference.Named = tagged
new := reference.FamiliarString(tagged)
if digest != "" {
target, err = reference.WithDigest(tagged, godigest.Digest(digest))
if err != nil {
return false, err
}
new += "@" + digest
}
new := reference.FamiliarString(target)
if old == new {
continue
}
// Match a complete image value, not a prefix of another image.
// Match complete image values so sibling tags and existing pins aren't rewritten.
pattern := regexp.MustCompile(`(?m)(^[\t ]*image:[\t ]*["']?)` + regexp.QuoteMeta(old) + `(["']?[\t ]*(?:#.*)?$)`)
replacedBytes := pattern.ReplaceAllStringFunc(string(bytes), func(line string) string {
match := pattern.FindStringSubmatch(line)
+25 -10
View File
@@ -703,49 +703,64 @@ teardown(){
assert_output --partial "unexpected character"
}
# Deploy pinned 1.21.5, upgrade to pinned 1.21.6, then roll back to 1.21.5.
# bats test_tags=slow
@test "deploy, upgrade and rollback digest-pinned images" {
recipeDir="$ABRA_DIR/recipes/$TEST_RECIPE"
service="${TEST_APP_DOMAIN//./_}_app"
# https://git.coopcloud.tech/toolshed/abra-test-recipe/commit/4f0642719d2e2a568f4350cf7eea182438d58be3
run git -C "$recipeDir" checkout 4f0642719d2e2a568f4350cf7eea182438d58be3
# Manually upgrade the unpinned 0.3.5 recipe to 1.21.5 and pin it.
run git -C "$recipeDir" checkout "0.3.5+1.21.0"
assert_success
run docker pull nginx:1.21.5
assert_success
run docker image inspect --format '{{index .RepoDigests 0}}' nginx:1.21.5
assert_success
oldImage="nginx:1.21.5@${output#*@}"
run sed -i "s|image: nginx:1.21.0|image: $oldImage|" "$recipeDir/compose.yml"
assert_success
# Label and tag the local recipe release so app commands can track it.
run sed -i 's/version=0.3.5+1.21.0/version=0.4.0+1.21.5/' "$recipeDir/compose.yml"
assert_success
_git_commit
run git -C "$recipeDir" tag "0.4.0+1.21.5"
assert_success
# https://git.coopcloud.tech/toolshed/abra-test-recipe/commit/77d752b3c61a71bfb2c45f40ec07c9d626a0e580
run git -C "$recipeDir" checkout 77d752b3c61a71bfb2c45f40ec07c9d626a0e580
# Manually upgrade and pin the next release to 1.21.6.
run docker pull nginx:1.21.6
assert_success
run $ABRA recipe upgrade "$TEST_RECIPE" --patch --pindigests --chaos --offline --no-input
run docker image inspect --format '{{index .RepoDigests 0}}' nginx:1.21.6
assert_success
run sed -i 's/version=0.3.5+1.21.0/version=0.4.1+1.21.6/' "$recipeDir/compose.yml"
newImage="nginx:1.21.6@${output#*@}"
run sed -i "s|image: $oldImage|image: $newImage|" "$recipeDir/compose.yml"
assert_success
# Update the release label to match the next recipe tag.
run sed -i 's/version=0.4.0+1.21.5/version=0.4.1+1.21.6/' "$recipeDir/compose.yml"
assert_success
_git_commit
run git -C "$recipeDir" tag "0.4.1+1.21.6"
assert_success
# Deploy recipe 0.4.0 with the exact pinned 1.21.5 image.
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.4.0+1.21.5" --offline --no-input --no-converge-checks
assert_success
run docker service inspect --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}' "$service"
assert_success
assert_output --regexp '^nginx:1\.21\.5@sha256:[a-f0-9]{64}$'
oldImage="$output"
assert_output "$oldImage"
# Upgrade to recipe 0.4.1 and the exact pinned 1.21.6 image.
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.4.1+1.21.6" --offline --no-input --no-converge-checks
assert_success
run docker service inspect --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}' "$service"
assert_success
assert_output --regexp '^nginx:1\.21\.6@sha256:[a-f0-9]{64}$'
assert_output "$newImage"
# Roll back to recipe 0.4.0 and its original pinned image.
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.4.0+1.21.5" --offline --no-input --no-converge-checks
assert_success
run docker service inspect --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}' "$service"
assert_success
assert_output "$oldImage"
}