From 6374d35bbc8b09b81e40d8de88c4e1126547399e Mon Sep 17 00:00:00 2001 From: sixsmith Date: Sat, 3 Oct 2026 02:03:05 -0400 Subject: [PATCH] update --- Makefile | 10 +- cli/recipe/upgrade.go | 13 +- cli/recipe/upgrade_e2e_test.go | 227 ++++++++++++++++---------------- pkg/client/registry_e2e_test.go | 111 ++++------------ pkg/formatter/formatter.go | 1 + pkg/recipe/compose.go | 1 + pkg/recipe/compose_e2e_test.go | 199 ++++++++++++---------------- pkg/recipe/git.go | 6 +- 8 files changed, 243 insertions(+), 325 deletions(-) diff --git a/Makefile b/Makefile index f79d137c..d3839533 100644 --- a/Makefile +++ b/Makefile @@ -40,10 +40,16 @@ check: (echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1) test: - @go test ./... -cover -v -p 1 + @go test ./... -short -failfast -cover -v -p 1 + +integration-test: + @go test ./... -short=false -failfast -cover -v -p 1 -count=1 find-tests: - @find . -name "*_test.go" + @find . -iname "*_test.go" + +find-integration-test: + @find . -iname "*_e2e_test.go" loc: @find . -name "*.go" | xargs wc -l diff --git a/cli/recipe/upgrade.go b/cli/recipe/upgrade.go index 7702840b..0acc9005 100644 --- a/cli/recipe/upgrade.go +++ b/cli/recipe/upgrade.go @@ -181,10 +181,11 @@ interface.`), var upgradeTag string 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)) - if !shouldPin(service.Image, internal.PinDigests) { - continue // skip on to the next tag and don't update any compose files + + // Might still want to pin the tag if the user has requested it, even if there are no upgrades available. + if internal.PinDigests { + upgradeTag = tag.String() } - upgradeTag = tag.String() } catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, internal.Offline) @@ -390,8 +391,8 @@ interface.`), // shouldPin reports whether a service image should be pinned to a digest. // An image that is already pinned stays pinned, even without --pindigests. -func shouldPin(image string, pinDigests bool) bool { - return pinDigests || strings.Contains(image, "@sha256:") +func shouldPin(image string, pinDigestsFlag bool) bool { + return pinDigestsFlag || strings.Contains(image, "@sha256:") } var ( @@ -452,7 +453,7 @@ func init() { &internal.PinDigests, i18n.G("pindigests"), i18n.GC("p", "pin container image by digest"), - false, + true, i18n.G("pin the container image version by manifest digest (image:tag@sha256:...)"), ) } diff --git a/cli/recipe/upgrade_e2e_test.go b/cli/recipe/upgrade_e2e_test.go index 1bb27812..788cc215 100644 --- a/cli/recipe/upgrade_e2e_test.go +++ b/cli/recipe/upgrade_e2e_test.go @@ -1,67 +1,99 @@ package recipe import ( + "bytes" "os" - "os/exec" "path/filepath" "regexp" + "strconv" "strings" "testing" + "coopcloud.tech/abra/cli/internal" + "coopcloud.tech/abra/pkg/config" + gitPkg "coopcloud.tech/abra/pkg/git" + "coopcloud.tech/abra/pkg/log" recipePkg "coopcloud.tech/abra/pkg/recipe" ) -func TestUpgradePinsCurrentImage(t *testing.T) { - dir := t.TempDir() - t.Setenv("ABRA_DIR", dir) - recipeDir := filepath.Join(dir, "recipes", "abra-test-recipe") - // EnsureLatest checks out the default branch, so the fixture has to live there. - cloneTestRecipe(t, recipeDir, "") - replaceImage(t, filepath.Join(recipeDir, "compose.yml"), "nginx:1.21.0", "nginx:1.21.6") - runGit(t, recipeDir, "commit", "-am", "already current") +func TestUpgradePinning(t *testing.T) { + if testing.Short() { + t.Skip("skipping e2e test in short mode") + } - out := runUpgrade(t, dir, "abra-test-recipe", "--no-input", "--patch", "--pindigests") + setupTestRecipeCatalogue(t) + // Test recipe is at nginx:1.21.6 and unpinned + // https://git.coopcloud.tech/sixsmith/abra-test-recipe/commit/ad6f6c7cfe129062a864532abacd00c6fb01f762 + setupTestRecipe(t, "ad6f6c7cfe129062a864532abacd00c6fb01f762") + + // We should find nginx >1.21.6 (minor version bump), upgrade to + // it and pin to the digest. + out := runUpgrade(t, "abra-test-recipe", false, true, true, true) + match := regexp.MustCompile(`tag upgraded and pinned from 1\.21\.6 to 1\.(\d+)\.\d+@sha256:[a-f0-9]{64}`).FindStringSubmatch(out) + if match == nil { + t.Fatalf("expected a pinned minor upgrade, got:\n%s", out) + } + if minor, _ := strconv.Atoi(match[1]); minor <= 21 { + t.Fatalf("expected a minor version above 1.21, got:\n%s", out) + } + image := getActualRecipeImage(t, "abra-test-recipe", "app") + if !regexp.MustCompile(`^nginx:1\.` + match[1] + `\.\d+@sha256:[a-f0-9]{64}$`).MatchString(image) { + t.Fatalf("got image %s, want nginx:1.%s.x@sha256:<64 hex>", image, match[1]) + } +} + +func TestPinningNoUpgrade(t *testing.T) { + if testing.Short() { + t.Skip("skipping e2e test in short mode") + } + + setupTestRecipeCatalogue(t) + // Test recipe is at nginx:1.21.6 and unpinned + // https://git.coopcloud.tech/sixsmith/abra-test-recipe/commit/ad6f6c7cfe129062a864532abacd00c6fb01f762 + setupTestRecipe(t, "ad6f6c7cfe129062a864532abacd00c6fb01f762") + + // Upgrade with --pindigests should pin the current image, even if there's no new upgrade available. + out := runUpgrade(t, "abra-test-recipe", true, false, true, true) if !strings.Contains(out, "tag upgraded and pinned from 1.21.6 to 1.21.6@sha256:") { t.Fatalf("expected current image to be pinned, got:\n%s", out) } - image := serviceImage(t, recipeDir, "app") + image := getActualRecipeImage(t, "abra-test-recipe", "app") if !regexp.MustCompile(`^nginx:1\.21\.6@sha256:[a-f0-9]{64}$`).MatchString(image) { t.Fatalf("got image %s, want nginx:1.21.6@sha256:<64 hex>", image) } } -func TestUpgradeKeepsExistingPin(t *testing.T) { - dir := t.TempDir() - t.Setenv("ABRA_DIR", dir) - recipeDir := filepath.Join(dir, "recipes", "abra-test-recipe") - cloneTestRecipe(t, recipeDir, "") - replaceImage(t, filepath.Join(recipeDir, "compose.yml"), "nginx:1.21.0", - "nginx:1.21.0@sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31") - runGit(t, recipeDir, "commit", "-am", "pin") - - out := runUpgrade(t, dir, "abra-test-recipe", "--no-input", "--patch") - if !strings.Contains(out, "tag upgraded and pinned from 1.21.0 to 1.21.6@sha256:") { - t.Fatalf("expected existing pin to be kept, got:\n%s", out) +// Ensure that --pindigests works on recipes that have already pinned their digest +func TestOverwriteExistingPin(t *testing.T) { + if testing.Short() { + t.Skip("skipping e2e test in short mode") } - image := serviceImage(t, recipeDir, "app") + + setupTestRecipeCatalogue(t) + // Test recipe is at nginx:1.21.5 and pinned + // https://git.coopcloud.tech/sixsmith/abra-test-recipe/commit/0a1facfba9a1870c212e01d4a7d64a1d3d4669f1 + setupTestRecipe(t, "0a1facfba9a1870c212e01d4a7d64a1d3d4669f1") + + out := runUpgrade(t, "abra-test-recipe", true, false, false, true) + if !strings.Contains(out, "tag upgraded and pinned from 1.21.5 to 1.21.6@sha256:") { + t.Fatalf("should ignore --pindigests if recipe already pins, got:\n%s", out) + } + image := getActualRecipeImage(t, "abra-test-recipe", "app") if !regexp.MustCompile(`^nginx:1\.21\.6@sha256:[a-f0-9]{64}$`).MatchString(image) { - t.Fatalf("got image %s, want a re-pinned 1.21.6", image) + t.Fatalf("got image %s, want 1.21.6@sha256...", image) } if strings.Contains(image, "0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31") { - t.Fatalf("kept the digest of the old tag: %s", image) + t.Fatalf("updated image tag but kept digest: %s", image) } } -func TestUpgradeCatalogueRecipes(t *testing.T) { +func TestUpgradeAll(t *testing.T) { + t.Skip() // Massive integration test, all recipes. 15+ minutes to run. if testing.Short() { - t.Skip("clones catalogue recipes") + t.Skip("skipping e2e test in short mode") } - dir := t.TempDir() - t.Setenv("ABRA_DIR", dir) - if err := os.MkdirAll(filepath.Join(dir, "recipes"), 0o755); err != nil { - t.Fatal(err) - } + setupTestRecipeCatalogue(t) catalogue, err := recipePkg.ReadRecipeCatalogue(false) if err != nil { @@ -71,62 +103,21 @@ func TestUpgradeCatalogueRecipes(t *testing.T) { t.Fatal("recipe catalogue is empty") } - names := []string{"abra-test-recipe", "photoprism", "funkwhale"} - for _, name := range names { - if _, ok := catalogue[name]; !ok { - t.Fatalf("recipe %s missing from catalogue", name) - } - } - - bin := abraBin(t) - for _, name := range names { - t.Run(name, func(t *testing.T) { - recipe := recipePkg.Get(name) + for _, rm := range catalogue { + t.Run(rm.Name, func(t *testing.T) { + recipe := recipePkg.Get(rm.Name) if err := recipe.EnsureExists(); err != nil { t.Fatal(err) } - cmd := exec.Command(bin, "recipe", "upgrade", name, "--no-input", "--offline") - cmd.Env = append(os.Environ(), "ABRA_DIR="+dir) - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("recipe upgrade %s: %v\n%s", name, err, out) - } + runUpgrade(t, rm.Name, false, false, true, false) }) } } -func cloneTestRecipe(t *testing.T, dir, ref string) { +func getActualRecipeImage(t *testing.T, recipe, service string) string { t.Helper() - runGit(t, "", "clone", "https://git.coopcloud.tech/toolshed/abra-test-recipe", dir) - runGit(t, dir, "config", "user.name", "t") - runGit(t, dir, "config", "user.email", "t@t") - if ref != "" { - runGit(t, dir, "checkout", ref) - } -} - -func replaceImage(t *testing.T, path, old, newImage string) { - t.Helper() - b, err := os.ReadFile(path) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(string(b), old) { - t.Fatalf("%s does not contain %s", path, old) - } - if err := os.WriteFile(path, []byte(strings.Replace(string(b), old, newImage, 1)), 0o644); err != nil { - t.Fatal(err) - } -} - -func serviceImage(t *testing.T, dir, service string) string { - t.Helper() - r := recipePkg.Recipe{ - Name: "abra-test-recipe", - Dir: dir, - SampleEnvPath: filepath.Join(dir, ".env.sample"), - } + r := recipePkg.Get(recipe) cfg, err := r.GetComposeConfig(nil) if err != nil { t.Fatal(err) @@ -140,53 +131,61 @@ func serviceImage(t *testing.T, dir, service string) string { return "" } -func runUpgrade(t *testing.T, abraDir, name string, args ...string) string { - t.Helper() - cmd := exec.Command(abraBin(t), append([]string{"recipe", "upgrade", name}, args...)...) - cmd.Env = append(os.Environ(), "ABRA_DIR="+abraDir) - out, err := cmd.CombinedOutput() +// Use a temporary ABRA_DIR for the whole run +func TestMain(m *testing.M) { + dir, err := os.MkdirTemp("", "abra-test-") if err != nil { - t.Fatalf("recipe upgrade: %v\n%s", err, out) + panic(err) } - return string(out) + os.Setenv("ABRA_DIR", dir) + config.ABRA_DIR = dir + config.CATALOGUE_DIR = filepath.Join(dir, "catalogue") + config.RECIPES_JSON = filepath.Join(config.CATALOGUE_DIR, "recipes.json") + + code := m.Run() + os.RemoveAll(dir) + os.Exit(code) } -func abraBin(t *testing.T) string { +// Give each test its own empty recipes directory. +func setupTestRecipeCatalogue(t *testing.T) { t.Helper() - bin := filepath.Join(t.TempDir(), "abra") - cmd := exec.Command("go", "build", "-ldflags", "-X main.Commit=12345678 -X main.Version=dev", "-o", bin, "./cmd/abra") - cmd.Dir = repoRoot(t) - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("build abra: %v\n%s", err, out) - } - return bin + config.RECIPES_DIR = t.TempDir() } -func repoRoot(t *testing.T) string { +// Use a test recipe at the given commit. +func setupTestRecipe(t *testing.T, commit string) { t.Helper() - dir, err := os.Getwd() - if err != nil { + trd := filepath.Join(config.RECIPES_DIR, "abra-test-recipe") + if err := gitPkg.Clone(trd, "https://git.coopcloud.tech/sixsmith/abra-test-recipe"); err != nil { t.Fatal(err) } - for { - if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { - return dir - } - parent := filepath.Dir(dir) - if parent == dir { - t.Fatal("go.mod not found") - } - dir = parent + + r := recipePkg.Get("abra-test-recipe") + if _, err := r.EnsureVersion(commit); err != nil { + t.Fatal(err) } } -func runGit(t *testing.T, dir string, args ...string) { +func runUpgrade(t *testing.T, recipe string, patch, minor, pinDigests, chaos bool) string { t.Helper() - cmd := exec.Command("git", args...) - cmd.Dir = dir - cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t") - if out, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out) + + internal.NoInput = true + internal.Offline = true + internal.Chaos = chaos + internal.Patch = patch + internal.Minor = minor + internal.PinDigests = pinDigests + + var out bytes.Buffer + log.Logger.SetOutput(&out) + defer log.Logger.SetOutput(os.Stdout) + oldFatal := log.Fatal + log.Fatal = func(message any, keyvals ...any) { + t.Fatalf("recipe upgrade %s: %v\n%s", recipe, message, out.String()) } + defer func() { log.Fatal = oldFatal }() + + RecipeUpgradeCommand.Run(RecipeUpgradeCommand, []string{recipe}) + return out.String() } diff --git a/pkg/client/registry_e2e_test.go b/pkg/client/registry_e2e_test.go index ff706f72..91f99f78 100644 --- a/pkg/client/registry_e2e_test.go +++ b/pkg/client/registry_e2e_test.go @@ -1,102 +1,41 @@ package client import ( - "context" "regexp" "testing" "github.com/distribution/reference" - "github.com/regclient/regclient" - "github.com/regclient/regclient/types/ref" ) +// Exercise function that looks up multiplatform digests, given an +// image name and tag. This is an e2e test, since it requires network access to the registry. func TestGetImageMultiplatformDigest(t *testing.T) { - img, err := reference.ParseNormalizedNamed("nginx") - if err != nil { - t.Fatal(err) + if testing.Short() { + t.Skip("skipping e2e test in short mode") } - 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) - } + nginx, _ := reference.ParseNormalizedNamed("nginx") - 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) - } -} - -// The digest must be the one of the manifest list / index, which covers all -// platforms, and not the one of the manifest for a single platform. -// Otherwise a pinned image can only be deployed on one architecture. -func TestGetImageMultiplatformDigestIsIndex(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) - } - - r, err := ref.New("nginx:1.21.6") - if err != nil { - t.Fatal(err) - } - - m, err := regclient.New().ManifestGet(context.Background(), r.SetDigest(digest)) - if err != nil { - t.Fatal(err) - } - if !m.IsList() { - t.Fatalf("%s is a single platform manifest (%s), want an index", digest, m.GetDescriptor().MediaType) - } - - platforms, err := m.GetManifestList() - if err != nil { - t.Fatal(err) - } - - found := make(map[string]bool) - for _, p := range platforms { - if p.Digest.String() == digest { - t.Errorf("digest %s is also the manifest of platform %s", digest, p.Platform) - } - if p.Platform != nil { - found[p.Platform.OS+"/"+p.Platform.Architecture] = true - } - } - - for _, want := range []string{"linux/amd64", "linux/arm64"} { - if !found[want] { - t.Errorf("index %s is missing platform %s, found %v", digest, want, found) - } - } -} - -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 { + // Should fail. + if _, err := GetImageMultiplatformDigest(nginx, "0.0.0"); err == nil { t.Fatal("expected an error for a missing tag") } + + older, err := GetImageMultiplatformDigest(nginx, "1.21.5") + if err != nil { + t.Fatal(err) + } + newer, err := GetImageMultiplatformDigest(nginx, "1.21.6") + if err != nil { + t.Fatal(err) + } + + for _, digest := range []string{older, newer} { + if !regexp.MustCompile(`^sha256:[a-f0-9]{64}$`).MatchString(digest) { + t.Fatalf("unexpected digest %s", digest) + } + } + if older == newer { + t.Errorf("expected different digests for 1.21.5 and 1.21.6, got %s", newer) + } } diff --git a/pkg/formatter/formatter.go b/pkg/formatter/formatter.go index 5678a91d..b157c49e 100644 --- a/pkg/formatter/formatter.go +++ b/pkg/formatter/formatter.go @@ -224,6 +224,7 @@ func CreateProgressbar(length int, title string) *progressbar.ProgressBar { } // StripTagMeta strips front-matter image tag data that we don't need for parsing. +// NB: Please preserve digests (sha256:...) func StripTagMeta(image string) string { originalImage := image diff --git a/pkg/recipe/compose.go b/pkg/recipe/compose.go index da93a032..1f77218b 100644 --- a/pkg/recipe/compose.go +++ b/pkg/recipe/compose.go @@ -116,6 +116,7 @@ var sha256Digest = regexp.MustCompile(`^sha256:[a-f0-9]{64}$`) // UpdateTag updates an image tag in-place on file system local compose files. // A non-empty digest pins the tag (image:tag@sha256:...). func (r Recipe) UpdateTag(image, tag, digest string) (bool, error) { + // Optional parameter, but must be a valid sha256 digest if provided. if digest != "" && !sha256Digest.MatchString(digest) { return false, errors.New(i18n.G("invalid digest %s, want sha256:<64 hex digits>", digest)) } diff --git a/pkg/recipe/compose_e2e_test.go b/pkg/recipe/compose_e2e_test.go index 0ed9389d..a8aa7402 100644 --- a/pkg/recipe/compose_e2e_test.go +++ b/pkg/recipe/compose_e2e_test.go @@ -1,133 +1,68 @@ package recipe import ( - "os" - "path" "testing" + + "coopcloud.tech/abra/pkg/config" + gitPkg "coopcloud.tech/abra/pkg/git" ) -const ( - testDigest = "sha256:2bcabc23b45489fb0885d69a06ba1d648aeda973fae7bb981bafbb884165e514" - testNewDigest = "sha256:908dc23e643a1447dbfb2e189ed268bfde6a51a5bf9a34d3dd3440a24f58ccf7" -) - -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) - } - } -} +const testDigest = "sha256:908dc23e643a1447dbfb2e189ed268bfde6a51a5bf9a34d3dd3440a24f58ccf7" func TestUpdateTagPinsDigest(t *testing.T) { + if testing.Short() { + t.Skip("skipping e2e test in short mode") + } + tests := []struct { name string - images []string + commit string tag string digest string wantUpdated bool - wantImages map[string]string + wantImage string }{ { - name: "pin unpinned image", - images: []string{"nginx:1.21.0"}, + name: "pin unpinned image", + // https://git.coopcloud.tech/sixsmith/abra-test-recipe/commit/ad6f6c7cfe129062a864532abacd00c6fb01f762 + commit: "ad6f6c7cfe129062a864532abacd00c6fb01f762", + tag: "1.22.0", + digest: testDigest, + wantUpdated: true, + wantImage: "nginx:1.22.0@" + testDigest, + }, + { + name: "pin same tag", + // https://git.coopcloud.tech/sixsmith/abra-test-recipe/commit/ad6f6c7cfe129062a864532abacd00c6fb01f762 + commit: "ad6f6c7cfe129062a864532abacd00c6fb01f762", tag: "1.21.6", digest: testDigest, wantUpdated: true, - wantImages: map[string]string{"app": "nginx:1.21.6@" + testDigest}, + wantImage: "nginx:1.21.6@" + testDigest, }, { - name: "pin same tag", - images: []string{"nginx:1.21.6"}, + name: "replace existing digest", + // https://git.coopcloud.tech/sixsmith/abra-test-recipe/commit/0a1facfba9a1870c212e01d4a7d64a1d3d4669f1 + commit: "0a1facfba9a1870c212e01d4a7d64a1d3d4669f1", tag: "1.21.6", digest: testDigest, wantUpdated: true, - wantImages: map[string]string{"app": "nginx:1.21.6@" + testDigest}, + wantImage: "nginx:1.21.6@" + testDigest, }, { - name: "replace existing digest", - images: []string{"nginx:1.21.6@" + testDigest}, - 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}, - tag: "1.21.6", - digest: testDigest, + name: "already pinned", + // https://git.coopcloud.tech/sixsmith/abra-test-recipe/commit/0a1facfba9a1870c212e01d4a7d64a1d3d4669f1 + commit: "0a1facfba9a1870c212e01d4a7d64a1d3d4669f1", + tag: "1.21.5", + digest: "sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31", wantUpdated: false, - wantImages: map[string]string{"app": "nginx:1.21.6@" + testDigest}, - }, - { - name: "other image is left alone", - images: []string{"nginx:1.21.0", "postgres:15"}, - tag: "1.21.6", - digest: testDigest, - wantUpdated: true, - wantImages: map[string]string{"app": "nginx:1.21.6@" + testDigest, "db": "postgres:15"}, + wantImage: "nginx:1.21.5@sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - r := writeTestRecipe(t, composeWithImages(test.images...), "") + r := setupTestRecipe(t, test.commit) updated, err := r.UpdateTag("nginx", test.tag, test.digest) if err != nil { @@ -136,32 +71,35 @@ func TestUpdateTagPinsDigest(t *testing.T) { if updated != test.wantUpdated { t.Errorf("got updated %v, want %v", updated, test.wantUpdated) } - - assertImages(t, r, test.wantImages) + if image := appImage(t, r); image != test.wantImage { + t.Errorf("got image %s, want %s", image, test.wantImage) + } }) } } -func TestUpdateTagRejectsInvalidDigest(t *testing.T) { - r := writeTestRecipe(t, composeWithImages("nginx:1.21.0"), "") +func TestUpdateTag(t *testing.T) { + if testing.Short() { + t.Skip("skipping e2e test in short mode") + } + + // https://git.coopcloud.tech/sixsmith/abra-test-recipe/commit/ad6f6c7cfe129062a864532abacd00c6fb01f762 + r := setupTestRecipe(t, "ad6f6c7cfe129062a864532abacd00c6fb01f762") for _, digest := range []string{ - "2bcabc23b45489fb0885d69a06ba1d648aeda973fae7bb981bafbb884165e514", + "908dc23e643a1447dbfb2e189ed268bfde6a51a5bf9a34d3dd3440a24f58ccf7", "sha256:notadigest", "sha256:0", } { - if _, err := r.UpdateTag("nginx", "1.21.6", digest); err == nil { + if _, err := r.UpdateTag("nginx", "1.22.0", digest); err == nil { t.Errorf("expected an error for digest %s", digest) } } - assertImages(t, r, map[string]string{"app": "nginx:1.21.0"}) -} - -func TestUpdateTag(t *testing.T) { - r := writeTestRecipe(t, composeWithImages("nginx:1.21.0"), "") - - updated, err := r.UpdateTag("nginx", "1.21.6", "") + if image := appImage(t, r); image != "nginx:1.21.6" { + t.Errorf("got image %s, want nginx:1.21.6", image) + } + updated, err := r.UpdateTag("nginx", "1.22.0", "") if err != nil { t.Fatal(err) } @@ -169,9 +107,11 @@ func TestUpdateTag(t *testing.T) { t.Error("expected image to be updated") } - assertImages(t, r, map[string]string{"app": "nginx:1.21.6"}) + if image := appImage(t, r); image != "nginx:1.22.0" { + t.Errorf("got image %s, want nginx:1.22.0", image) + } - updated, err = r.UpdateTag("nginx", "1.21.6", "") + updated, err = r.UpdateTag("nginx", "1.22.0", "") if err != nil { t.Fatal(err) } @@ -179,3 +119,34 @@ func TestUpdateTag(t *testing.T) { t.Error("expected no update for the same tag") } } + +// Use the test recipe at the given commit, in a temporary recipes directory. +func setupTestRecipe(t *testing.T, commit string) Recipe { + t.Helper() + d := t.TempDir() + config.RECIPES_DIR = d + + r := Get("abra-test-recipe") + if err := gitPkg.Clone(r.Dir, "https://git.coopcloud.tech/sixsmith/abra-test-recipe"); err != nil { + t.Fatal(err) + } + if _, err := r.EnsureVersion(commit); err != nil { + t.Fatal(err) + } + return r +} + +func appImage(t *testing.T, r Recipe) string { + t.Helper() + cfg, err := r.GetComposeConfig(nil) + if err != nil { + t.Fatal(err) + } + for _, s := range cfg.Services { + if s.Name == "app" { + return s.Image + } + } + t.Fatal("service app not found") + return "" +} diff --git a/pkg/recipe/git.go b/pkg/recipe/git.go index c074205c..f4e57f9c 100644 --- a/pkg/recipe/git.go +++ b/pkg/recipe/git.go @@ -164,7 +164,7 @@ func (r Recipe) EnsureVersion(version string) (bool, error) { hash, err := repo.ResolveRevision(plumbing.Revision(version)) if err != nil { - log.Fatal(i18n.G("unable to resolve '%s': %s", version, err)) + return isChaosCommit, errors.New(i18n.G("unable to resolve '%s': %s", version, err)) } opts = &git.CheckoutOptions{Hash: *hash, Create: false, Force: true} @@ -175,11 +175,11 @@ func (r Recipe) EnsureVersion(version string) (bool, error) { worktree, err := repo.Worktree() if err != nil { - return isChaosCommit, nil + return isChaosCommit, err } if err := worktree.Checkout(opts); err != nil { - return isChaosCommit, nil + return isChaosCommit, err } log.Debug(i18n.G("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), r.Dir))