forked from toolshed/abra
WIP: address digest pinning review comments
This commit is contained in:
@@ -7,3 +7,4 @@
|
||||
/bin
|
||||
dist/
|
||||
tests/integration/.bats
|
||||
.profile/
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user