forked from toolshed/abra
update
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user