quorate/services/project-delivery/controller/qualification_test.go

326 lines
13 KiB
Go
Raw Permalink Normal View History

package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
dynamicfake "k8s.io/client-go/dynamic/fake"
k8stesting "k8s.io/client-go/testing"
)
func TestQualManual(t *testing.T) {
c, now := qualFixture(t, "")
build := publishQual(t, c, "point", strings.Repeat("1", 40))
stepQual(t, c)
if len(readQual(t, c, "point").Attempts) != 0 {
t.Fatal("omitted mode started automatic qualification")
}
requestQual(t, c, "point", build+"/first")
stepQual(t, c)
first := readQual(t, c, "point").Attempts[0]
if first.Phase != "Reserved" || countWorkflows(t, c, "qualification") != 0 {
t.Fatal("reservation was not persisted before submission")
}
stepQual(t, c)
markQual(t, c, "delivery", first.WorkflowName, "Running", now, nil)
stepQual(t, c)
for _, id := range []string{"second", "third"} {
requestQual(t, c, "point", build+"/"+id)
stepQual(t, c)
}
q := readQual(t, c, "point")
if len(q.Attempts) != 3 || q.Attempts[1].Phase != "Pending" || q.Attempts[2].Phase != "Pending" {
t.Fatalf("accepted requests = %#v", q.Attempts)
}
requestQual(t, c, "point", build+"/first")
stepQual(t, c)
if len(readQual(t, c, "point").Attempts) != 3 || countWorkflows(t, c, "qualification") != 1 {
t.Fatal("reapplying a consumed request created another run")
}
markQual(t, c, "delivery", first.WorkflowName, "Succeeded", now, map[string]string{"qualification-result": `{"report":"synthetic://reports/first"}`})
stepQual(t, c)
stepQual(t, c)
stepQual(t, c)
if countWorkflows(t, c, "qualification") != 2 {
t.Fatal("pending manual request did not advance after completion")
}
q = readQual(t, c, "point")
if string(q.Attempts[0].Result) != `{"report":"synthetic://reports/first"}` {
t.Fatalf("qualification result = %s", q.Attempts[0].Result)
}
}
func TestQualInterval(t *testing.T) {
c, now := qualFixture(t, "OnBuild")
firstBuild := publishQual(t, c, "point", strings.Repeat("1", 40))
stepQual(t, c)
stepQual(t, c)
first := readQual(t, c, "point").Attempts[0]
markQual(t, c, "delivery", first.WorkflowName, "Running", now, nil)
stepQual(t, c)
publishQual(t, c, "point", strings.Repeat("2", 40))
thirdBuild := publishQual(t, c, "point", strings.Repeat("3", 40))
markQual(t, c, "delivery", first.WorkflowName, "Failed", now, nil)
stepQual(t, c)
*now = now.Add(167 * time.Hour)
stepQual(t, c)
if len(readQual(t, c, "point").Attempts) != 1 {
t.Fatal("automatic qualification ignored the interval after failure")
}
*now = now.Add(time.Hour)
stepQual(t, c)
q := readQual(t, c, "point")
if len(q.Attempts) != 2 || q.Builds[q.Attempts[1].Build-1].WorkflowName != thirdBuild {
t.Fatalf("expiry did not select latest build: %#v", q.Attempts)
}
stepQual(t, c)
second := q.Attempts[1]
markQual(t, c, "delivery", second.WorkflowName, "Succeeded", now, nil)
stepQual(t, c)
requestQual(t, c, "point", firstBuild+"/old-build")
stepQual(t, c)
stepQual(t, c)
q = readQual(t, c, "point")
manual := q.Attempts[2]
if manual.Kind != "Manual" || manual.Build != 1 {
t.Fatalf("manual selection = %#v", manual)
}
*now = now.Add(time.Hour)
markQual(t, c, "delivery", manual.WorkflowName, "Succeeded", now, nil)
stepQual(t, c)
q = readQual(t, c, "point")
if q.Covered != 3 || !q.LastStart.Equal(*now) {
t.Fatalf("manual start accounting = %#v", q)
}
*now = now.Add(168 * time.Hour)
stepQual(t, c)
if len(readQual(t, c, "point").Attempts) != 3 {
t.Fatal("expiry without a new build repeated qualification")
}
}
func TestQualSnapshot(t *testing.T) {
c, _ := qualFixture(t, "OnBuild")
build := publishQual(t, c, "point", strings.Repeat("1", 40))
original := readQual(t, c, "point").Builds[0].Input
object := getDelivery(t, c, "point")
spec := object.Object["spec"].(map[string]any)
spec["source"].(map[string]any)["repository"] = "https://example.test/other/repo"
spec["targets"] = []any{map[string]any{"kind": "Other", "repository": "changed"}}
if _, err := c.client.Resource(projectDeliveryGVR).Namespace("delivery").Update(context.Background(), object, metav1.UpdateOptions{}); err != nil {
t.Fatal(err)
}
if err := c.client.Resource(workflowGVR).Namespace("delivery").Delete(context.Background(), build, metav1.DeleteOptions{}); err != nil {
t.Fatal(err)
}
stepQual(t, c)
object = getDelivery(t, c, "point")
object.Object["spec"].(map[string]any)["qualification"].(map[string]any)["workflowTemplateRef"] = map[string]any{"name": "other-template"}
if _, err := c.client.Resource(projectDeliveryGVR).Namespace("delivery").Update(context.Background(), object, metav1.UpdateOptions{}); err != nil {
t.Fatal(err)
}
stepQual(t, c)
attempt := readQual(t, c, "point").Attempts[0]
workflow, err := c.client.Resource(workflowGVR).Namespace("delivery").Get(context.Background(), attempt.WorkflowName, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
template := workflow.GetAnnotations()[templateKey]
if template != "qual-tests" {
t.Fatalf("reserved template changed to %q", template)
}
for key, value := range original {
if got := workflowParameter(t, workflow, key); got != value {
t.Fatalf("parameter %s = %q, want %q", key, got, value)
}
}
if workflowHasParameter(t, workflow, "image-digest") {
t.Fatal("controller interpreted the publication result")
}
}
func TestQualReservation(t *testing.T) {
c, _ := qualFixture(t, "OnBuild")
publishQual(t, c, "point", strings.Repeat("1", 40))
fail := true
c.client.(*dynamicfake.FakeDynamicClient).PrependReactor("update", "projectdeliveries", func(action k8stesting.Action) (bool, runtime.Object, error) {
if fail {
fail = false
return true, nil, errors.New("synthetic status write failure")
}
return false, nil, nil
})
if err := c.qualifyAll(context.Background()); err == nil {
t.Fatal("expected reservation write failure")
}
if countWorkflows(t, c, "qualification") != 0 || len(readQual(t, c, "point").Attempts) != 0 {
t.Fatal("unpersisted reservation created a Workflow")
}
stepQual(t, c)
restarted := *c
restarted.qualCursor = ""
conflicts := 0
c.client.(*dynamicfake.FakeDynamicClient).PrependReactor("update", "projectdeliveries", func(action k8stesting.Action) (bool, runtime.Object, error) {
object := action.(k8stesting.UpdateAction).GetObject().(*unstructured.Unstructured)
attempts, _, _ := unstructured.NestedSlice(object.Object, "status", "qualification", "attempts")
queued := len(attempts) != 0 && attempts[0].(map[string]any)["phase"] == "Queued"
if conflicts == 0 && queued {
conflicts++
return true, nil, apierrors.NewConflict(projectDeliveryGVR.GroupResource(), "point", errors.New("synthetic conflict"))
}
return false, nil, nil
})
stepQual(t, &restarted)
stepQual(t, &restarted)
if conflicts != 1 || countWorkflows(t, c, "qualification") != 1 {
t.Fatalf("reservation recovery: conflicts=%d workflows=%d", conflicts, countWorkflows(t, c, "qualification"))
}
}
func TestQualGlobalCap(t *testing.T) {
c, now := qualFixture(t, "OnBuild")
second := getDelivery(t, c, "point").DeepCopy()
second.SetNamespace("other")
second.SetUID("other-delivery")
second.SetResourceVersion("")
second.Object["spec"].(map[string]any)["qualification"].(map[string]any)["workflowTemplateRef"] = map[string]any{"name": "other-qual"}
if _, err := c.client.Resource(projectDeliveryGVR).Namespace("other").Create(context.Background(), second, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
publishQual(t, c, "point", strings.Repeat("1", 40))
other, err := decodeProjectDelivery(second)
if err != nil {
t.Fatal(err)
}
input := map[string]string{"source-repository": "https://example.test/repo/other", "source-ref": "", "source-revision": strings.Repeat("2", 40), "targets": "[{}]"}
observeQualBuild(&other, &attemptStatus{WorkflowName: "other-build", Phase: "Succeeded", Input: input, Result: json.RawMessage(`{"image":"other"}`)})
if err := c.updateStatus(context.Background(), second, &other); err != nil {
t.Fatal(err)
}
stepQual(t, c)
stepQual(t, c)
first := readQual(t, c, "point").Attempts[0]
markQual(t, c, "delivery", first.WorkflowName, "Running", now, nil)
stepQual(t, c)
objects, err := c.client.Resource(workflowGVR).Namespace(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{LabelSelector: purposeAnnotation + "=qualification"})
if err != nil || len(objects.Items) != 1 {
t.Fatalf("global Workflows = %d, error=%v", len(objects.Items), err)
}
publishQual(t, c, "point", strings.Repeat("3", 40))
markQual(t, c, "delivery", first.WorkflowName, "Succeeded", now, nil)
stepQual(t, c)
*now = now.Add(168 * time.Hour)
stepQual(t, c)
stepQual(t, c)
objects, err = c.client.Resource(workflowGVR).Namespace("other").List(context.Background(), metav1.ListOptions{LabelSelector: purposeAnnotation + "=qualification"})
if err != nil || len(objects.Items) != 1 {
t.Fatalf("waiting namespace did not advance: count=%d error=%v", len(objects.Items), err)
}
}
func TestQualPolicyChange(t *testing.T) {
c, _ := qualFixture(t, "OnBuild")
publishQual(t, c, "point", strings.Repeat("1", 40))
object := getDelivery(t, c, "point")
spec := object.Object["spec"].(map[string]any)["qualification"].(map[string]any)
spec["mode"] = "ManualOnly"
if _, err := c.client.Resource(projectDeliveryGVR).Namespace("delivery").Update(context.Background(), object, metav1.UpdateOptions{}); err != nil {
t.Fatal(err)
}
stepQual(t, c)
if len(readQual(t, c, "point").Attempts) != 0 {
t.Fatal("ManualOnly reserved an automatic candidate")
}
object = getDelivery(t, c, "point")
object.Object["spec"].(map[string]any)["qualification"].(map[string]any)["mode"] = "OnBuild"
if _, err := c.client.Resource(projectDeliveryGVR).Namespace("delivery").Update(context.Background(), object, metav1.UpdateOptions{}); err != nil {
t.Fatal(err)
}
stepQual(t, c)
if readQual(t, c, "point").Attempts[0].Phase != "Reserved" {
t.Fatal("OnBuild did not reconsider the waiting candidate")
}
}
func qualFixture(t *testing.T, mode string) (*controller, *time.Time) {
t.Helper()
now := time.Date(2026, time.September, 11, 0, 0, 0, 0, time.UTC)
object := pointDelivery(strings.Repeat("1", 40))
object.Object["spec"].(map[string]any)["qualification"] = map[string]any{
"workflowTemplateRef": map[string]any{"name": "qual-tests"}, "mode": mode, "interval": "168h",
}
return testController(t, object, &mutableRefs{}, &now), &now
}
func publishQual(t *testing.T, c *controller, name, revision string) string {
t.Helper()
object := getDelivery(t, c, name)
d, err := decodeProjectDelivery(object)
if err != nil {
t.Fatal(err)
}
attempt := attemptStatus{Revision: revision, Phase: "Pending"}
if err := c.advanceAttempt(context.Background(), &d, &attempt); err != nil {
t.Fatal(err)
}
result := fmt.Sprintf(`{"sourceRevision":%q,"targets":[{"reference":%q}]}`, revision, "synthetic/image@sha256:"+strings.Repeat("a", 64))
setNamedWorkflowStatus(t, c, attempt.WorkflowName, "Succeeded", map[string]string{"delivery-result": result})
if err := c.advanceAttempt(context.Background(), &d, &attempt); err != nil {
t.Fatal(err)
}
d.Status.Points = append(d.Status.Points, attempt)
if err := c.updateStatus(context.Background(), object, &d); err != nil {
t.Fatal(err)
}
return attempt.WorkflowName
}
func stepQual(t *testing.T, c *controller) {
t.Helper()
if err := c.qualifyAll(context.Background()); err != nil {
t.Fatal(err)
}
}
func readQual(t *testing.T, c *controller, name string) *qualStatus {
t.Helper()
d, err := decodeProjectDelivery(getDelivery(t, c, name))
if err != nil {
t.Fatal(err)
}
return d.Status.Qualification
}
func requestQual(t *testing.T, c *controller, name, request string) {
t.Helper()
object := getDelivery(t, c, name)
object.SetAnnotations(map[string]string{qualAnnotation: request})
if _, err := c.client.Resource(projectDeliveryGVR).Namespace("delivery").Update(context.Background(), object, metav1.UpdateOptions{}); err != nil {
t.Fatal(err)
}
}
func markQual(t *testing.T, c *controller, namespace, name, phase string, started *time.Time, outputs map[string]string) {
t.Helper()
object, err := c.client.Resource(workflowGVR).Namespace(namespace).Get(context.Background(), name, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
params := []any{}
for name, value := range outputs {
params = append(params, map[string]any{"name": name, "value": value})
}
object.Object["status"] = map[string]any{"phase": phase, "startedAt": started.Format(time.RFC3339), "outputs": map[string]any{"parameters": params}}
if _, err := c.client.Resource(workflowGVR).Namespace(namespace).UpdateStatus(context.Background(), object, metav1.UpdateOptions{}); err != nil {
t.Fatal(err)
}
}