This commit is contained in:
2026-10-03 02:03:05 -04:00
parent 412f79c0ec
commit 6374d35bbc
8 changed files with 243 additions and 325 deletions
+8 -2
View File
@@ -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
+7 -6
View File
@@ -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:...)"),
)
}
+113 -114
View File
@@ -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()
}
+25 -86
View File
@@ -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)
}
}
+1
View File
@@ -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
+1
View File
@@ -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))
}
+85 -114
View File
@@ -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 ""
}
+3 -3
View File
@@ -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))