Stitch any CI into Tangled
3

Configure Feed

Select the types of activity you want to include in your feed.

1package main 2 3import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "log/slog" 8 "testing" 9 "time" 10 11 corev1 "k8s.io/api/core/v1" 12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 14 "k8s.io/apimachinery/pkg/runtime" 15 runtimeschema "k8s.io/apimachinery/pkg/runtime/schema" 16 dynamicfake "k8s.io/client-go/dynamic/fake" 17 kubefake "k8s.io/client-go/kubernetes/fake" 18 "tangled.org/core/api/tangled" 19) 20 21func newTektonTestProvider(t *testing.T, objs ...runtime.Object) (*tektonProvider, *store, *broker) { 22 t.Helper() 23 st := newTestStore(t) 24 br := newBroker(st) 25 dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds( 26 runtime.NewScheme(), 27 map[runtimeschema.GroupVersionResource]string{ 28 pipelineRunsGVR: "PipelineRunList", 29 taskRunsGVR: "TaskRunList", 30 }, 31 objs..., 32 ) 33 kube := kubefake.NewSimpleClientset() 34 p := newTektonProvider(br, st, dyn, kube, "ci", slog.Default()) 35 return p, st, br 36} 37 38func TestTektonWorkflowConfig(t *testing.T) { 39 raw := "tack:\n tekton:\n pipeline: repo-ci\n service_account: runner\n params:\n image: example/app\n" 40 cfg, err := parseTektonWorkflowConfig(raw) 41 if err != nil { 42 t.Fatalf("parse: %v", err) 43 } 44 if cfg.Pipeline != "repo-ci" || cfg.ServiceAccount != "runner" { 45 t.Fatalf("cfg mismatch: %+v", cfg) 46 } 47 if got := cfg.Params["image"]; got != "example/app" { 48 t.Fatalf("params[image] = %q", got) 49 } 50 51 if _, err := parseTektonWorkflowConfig("tack:\n tekton: {}\n"); err == nil { 52 t.Fatal("missing pipeline should fail") 53 } 54} 55 56func TestTektonBuildPipelineRun(t *testing.T) { 57 cfg := &tektonWorkflowConfig{ 58 Pipeline: "repo-ci", 59 ServiceAccount: "runner", 60 Params: map[string]string{ 61 "image": "example/app", 62 }, 63 } 64 name := tektonPipelineRunName("knot.example.com", "rkey-1", "ci.yml", "abcdef", "main") 65 if len(name) > 63 || name == "" { 66 t.Fatalf("bad generated name: %q", name) 67 } 68 69 obj := buildTektonPipelineRun("ci", name, cfg, 70 "knot.example.com", "rkey-1", "did:plc:actor", "abcdef", "main", 71 &tangled.Pipeline_Workflow{Name: "ci.yml"}, 72 ) 73 if obj.GetAPIVersion() != tektonAPIVersion || obj.GetKind() != tektonRunKind { 74 t.Fatalf("type meta mismatch: %s %s", obj.GetAPIVersion(), obj.GetKind()) 75 } 76 pipeline, _, _ := unstructured.NestedString(obj.Object, "spec", "pipelineRef", "name") 77 if pipeline != "repo-ci" { 78 t.Fatalf("pipelineRef.name = %q", pipeline) 79 } 80 sa, _, _ := unstructured.NestedString(obj.Object, "spec", "serviceAccountName") 81 if sa != "runner" { 82 t.Fatalf("serviceAccountName = %q", sa) 83 } 84 params, _, _ := unstructured.NestedSlice(obj.Object, "spec", "params") 85 if len(params) != 1 { 86 t.Fatalf("params = %+v", params) 87 } 88 if obj.GetAnnotations()[tektonAnnotationActor] != "did:plc:actor" || 89 obj.GetAnnotations()[tektonAnnotationCommit] != "abcdef" { 90 t.Fatalf("annotations missing identity: %+v", obj.GetAnnotations()) 91 } 92} 93 94func TestTektonStatusMapping(t *testing.T) { 95 tests := []struct { 96 name string 97 cond string 98 reason string 99 status string 100 terminal bool 101 ok bool 102 }{ 103 {name: "unknown", cond: "Unknown", status: "running", ok: true}, 104 {name: "success", cond: "True", status: "success", terminal: true, ok: true}, 105 {name: "failed", cond: "False", reason: "Failed", status: "failed", terminal: true, ok: true}, 106 {name: "cancelled", cond: "False", reason: "PipelineRunCancelled", status: "cancelled", terminal: true, ok: true}, 107 {name: "stopped", cond: "False", reason: "PipelineRunStopped", status: "cancelled", terminal: true, ok: true}, 108 } 109 for _, tt := range tests { 110 t.Run(tt.name, func(t *testing.T) { 111 obj := tektonStatusObject(tt.cond, tt.reason) 112 status, terminal, ok := mapTektonPipelineRunStatus(obj) 113 if status != tt.status || terminal != tt.terminal || ok != tt.ok { 114 t.Fatalf("got %q/%v/%v; want %q/%v/%v", 115 status, terminal, ok, tt.status, tt.terminal, tt.ok) 116 } 117 }) 118 } 119} 120 121func TestTektonSpawnCreatesPipelineRun(t *testing.T) { 122 p, st, _ := newTektonTestProvider(t) 123 ctx, cancel := context.WithCancel(context.Background()) 124 defer cancel() 125 126 trigger := &tangled.Pipeline_TriggerMetadata{ 127 Push: &tangled.Pipeline_PushTriggerData{ 128 NewSha: "abcdef0123", 129 Ref: "refs/heads/main", 130 }, 131 } 132 p.Spawn(ctx, "knot.example.com", "rkey-1", "did:plc:actor", trigger, 133 []*tangled.Pipeline_Workflow{{Name: "ci.yml", 134 Raw: "tack:\n tekton:\n pipeline: repo-ci\n"}}, 135 ) 136 137 ref := waitTektonRef(t, st, "knot.example.com", "rkey-1", "ci.yml") 138 if ref.Namespace != "ci" || ref.PipelineName != "repo-ci" { 139 t.Fatalf("ref mismatch: %+v", ref) 140 } 141 142 obj, err := p.dyn.Resource(pipelineRunsGVR).Namespace("ci"). 143 Get(context.Background(), ref.PipelineRunName, metav1.GetOptions{}) 144 if err != nil { 145 t.Fatalf("get PipelineRun: %v", err) 146 } 147 pipeline, _, _ := unstructured.NestedString(obj.Object, "spec", "pipelineRef", "name") 148 if pipeline != "repo-ci" { 149 t.Fatalf("pipelineRef.name = %q", pipeline) 150 } 151 152 rows, err := st.EventsAfter(context.Background(), 0) 153 if err != nil { 154 t.Fatalf("EventsAfter: %v", err) 155 } 156 if len(rows) != 1 { 157 t.Fatalf("got %d events, want 1", len(rows)) 158 } 159 var rec tangled.PipelineStatus 160 if err := json.Unmarshal(rows[0].EventJSON, &rec); err != nil { 161 t.Fatalf("decode status: %v", err) 162 } 163 if rec.Status != "pending" || rec.Workflow != "ci.yml" { 164 t.Fatalf("bad pending status: %+v", rec) 165 } 166} 167 168func TestTektonSpawnAlreadyExists(t *testing.T) { 169 name := tektonPipelineRunName("knot.example.com", "rkey-1", "ci.yml", "abcdef0123", "main") 170 existing := buildTektonPipelineRun("ci", name, 171 &tektonWorkflowConfig{Pipeline: "repo-ci"}, 172 "knot.example.com", "rkey-1", "did:plc:actor", "abcdef0123", "main", 173 &tangled.Pipeline_Workflow{Name: "ci.yml"}, 174 ) 175 existing.SetUID("uid-1") 176 p, st, _ := newTektonTestProvider(t, existing) 177 ctx, cancel := context.WithCancel(context.Background()) 178 defer cancel() 179 180 p.Spawn(ctx, "knot.example.com", "rkey-1", "did:plc:actor", 181 &tangled.Pipeline_TriggerMetadata{Push: &tangled.Pipeline_PushTriggerData{ 182 NewSha: "abcdef0123", 183 Ref: "refs/heads/main", 184 }}, 185 []*tangled.Pipeline_Workflow{{Name: "ci.yml", 186 Raw: "tack:\n tekton:\n pipeline: repo-ci\n"}}, 187 ) 188 189 ref := waitTektonRef(t, st, "knot.example.com", "rkey-1", "ci.yml") 190 if ref.PipelineRunName != name || ref.PipelineRunUID != "uid-1" { 191 t.Fatalf("ref mismatch: %+v", ref) 192 } 193} 194 195func TestTektonLogsLookup(t *testing.T) { 196 p, st, _ := newTektonTestProvider(t) 197 ctx := context.Background() 198 if _, err := p.Logs(ctx, "knot.example.com", "rkey-1", "ci.yml"); !errors.Is(err, ErrLogsNotFound) { 199 t.Fatalf("logs before mapping err = %v; want ErrLogsNotFound", err) 200 } 201 ref := TektonRunRef{ 202 Knot: "knot.example.com", 203 PipelineRkey: "rkey-1", 204 Workflow: "ci.yml", 205 Namespace: "ci", 206 PipelineRunName: "run-1", 207 PipelineRunUID: "uid-1", 208 PipelineName: "repo-ci", 209 PipelineURI: pipelineATURI("knot.example.com", "rkey-1"), 210 } 211 if err := st.InsertTektonRun(ctx, ref); err != nil { 212 t.Fatalf("insert ref: %v", err) 213 } 214 if _, err := p.Logs(ctx, "knot.example.com", "rkey-1", "ci.yml"); !errors.Is(err, ErrLogsNotFound) { 215 t.Fatalf("logs before TaskRuns err = %v; want ErrLogsNotFound", err) 216 } 217 218 taskRun := &unstructured.Unstructured{Object: map[string]interface{}{ 219 "apiVersion": "tekton.dev/v1", 220 "kind": "TaskRun", 221 "metadata": map[string]interface{}{ 222 "name": "task-1", 223 "namespace": "ci", 224 "labels": map[string]interface{}{ 225 "tekton.dev/pipelineRun": "run-1", 226 }, 227 }, 228 }} 229 _, err := p.dyn.Resource(taskRunsGVR).Namespace("ci"). 230 Create(ctx, taskRun, metav1.CreateOptions{}) 231 if err != nil { 232 t.Fatalf("create TaskRun: %v", err) 233 } 234 _, err = p.kube.CoreV1().Pods("ci").Create(ctx, &corev1.Pod{ 235 ObjectMeta: metav1.ObjectMeta{ 236 Name: "pod-1", 237 Namespace: "ci", 238 Labels: map[string]string{ 239 "tekton.dev/taskRun": "task-1", 240 }, 241 }, 242 Spec: corev1.PodSpec{ 243 Containers: []corev1.Container{{Name: "step-test", Image: "busybox"}}, 244 }, 245 }, metav1.CreateOptions{}) 246 if err != nil { 247 t.Fatalf("create pod: %v", err) 248 } 249 250 ch, err := p.Logs(ctx, "knot.example.com", "rkey-1", "ci.yml") 251 if err != nil { 252 t.Fatalf("Logs after pods: %v", err) 253 } 254 var got []LogLine 255 for line := range ch { 256 got = append(got, line) 257 } 258 if len(got) < 2 || got[0].StepStatus != StepStatusStart || 259 got[len(got)-1].StepStatus != StepStatusEnd { 260 t.Fatalf("log frames = %+v", got) 261 } 262} 263 264func tektonStatusObject(condStatus, reason string) *unstructured.Unstructured { 265 return &unstructured.Unstructured{Object: map[string]interface{}{ 266 "status": map[string]interface{}{ 267 "conditions": []interface{}{map[string]interface{}{ 268 "type": "Succeeded", 269 "status": condStatus, 270 "reason": reason, 271 }}, 272 }, 273 }} 274} 275 276func waitTektonRef(t *testing.T, st *store, knot, rkey, workflow string) *TektonRunRef { 277 t.Helper() 278 deadline := time.Now().Add(2 * time.Second) 279 for time.Now().Before(deadline) { 280 ref, err := st.LookupTektonRunByTuple(context.Background(), knot, rkey, workflow) 281 if err != nil { 282 t.Fatalf("lookup: %v", err) 283 } 284 if ref != nil { 285 return ref 286 } 287 time.Sleep(20 * time.Millisecond) 288 } 289 t.Fatal("tekton run row not persisted within deadline") 290 return nil 291}