ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
1 changed files with 145336 additions and 0 deletions
367
services/project-delivery/controller/atomic_test.go
Normal file
367
services/project-delivery/controller/atomic_test.go
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
type admitClient struct {
|
||||
dynamic.Interface
|
||||
before func(*unstructured.Unstructured)
|
||||
afterList func()
|
||||
}
|
||||
|
||||
type admitScope struct {
|
||||
dynamic.NamespaceableResourceInterface
|
||||
before func(*unstructured.Unstructured)
|
||||
afterList func()
|
||||
}
|
||||
|
||||
type admitResource struct {
|
||||
dynamic.ResourceInterface
|
||||
before func(*unstructured.Unstructured)
|
||||
afterList func()
|
||||
}
|
||||
|
||||
func (c admitClient) Resource(gvr schema.GroupVersionResource) dynamic.NamespaceableResourceInterface {
|
||||
resource := c.Interface.Resource(gvr)
|
||||
if gvr == workflowGVR {
|
||||
return admitScope{NamespaceableResourceInterface: resource, before: c.before, afterList: c.afterList}
|
||||
}
|
||||
return resource
|
||||
}
|
||||
|
||||
func (r admitScope) Namespace(namespace string) dynamic.ResourceInterface {
|
||||
return admitResource{ResourceInterface: r.NamespaceableResourceInterface.Namespace(namespace), before: r.before, afterList: r.afterList}
|
||||
}
|
||||
|
||||
func (r admitResource) Create(ctx context.Context, object *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
if r.before != nil {
|
||||
r.before(object)
|
||||
}
|
||||
return r.ResourceInterface.Create(ctx, object, options, subresources...)
|
||||
}
|
||||
|
||||
func TestLateAdmission(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
instance, template := bundleFixture(t)
|
||||
peer := *instance
|
||||
previous, err := peer.readyTemplate(ctx, "delivery", template.GetName())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
old, err := peer.client.Resource(templateGVR).Namespace("delivery").Get(ctx, previous, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
published := ""
|
||||
instance.client = admitClient{Interface: peer.client, before: func(_ *unstructured.Unstructured) {
|
||||
changeImage(t, &peer, template.GetName(), "example.invalid/builder:v2")
|
||||
published, err = peer.readyTemplate(ctx, "delivery", template.GetName())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}}
|
||||
delivery, err := decodeProjectDelivery(continuousDelivery())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := instance.ensureWorkflow(ctx, &delivery, "late", template.GetName(), "build", nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
captured := boundTemplate(t, instance, "late")
|
||||
if !reflect.DeepEqual(old.Object["spec"], captured.Object["spec"]) {
|
||||
t.Fatal("captured content changed")
|
||||
}
|
||||
if captured.GetName() == published {
|
||||
t.Fatal("the schedule did not overlap two published revisions")
|
||||
}
|
||||
}
|
||||
|
||||
func (r admitResource) List(ctx context.Context, options metav1.ListOptions) (*unstructured.UnstructuredList, error) {
|
||||
items, err := r.ResourceInterface.List(ctx, options)
|
||||
if err == nil && r.afterList != nil {
|
||||
r.afterList()
|
||||
}
|
||||
return items, err
|
||||
}
|
||||
|
||||
func TestPublishOverlap(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
instance, template := bundleFixture(t)
|
||||
peer := *instance
|
||||
base := peer.client
|
||||
previous, err := peer.readyTemplate(ctx, "delivery", template.GetName())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
published := ""
|
||||
instance.client = admitClient{Interface: base, before: func(workflow *unstructured.Unstructured) {
|
||||
changeImage(t, &peer, template.GetName(), "example.invalid/builder:v2")
|
||||
peer.client = admitClient{Interface: base, afterList: func() {
|
||||
if _, err := base.Resource(workflowGVR).Namespace("delivery").Create(ctx, workflow.DeepCopy(), metav1.CreateOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}}
|
||||
published, err = peer.readyTemplate(ctx, "delivery", template.GetName())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}}
|
||||
delivery, err := decodeProjectDelivery(continuousDelivery())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := instance.ensureWorkflow(ctx, &delivery, "late", template.GetName(), "build", nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
captured := boundTemplate(t, instance, "late")
|
||||
if captured.GetName() != previous {
|
||||
t.Fatal("execution lost its captured revision")
|
||||
}
|
||||
if published == previous {
|
||||
t.Fatal("the schedule did not publish during an in-flight admission")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuietSameBundle(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
instance, template := bundleFixture(t)
|
||||
now := instance.now()
|
||||
instance.now = func() time.Time { return now }
|
||||
instance.config.templateQuiet = time.Minute
|
||||
if err := unstructured.SetNestedField(template.Object, "nodeSelector: {}", "spec", "podSpecPatch"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := instance.client.Resource(templateGVR).Namespace("delivery").Update(ctx, template, metav1.UpdateOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := instance.readyTemplate(ctx, "delivery", template.GetName()); !errors.Is(err, templatePending) {
|
||||
t.Fatalf("initial observation = %v", err)
|
||||
}
|
||||
now = now.Add(time.Minute)
|
||||
previous, err := instance.readyTemplate(ctx, "delivery", template.GetName())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
template, err = instance.client.Resource(templateGVR).Namespace("delivery").Get(ctx, template.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
template.SetGeneration(template.GetGeneration() + 1)
|
||||
if err := unstructured.SetNestedField(template.Object, "{nodeSelector: {}}\n", "spec", "podSpecPatch"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := instance.client.Resource(templateGVR).Namespace("delivery").Update(ctx, template, metav1.UpdateOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bundle, err := instance.captureBundle(ctx, template)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bundle.name != previous {
|
||||
t.Fatal("equivalent patches changed captured revision")
|
||||
}
|
||||
published, err := instance.readyTemplate(ctx, "delivery", template.GetName())
|
||||
if err != nil || published != previous {
|
||||
t.Fatalf("unchanged revision restarted quiet interval: published=%q error=%v", published, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPointCheckpoint(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Date(2026, 10, 2, 0, 0, 0, 0, time.UTC)
|
||||
revisions := []string{"0123456789abcdef0123456789abcdef01234567", "1123456789abcdef0123456789abcdef01234567"}
|
||||
object := pointDelivery(revisions...)
|
||||
instance := testController(t, object, &mutableRefs{}, &now)
|
||||
delivery, err := decodeProjectDelivery(object)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, revision := range revisions {
|
||||
attempt := &attemptStatus{Revision: revision}
|
||||
if err := instance.ensureWorkflow(ctx, &delivery, attemptName(&delivery, attempt), delivery.Spec.WorkflowTemplateRef.Name, "build", map[string]string{"source-ref": "", "source-revision": revision}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := instance.reconcileKey(ctx, "delivery", "point"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, err := decodeProjectDelivery(getDelivery(t, instance, "point"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(stored.Status.Points) != 1 {
|
||||
t.Fatalf("Point checkpoint count = %d", len(stored.Status.Points))
|
||||
}
|
||||
first := stored.Status.Points[0].WorkflowName
|
||||
for _, revision := range revisions {
|
||||
name := attemptName(&delivery, &attemptStatus{Revision: revision})
|
||||
workflow, err := instance.client.Resource(workflowGVR).Namespace("delivery").Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
held := slices.Contains(workflow.GetFinalizers(), recordKey)
|
||||
if held != (name != first) {
|
||||
t.Fatalf("checkpoint protection for %s = %t", name, held)
|
||||
}
|
||||
}
|
||||
setNamedWorkflowStatus(t, instance, first, "Succeeded", nil)
|
||||
for range 2 {
|
||||
if err := instance.reconcileKey(ctx, "delivery", "point"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
stored, err = decodeProjectDelivery(getDelivery(t, instance, "point"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(stored.Status.Points) != 2 {
|
||||
t.Fatalf("recovered checkpoint count = %d", len(stored.Status.Points))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryOldMode(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Date(2026, 10, 2, 0, 0, 0, 0, time.UTC)
|
||||
revision := "0123456789abcdef0123456789abcdef01234567"
|
||||
instance := testController(t, continuousDelivery(), &mutableRefs{values: map[string]string{"refs/heads/main": revision}}, &now)
|
||||
for range 3 {
|
||||
instance.reconcileAll(ctx)
|
||||
now = now.Add(time.Minute)
|
||||
}
|
||||
object := getDelivery(t, instance, "continuous")
|
||||
delivery, err := decodeProjectDelivery(object)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := delivery.Status.Continuous.Attempt.WorkflowName
|
||||
setNamedWorkflowStatus(t, instance, first, "Failed", nil)
|
||||
instance.reconcileAll(ctx)
|
||||
object = getDelivery(t, instance, "continuous")
|
||||
unstructured.RemoveNestedField(object.Object, "spec", "source", "continuous")
|
||||
if err := unstructured.SetNestedMap(object.Object, map[string]any{"revisions": []any{revision}}, "spec", "source", "point"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
annotations := object.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = make(map[string]string)
|
||||
}
|
||||
annotations[retryAnnotation] = first
|
||||
object.SetAnnotations(annotations)
|
||||
if _, err := instance.client.Resource(projectDeliveryGVR).Namespace("delivery").Update(ctx, object, metav1.UpdateOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := instance.reconcileKey(ctx, "delivery", "continuous"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, err := decodeProjectDelivery(getDelivery(t, instance, "continuous"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
retry := stored.Status.Continuous.Attempt
|
||||
if stored.Spec.Source.Point == nil || retry.RetryOf != first || retry.Ref != "refs/heads/main" || retry.WorkflowName == first {
|
||||
t.Fatalf("old-mode retry did not preserve captured selection: %+v", retry)
|
||||
}
|
||||
t.Log("retry accepted in Continuous mode while the declared source mode is Point")
|
||||
}
|
||||
|
||||
func TestTreeOverlap(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
instance, _ := bundleFixture(t)
|
||||
peer := *instance
|
||||
delivery, err := decodeProjectDelivery(continuousDelivery())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := &attemptStatus{Ref: "refs/heads/main", Revision: "0123456789abcdef0123456789abcdef01234567", SourceIdentity: "sha256:tree"}
|
||||
second := &attemptStatus{Ref: first.Ref, Revision: "1123456789abcdef0123456789abcdef01234567", SourceIdentity: first.SourceIdentity}
|
||||
instance.client = admitClient{Interface: peer.client, before: func(_ *unstructured.Unstructured) {
|
||||
if err := peer.advanceAttempt(ctx, &delivery, second); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}}
|
||||
if err := instance.advanceAttempt(ctx, &delivery, first); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count := countWorkflows(t, instance, "build"); count != 1 {
|
||||
t.Fatalf("one source identity produced %d accepted workflows from overlapping SHA observations", count)
|
||||
}
|
||||
if first.WorkflowName != second.WorkflowName {
|
||||
t.Fatal("overlapping observations did not converge on one captured execution")
|
||||
}
|
||||
if first.Revision != first.Input["source-revision"] {
|
||||
t.Fatalf("accepted Workflow SHA %q differs from recorded attempt SHA %q", first.Input["source-revision"], first.Revision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefOverlap(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
instance, _ := bundleFixture(t)
|
||||
peer := *instance
|
||||
delivery, err := decodeProjectDelivery(continuousDelivery())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := &attemptStatus{Ref: "refs/heads/main", Revision: "0123456789abcdef0123456789abcdef01234567", SourceIdentity: "0123456789abcdef0123456789abcdef01234567"}
|
||||
second := &attemptStatus{Ref: "refs/heads/release", Revision: first.Revision, SourceIdentity: first.SourceIdentity}
|
||||
instance.client = admitClient{Interface: peer.client, before: func(_ *unstructured.Unstructured) {
|
||||
if err := peer.advanceAttempt(ctx, &delivery, second); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}}
|
||||
if err := instance.advanceAttempt(ctx, &delivery, first); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count := countWorkflows(t, instance, "build"); count != 1 {
|
||||
t.Fatalf("one trigger identity produced %d workflows after a ref change", count)
|
||||
}
|
||||
if first.Ref != first.Input["source-ref"] || first.WorkflowName != second.WorkflowName {
|
||||
t.Fatal("captured ref did not follow the accepted execution")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetachedPublish(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
instance, template := bundleFixture(t)
|
||||
delivery, err := decodeProjectDelivery(continuousDelivery())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := instance.ensureWorkflow(ctx, &delivery, "old", template.GetName(), "build", nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
changeImage(t, instance, template.GetName(), "example.invalid/builder:v2")
|
||||
if _, err := instance.readyTemplate(ctx, "delivery", template.GetName()); !errors.Is(err, templatePending) {
|
||||
t.Fatalf("pending update = %v", err)
|
||||
}
|
||||
object := getDelivery(t, instance, "continuous")
|
||||
if err := unstructured.SetNestedField(object.Object, "other-delivery", "spec", "workflowTemplateRef", "name"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := instance.client.Resource(projectDeliveryGVR).Namespace("delivery").Update(ctx, object, metav1.UpdateOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
setNamedWorkflowStatus(t, instance, "old", "Succeeded", nil)
|
||||
for range 2 {
|
||||
if err := instance.reconcileTemplates(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
stored, err := instance.client.Resource(templateGVR).Namespace("delivery").Get(ctx, template.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored.GetAnnotations()[publishedKey] != stored.GetAnnotations()[candidateKey] {
|
||||
t.Fatal("unreferenced template retained its update but never published it after drain")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue