diff --git a/provider_tekton.go b/provider_tekton.go index 30fdab9..41f1587 100644 --- a/provider_tekton.go +++ b/provider_tekton.go @@ -89,8 +89,46 @@ func parseTektonWorkflowConfig(raw string) (*tektonWorkflowConfig, error) { } cfg := doc.Tack.Tekton if cfg.Pipeline == "" { - return nil, errors.New("workflow yaml: `tack.tekton.pipeline` is required") + return nil, errors.New( + "workflow yaml: `tack.tekton.pipeline` is required", + ) } + + // Validate workspaces: each must have exactly one volume source + // so the resulting PipelineRun is unambiguous. Without this, + // a workspace with e.g. both `storage` and `pvc` set silently + // picks whichever the switch statement matches first. + for _, ws := range cfg.Workspaces { + if ws.Name == "" { + return nil, errors.New("workspace: name is required") + } + sources := 0 + if ws.Storage != nil { + sources++ + } + if ws.PVC != nil { + sources++ + } + if ws.Secret != nil { + sources++ + } + if ws.ConfigMap != nil { + sources++ + } + if sources == 0 { + return nil, fmt.Errorf( + "workspace %q: no volume source specified", ws.Name, + ) + } + if sources > 1 { + return nil, fmt.Errorf( + "workspace %q: multiple volume sources specified"+ + " (pick one of storage, pvc, secret, config_map)", + ws.Name, + ) + } + } + return &cfg, nil } diff --git a/provider_tekton_test.go b/provider_tekton_test.go index eb62de8..1021ab6 100644 --- a/provider_tekton_test.go +++ b/provider_tekton_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "log/slog" + "strings" "testing" "time" @@ -40,6 +41,54 @@ func TestTektonWorkflowConfig(t *testing.T) { } } +func TestTektonWorkspaceValidation(t *testing.T) { + tests := []struct { + name string + yaml string + wantErr string + }{ + { + name: "empty name", + yaml: "tack:\n tekton:\n pipeline: ci\n workspaces:\n - storage: 1Gi\n", + wantErr: "name is required", + }, + { + name: "no source", + yaml: "tack:\n tekton:\n pipeline: ci\n workspaces:\n - name: scratch\n", + wantErr: "no volume source", + }, + { + name: "multiple sources", + yaml: "tack:\n tekton:\n pipeline: ci\n workspaces:\n - name: data\n storage: 5Gi\n pvc: my-pvc\n", + wantErr: "multiple volume sources", + }, + { + name: "valid single source", + yaml: "tack:\n tekton:\n pipeline: ci\n workspaces:\n - name: scratch\n storage: 1Gi\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := parseTektonWorkflowConfig(tt.yaml) + if tt.wantErr == "" { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + return + } + if err == nil { + t.Fatalf("expected error containing %q", tt.wantErr) + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf( + "error %q does not contain %q", + err.Error(), tt.wantErr, + ) + } + }) + } +} + func TestTektonBuildPipelineRun(t *testing.T) { cfg := &tektonWorkflowConfig{ Pipeline: "repo-ci",