ce7cb702218693d08ea6d83464d2457c9250027c

This commit is contained in:
ms2k 2026-11-12 20:46:28 +09:00
commit 34c5f8963c

View file

@ -0,0 +1,577 @@
package main
import (
"context"
"errors"
"fmt"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic/fake"
)
const testDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
func TestRetentionTagOwnership(t *testing.T) {
t.Parallel()
for _, test := range []struct {
tag string
valid bool
}{
{"", true},
{"stable", true},
{"sha256-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true},
{"sha256-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", false},
{"sha256-release", true},
} {
t.Run(test.tag, func(t *testing.T) {
image := testImage()
image.Spec.Digests = []digestSpec{{Digest: testDigest, Tag: test.tag}}
err := validateImage(&image)
if (err == nil) != test.valid {
t.Fatalf("RetentionTags.AliasAllowed: valid=%t err=%v", test.valid, err)
}
})
}
}
type fakeRegistry struct {
scanPhase string
present map[string]bool
stored map[string]bool
}
func (f *fakeRegistry) scan(context.Context, string, string, string) (string, error) {
return f.scanPhase, nil
}
func (f *fakeRegistry) exists(_ context.Context, repository, digest string) (bool, error) {
return f.present[repository+"@"+digest], nil
}
func (f *fakeRegistry) physicallyPresent(_ context.Context, repository, digest string) (bool, error) {
if f.stored != nil {
return f.stored[repository+"@"+digest], nil
}
return f.present[repository+"@"+digest], nil
}
func TestCleanupObservedMeansGone(t *testing.T) {
t.Parallel()
for _, repository := range []string{"candidates/pause", "mirror/pause"} {
t.Run(repository, func(t *testing.T) {
registry := &fakeRegistry{stored: map[string]bool{repository + "@" + testDigest: true}}
instance := testController(newClient(), registry)
image := testImage()
image.Spec.Digests = nil
image.Status.Digests = []digestStatus{{Digest: testDigest, Cleanup: &attemptStatus{WorkflowName: "cleanup", Phase: "Succeeded"}}}
reconcileDesired(context.Background(), t, instance, &image)
if len(image.Status.Digests) != 1 {
t.Fatal("CleanupObservedMeansGone: physical manifest was forgotten")
}
delete(registry.stored, repository+"@"+testDigest)
reconcileDesired(context.Background(), t, instance, &image)
if len(image.Status.Digests) != 0 {
t.Fatal("physically absent digest was retained")
}
})
}
}
func TestCopyRetryRearmsCleanup(t *testing.T) {
t.Parallel()
for _, stage := range []string{prepareStage, promotionStage} {
for _, readd := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/readd=%t", stage, readd), func(t *testing.T) {
ctx := context.Background()
client := newClient()
registry := &fakeRegistry{scanPhase: scanAccepted, stored: map[string]bool{}}
instance := testController(client, registry)
image := testImage()
image.Spec.Digests = nil
image.Status.Digests = []digestStatus{{
Digest: testDigest, Tag: "stable", Scan: scanAccepted,
Prepare: &attemptStatus{WorkflowName: "prepare", Phase: "Succeeded"},
Promotion: &attemptStatus{WorkflowName: "promotion", Phase: "Succeeded"},
}}
status := &image.Status.Digests[0]
status.Cleanup = instance.newAttempt(&image, status, cleanupStage, "")
status.Cleanup.Phase = "Succeeded"
oldCleanup := status.Cleanup.WorkflowName
if _, err := client.Resource(workflowGVR).Namespace("mirrored-image").Create(ctx, workflowObject(oldCleanup, "Succeeded"), metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
image.Metadata.Annotations = map[string]string{retryAnnotation: stage}
for pass := 0; pass < 2; pass++ {
handled, err := instance.reconcileRetry(ctx, &image)
if err != nil || !handled {
t.Fatalf("retry pass %d: handled=%t err=%v", pass, handled, err)
}
if pass == 0 {
if image.Status.LastHandledRetry != "" {
t.Fatal("RetryAcknowledgedAfterCreation")
}
if readd {
image.Spec.Digests = []digestSpec{{Digest: testDigest, Tag: "edited"}}
}
}
}
if image.Status.LastHandledRetry != stage {
t.Fatal("retry was not acknowledged")
}
copy := status.Prepare
if stage == promotionStage {
copy = status.Promotion
}
setPhase(ctx, t, client, copy.WorkflowName, "Succeeded")
registry.stored["candidates/pause@"+testDigest] = true
reconcileDesired(ctx, t, instance, &image)
reconcileDesired(ctx, t, instance, &image)
if status.Cleanup == nil || status.Cleanup.WorkflowName == oldCleanup || terminalPhase(status.Cleanup.Phase) {
t.Fatal("copy retry reused completed cleanup")
}
fresh := status.Cleanup.WorkflowName
if _, err := client.Resource(workflowGVR).Namespace("mirrored-image").Get(ctx, fresh, metav1.GetOptions{}); err != nil {
t.Fatal(err)
}
if status.Tag != "stable" {
t.Fatal("RetryPreservesTagIdentity")
}
setPhase(ctx, t, client, fresh, "Succeeded")
reconcileDesired(ctx, t, instance, &image)
reconcileDesired(ctx, t, instance, &image)
if len(image.Status.Digests) != 1 {
t.Fatal("CleanupObservedMeansGone")
}
delete(registry.stored, "candidates/pause@"+testDigest)
reconcileDesired(ctx, t, instance, &image)
if readd {
if status.Cleanup != nil || status.Prepare != nil || status.Tag != "edited" {
t.Fatal("re-addition did not wait for fresh cleanup")
}
} else if len(image.Status.Digests) != 0 {
t.Fatal("retirement did not complete")
}
})
}
}
}
func TestDigestLifecycle(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := newClient()
registry := &fakeRegistry{scanPhase: scanAccepted, present: map[string]bool{}}
instance := testController(client, registry)
image := testImage()
reconcileDesired(ctx, t, instance, &image)
status := &image.Status.Digests[0]
if status.Prepare == nil || status.Prepare.WorkflowName == "" {
t.Fatal("prepare workflow was not recorded")
}
reconcileDesired(ctx, t, instance, &image)
setPhase(ctx, t, client, status.Prepare.WorkflowName, "Succeeded")
reconcileDesired(ctx, t, instance, &image)
reconcileDesired(ctx, t, instance, &image)
if image.Status.Digests[0].Scan != scanAccepted {
t.Fatal("candidate scan was not accepted")
}
reconcileDesired(ctx, t, instance, &image)
status = &image.Status.Digests[0]
if status.Promotion == nil || status.Promotion.WorkflowName == "" {
t.Fatal("promotion workflow was not recorded")
}
reconcileDesired(ctx, t, instance, &image)
setPhase(ctx, t, client, status.Promotion.WorkflowName, "Succeeded")
registry.present["mirror/pause@"+testDigest] = true
reconcileDesired(ctx, t, instance, &image)
image.Metadata.Generation++
image.Spec.Digests = nil
reconcileDesired(ctx, t, instance, &image)
status = &image.Status.Digests[0]
if status.Cleanup == nil || status.Cleanup.WorkflowName == "" {
t.Fatal("cleanup workflow was not recorded")
}
reconcileDesired(ctx, t, instance, &image)
setPhase(ctx, t, client, status.Cleanup.WorkflowName, "Succeeded")
delete(registry.present, "mirror/pause@"+testDigest)
reconcileDesired(ctx, t, instance, &image)
reconcileDesired(ctx, t, instance, &image)
if len(image.Status.Digests) != 0 {
t.Fatalf("cleanup retained %d digest statuses", len(image.Status.Digests))
}
}
func TestAttemptIsRecordedFirst(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := newClient()
instance := testController(client, &fakeRegistry{present: map[string]bool{}})
image := testImage()
reconcileDesired(ctx, t, instance, &image)
if image.Status.Digests[0].Prepare == nil {
t.Fatal("prepare attempt was not recorded")
}
workflows, err := client.Resource(workflowGVR).Namespace("mirrored-image").List(ctx, metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if len(workflows.Items) != 0 {
t.Fatal("workflow existed before its status record")
}
reconcileDesired(ctx, t, instance, &image)
workflows, err = client.Resource(workflowGVR).Namespace("mirrored-image").List(ctx, metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if len(workflows.Items) != 1 || workflows.Items[0].GetName() != image.Status.Digests[0].Prepare.WorkflowName {
t.Fatal("recorded prepare workflow was not created")
}
}
func TestCleanupWaitsForAbsence(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := newClient()
registry := &fakeRegistry{present: map[string]bool{"candidates/pause@" + testDigest: true}}
instance := testController(client, registry)
image := testImage()
image.Spec.Digests = nil
image.Status.Digests = []digestStatus{{
Digest: testDigest,
Cleanup: &attemptStatus{WorkflowName: "cleanup", Phase: "Succeeded"},
}}
reconcileDesired(ctx, t, instance, &image)
if len(image.Status.Digests) != 1 || image.Status.Message != "cleanup: awaiting repository GC" {
t.Fatal("cleanup completed while the candidate remained present")
}
delete(registry.present, "candidates/pause@"+testDigest)
reconcileDesired(ctx, t, instance, &image)
if len(image.Status.Digests) != 0 {
t.Fatal("cleanup state remained after both repositories were absent")
}
}
func TestReaddWaitsForCleanup(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := newClient()
registry := &fakeRegistry{scanPhase: scanAccepted, present: map[string]bool{}}
instance := testController(client, registry)
image := testImage()
oldPrepare := attemptName(&image, testDigest, prepareStage, image.Metadata.Generation, "")
image.Metadata.Generation = 3
image.Status.Digests = []digestStatus{{
Digest: testDigest,
Tag: "stable",
Prepare: &attemptStatus{WorkflowName: oldPrepare, Phase: "Succeeded", Generation: 1},
Scan: scanAccepted,
Promotion: &attemptStatus{WorkflowName: "old-promotion", Phase: "Succeeded"},
Cleanup: &attemptStatus{WorkflowName: "old-cleanup", Phase: "Running"},
}}
workflow := workflowObject("old-cleanup", "Running")
if _, err := client.Resource(workflowGVR).Namespace("mirrored-image").Create(ctx, workflow, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
reconcileDesired(ctx, t, instance, &image)
if image.Status.Digests[0].Prepare.WorkflowName != oldPrepare {
t.Fatal("prepare restarted while cleanup was active")
}
setPhase(ctx, t, client, "old-cleanup", "Succeeded")
reconcileDesired(ctx, t, instance, &image)
reconcileDesired(ctx, t, instance, &image)
status := image.Status.Digests[0]
if status.Cleanup != nil || status.Prepare != nil {
t.Fatal("completed cleanup did not reset digest state")
}
reconcileDesired(ctx, t, instance, &image)
if image.Status.Digests[0].Prepare == nil {
t.Fatal("prepare did not restart after cleanup")
}
if image.Status.Digests[0].Prepare.WorkflowName == oldPrepare {
t.Fatal("prepare reused the workflow from an earlier generation")
}
}
func TestRetryCreatesSuccessor(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := newClient()
registry := &fakeRegistry{scanPhase: scanAccepted, present: map[string]bool{}}
instance := testController(client, registry)
image := testImage()
image.Status.Digests = []digestStatus{{
Digest: testDigest,
Tag: "stable",
Prepare: &attemptStatus{WorkflowName: "failed-prepare", Phase: "Failed"},
}}
image.Metadata.Annotations = map[string]string{retryAnnotation: "failed-prepare"}
retried, err := instance.reconcileRetry(ctx, &image)
if err != nil {
t.Fatal(err)
}
if !retried {
t.Fatal("terminal retry was not handled")
}
attempt := image.Status.Digests[0].Prepare
if attempt.RetryOf != "failed-prepare" || attempt.WorkflowName == "failed-prepare" {
t.Fatalf("invalid successor: %#v", attempt)
}
if image.Status.LastHandledRetry != "" {
t.Fatal("retry was acknowledged before the successor existed")
}
retried, err = instance.reconcileRetry(ctx, &image)
if err != nil || !retried {
t.Fatalf("create retry successor: retried=%v error=%v", retried, err)
}
if image.Status.LastHandledRetry != "failed-prepare" {
t.Fatal("retry was not acknowledged after the successor existed")
}
setPhase(ctx, t, client, attempt.WorkflowName, "Succeeded")
reconcileDesired(ctx, t, instance, &image)
reconcileDesired(ctx, t, instance, &image)
reconcileDesired(ctx, t, instance, &image)
promotion := image.Status.Digests[0].Promotion
if promotion == nil || promotion.WorkflowName == "" {
t.Fatal("promotion was not created after the retry")
}
oldName := attemptName(&image, testDigest, promotionStage, image.Metadata.Generation)
if promotion.WorkflowName == oldName {
t.Fatal("promotion reused the predecessor prepare lineage")
}
}
func TestRetryNameIgnoresGeneration(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := newClient()
instance := testController(client, &fakeRegistry{present: map[string]bool{}})
image := testImage()
image.Status.Digests = []digestStatus{{
Digest: testDigest,
Prepare: &attemptStatus{WorkflowName: "failed-prepare", Phase: "Failed"},
}}
image.Metadata.Annotations = map[string]string{retryAnnotation: "failed-prepare"}
if _, err := instance.reconcileRetry(ctx, &image); err != nil {
t.Fatal(err)
}
first := image.Status.Digests[0].Prepare.WorkflowName
image.Metadata.Generation++
image.Status.Digests[0].Prepare = &attemptStatus{WorkflowName: "failed-prepare", Phase: "Failed"}
image.Status.LastHandledRetry = ""
if _, err := instance.reconcileRetry(ctx, &image); err != nil {
t.Fatal(err)
}
if second := image.Status.Digests[0].Prepare.WorkflowName; second != first {
t.Fatalf("retry generation changed successor from %q to %q", first, second)
}
}
func TestRunStopsWhenIdle(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
client := newClient()
instance := testController(client, &fakeRegistry{present: map[string]bool{}})
done := make(chan error, 1)
go func() {
done <- instance.run(ctx)
}()
time.Sleep(20 * time.Millisecond)
cancel()
select {
case err := <-done:
if !errors.Is(err, context.Canceled) {
t.Fatalf("run returned %v", err)
}
case <-time.After(time.Second):
t.Fatal("run did not stop")
}
}
func TestRejectedCandidateIsNotPromoted(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := newClient()
registry := &fakeRegistry{scanPhase: scanRejected, present: map[string]bool{}}
instance := testController(client, registry)
image := testImage()
image.Status.Digests = []digestStatus{{
Digest: testDigest,
Tag: "stable",
Prepare: &attemptStatus{WorkflowName: "prepare", Phase: "Succeeded"},
}}
reconcileDesired(ctx, t, instance, &image)
status := image.Status.Digests[0]
if status.Scan != scanRejected || status.Promotion != nil {
t.Fatal("rejected candidate was promoted")
}
if image.Status.Message != "scan: Rejected" {
t.Fatalf("unexpected status message %q", image.Status.Message)
}
}
func TestTaglessDeclarationIsValid(t *testing.T) {
t.Parallel()
image := testImage()
image.Spec.Digests[0].Tag = ""
if err := validateImage(&image); err != nil {
t.Fatal(err)
}
}
func TestTagEditPreservesState(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := newClient()
registry := &fakeRegistry{present: map[string]bool{"mirror/pause@" + testDigest: true}}
instance := testController(client, registry)
image := testImage()
image.Spec.Digests[0].Tag = "new"
image.Status.Digests = []digestStatus{{
Digest: testDigest,
Tag: "stable",
Prepare: &attemptStatus{WorkflowName: "prepare", Phase: "Succeeded"},
Scan: scanAccepted,
Promotion: &attemptStatus{WorkflowName: "promotion", Phase: "Succeeded"},
}}
reconcileDesired(ctx, t, instance, &image)
status := image.Status.Digests[0]
if status.Tag != "stable" || status.Cleanup != nil {
t.Fatalf("tag edit changed managed state: %#v", status)
}
workflows, err := client.Resource(workflowGVR).Namespace("mirrored-image").List(ctx, metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if len(workflows.Items) != 0 {
t.Fatal("tag edit created a workflow")
}
}
func TestFailureMessage(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := newClient()
registry := &fakeRegistry{present: map[string]bool{}}
instance := testController(client, registry)
image := testImage()
image.Status.Digests = []digestStatus{{
Digest: testDigest,
Tag: "stable",
Prepare: &attemptStatus{WorkflowName: "failed-prepare", Phase: "Failed"},
}}
reconcileDesired(ctx, t, instance, &image)
if image.Status.Message != "prepare: Failed" {
t.Fatalf("unexpected status message %q", image.Status.Message)
}
}
func TestPromotionRescan(t *testing.T) {
for _, phase := range []string{scanPending, scanRejected, scanAccepted} {
t.Run(phase, func(t *testing.T) {
ctx := context.Background()
client := newClient()
registry := &fakeRegistry{scanPhase: phase}
instance := testController(client, registry)
image := testImage()
image.Status.Digests = []digestStatus{{
Digest: testDigest, Scan: scanAccepted,
Prepare: &attemptStatus{WorkflowName: "prepare", Phase: "Succeeded"},
Promotion: &attemptStatus{WorkflowName: "promotion", Phase: "Pending"},
}}
err := instance.reconcileDesired(ctx, &image)
if (err == nil) != (phase == scanAccepted) {
t.Fatalf("scan %s: %v", phase, err)
}
workflows, err := client.Resource(workflowGVR).Namespace(image.Metadata.Namespace).List(ctx, metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if (len(workflows.Items) == 1) != (phase == scanAccepted) {
t.Fatalf("scan %s created %d workflows", phase, len(workflows.Items))
}
if phase == scanAccepted {
registry.scanPhase = scanPending
if err := instance.reconcileDesired(ctx, &image); err != nil {
t.Fatalf("existing workflow observation requires a scan: %v", err)
}
}
})
}
}
func testController(client *fake.FakeDynamicClient, registry registryReader) *controller {
return &controller{client: client, registry: registry, config: controllerConfig{
prepareTemplate: "image-prepare",
promotionTemplate: "image-promote",
cleanupTemplate: "image-cleanup",
}}
}
func newClient(objects ...runtime.Object) *fake.FakeDynamicClient {
return fake.NewSimpleDynamicClientWithCustomListKinds(
runtime.NewScheme(),
map[schema.GroupVersionResource]string{
imageGVR: "MirroredImageList",
workflowGVR: "WorkflowList",
},
objects...,
)
}
func testImage() mirroredImage {
return mirroredImage{
Metadata: objectMetadata{Name: "pause", Namespace: "mirrored-image", UID: "image-uid", Generation: 1},
Spec: mirroredImageSpec{
Upstream: "registry.k8s.io/pause",
Policy: scanPolicy{RejectAtOrAbove: "High"},
Digests: []digestSpec{{Digest: testDigest, Tag: "stable"}},
},
}
}
func reconcileDesired(ctx context.Context, t *testing.T, instance *controller, image *mirroredImage) {
t.Helper()
if err := instance.reconcileDesired(ctx, image); err != nil {
t.Fatal(err)
}
}
func setPhase(ctx context.Context, t *testing.T, client *fake.FakeDynamicClient, name, phase string) {
t.Helper()
resource := client.Resource(workflowGVR).Namespace("mirrored-image")
object, err := resource.Get(ctx, name, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
if err := unstructured.SetNestedField(object.Object, phase, "status", "phase"); err != nil {
t.Fatal(err)
}
if _, err := resource.Update(ctx, object, metav1.UpdateOptions{}); err != nil {
t.Fatal(err)
}
}
func workflowObject(name, phase string) *unstructured.Unstructured {
return &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "argoproj.io/v1alpha1",
"kind": "Workflow",
"metadata": map[string]any{"name": name, "namespace": "mirrored-image"},
"status": map[string]any{"phase": phase},
}}
}