From f800bb2bcc4bf2bca1d6f95c7c40ce5512c930be Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 7 Jan 2026 13:04:51 +0100 Subject: [PATCH 1/3] chore(runner): First cut on Tekton pipeline detection Signed-off-by: Javier Rodriguez --- app/cli/pkg/action/workflow_run_list.go | 1 + app/cli/pkg/action/workflow_run_list_test.go | 4 + .../workflowcontract/v1/crafting_schema.ts | 6 + ...attestation.v1.Attestation.jsonschema.json | 6 +- .../attestation.v1.Attestation.schema.json | 6 +- ...ation.v1.RunnerEnvironment.jsonschema.json | 3 +- ...testation.v1.RunnerEnvironment.schema.json | 3 +- ...estationServiceInitRequest.jsonschema.json | 3 +- ....AttestationServiceInitRequest.schema.json | 3 +- ...lane.v1.MetricsRunnerCount.jsonschema.json | 6 +- ...rolplane.v1.MetricsRunnerCount.schema.json | 6 +- ...olplane.v1.WorkflowRunItem.jsonschema.json | 6 +- ...ontrolplane.v1.WorkflowRunItem.schema.json | 6 +- ...WorkflowServiceListRequest.jsonschema.json | 6 +- ....v1.WorkflowServiceListRequest.schema.json | 6 +- ...t.v1.CraftingSchema.Runner.jsonschema.json | 3 +- ...tract.v1.CraftingSchema.Runner.schema.json | 3 +- .../workflowcontract/v1/crafting_schema.pb.go | 12 +- .../workflowcontract/v1/crafting_schema.proto | 1 + pkg/attestation/crafter/runner.go | 5 +- .../crafter/runners/tektonpipeline.go | 212 +++++++++++++++ .../crafter/runners/tektonpipeline_test.go | 254 ++++++++++++++++++ 22 files changed, 534 insertions(+), 27 deletions(-) create mode 100644 pkg/attestation/crafter/runners/tektonpipeline.go create mode 100644 pkg/attestation/crafter/runners/tektonpipeline_test.go diff --git a/app/cli/pkg/action/workflow_run_list.go b/app/cli/pkg/action/workflow_run_list.go index face16221..5d313212d 100644 --- a/app/cli/pkg/action/workflow_run_list.go +++ b/app/cli/pkg/action/workflow_run_list.go @@ -193,6 +193,7 @@ func humanizedRunnerType(in v1.CraftingSchema_Runner_RunnerType) string { *v1.CraftingSchema_Runner_CIRCLECI_BUILD.Enum(): "CircleCI Build", *v1.CraftingSchema_Runner_DAGGER_PIPELINE.Enum(): "Dagger Pipeline", *v1.CraftingSchema_Runner_TEAMCITY_PIPELINE.Enum(): "TeamCity Pipeline", + *v1.CraftingSchema_Runner_TEKTON_PIPELINE.Enum(): "Tekton Pipeline", } hrt, ok := mapping[in] diff --git a/app/cli/pkg/action/workflow_run_list_test.go b/app/cli/pkg/action/workflow_run_list_test.go index afd7c2dfa..1c12afc6f 100644 --- a/app/cli/pkg/action/workflow_run_list_test.go +++ b/app/cli/pkg/action/workflow_run_list_test.go @@ -64,6 +64,10 @@ func (s *workflowRunListSuite) TestHumanizedRunnerType() { name: "teamcity runner", testInput: v1.CraftingSchema_Runner_TEAMCITY_PIPELINE, expectedOutput: "TeamCity Pipeline", + }, { + name: "tekton runner", + testInput: v1.CraftingSchema_Runner_TEKTON_PIPELINE, + expectedOutput: "Tekton Pipeline", }, { name: "unknown runner", testInput: -34, diff --git a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts index 823bc500e..5703a9ca2 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -59,6 +59,7 @@ export enum CraftingSchema_Runner_RunnerType { CIRCLECI_BUILD = 5, DAGGER_PIPELINE = 6, TEAMCITY_PIPELINE = 7, + TEKTON_PIPELINE = 8, UNRECOGNIZED = -1, } @@ -88,6 +89,9 @@ export function craftingSchema_Runner_RunnerTypeFromJSON(object: any): CraftingS case 7: case "TEAMCITY_PIPELINE": return CraftingSchema_Runner_RunnerType.TEAMCITY_PIPELINE; + case 8: + case "TEKTON_PIPELINE": + return CraftingSchema_Runner_RunnerType.TEKTON_PIPELINE; case -1: case "UNRECOGNIZED": default: @@ -113,6 +117,8 @@ export function craftingSchema_Runner_RunnerTypeToJSON(object: CraftingSchema_Ru return "DAGGER_PIPELINE"; case CraftingSchema_Runner_RunnerType.TEAMCITY_PIPELINE: return "TEAMCITY_PIPELINE"; + case CraftingSchema_Runner_RunnerType.TEKTON_PIPELINE: + return "TEKTON_PIPELINE"; case CraftingSchema_Runner_RunnerType.UNRECOGNIZED: default: return "UNRECOGNIZED"; diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.jsonschema.json index 52e25ff42..79631f02e 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.jsonschema.json @@ -60,7 +60,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" @@ -166,7 +167,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.schema.json index a63621a89..733a8dbed 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.schema.json @@ -60,7 +60,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" @@ -166,7 +167,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.RunnerEnvironment.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.RunnerEnvironment.jsonschema.json index 5232b1d25..66d729043 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.RunnerEnvironment.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.RunnerEnvironment.jsonschema.json @@ -29,7 +29,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.RunnerEnvironment.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.RunnerEnvironment.schema.json index a106ad10c..cf6cb244d 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.RunnerEnvironment.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.RunnerEnvironment.schema.json @@ -29,7 +29,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.jsonschema.json index 4eb02cd6b..85c42f1fc 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.jsonschema.json @@ -60,7 +60,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.schema.json index 0379ff6f5..707469a66 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.schema.json @@ -60,7 +60,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.MetricsRunnerCount.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.MetricsRunnerCount.jsonschema.json index 34950a0c9..63d9c4a21 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.MetricsRunnerCount.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.MetricsRunnerCount.jsonschema.json @@ -14,7 +14,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" @@ -44,7 +45,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.MetricsRunnerCount.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.MetricsRunnerCount.schema.json index 066e8b29e..54a36e983 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.MetricsRunnerCount.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.MetricsRunnerCount.schema.json @@ -14,7 +14,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" @@ -44,7 +45,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowRunItem.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowRunItem.jsonschema.json index 02fb307e1..cfa5aee10 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowRunItem.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowRunItem.jsonschema.json @@ -42,7 +42,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" @@ -102,7 +103,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowRunItem.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowRunItem.schema.json index 6192da887..67c1560a8 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowRunItem.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowRunItem.schema.json @@ -42,7 +42,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" @@ -102,7 +103,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowServiceListRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowServiceListRequest.jsonschema.json index 363cd21a5..d870a1966 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowServiceListRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowServiceListRequest.jsonschema.json @@ -82,7 +82,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" @@ -184,7 +185,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowServiceListRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowServiceListRequest.schema.json index d6683179e..7f7ddac4d 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowServiceListRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.WorkflowServiceListRequest.schema.json @@ -82,7 +82,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" @@ -184,7 +185,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Runner.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Runner.jsonschema.json index 266e46c79..ae0d21cb3 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Runner.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Runner.jsonschema.json @@ -14,7 +14,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Runner.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Runner.schema.json index 440b24ed4..ac54e3edc 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Runner.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Runner.schema.json @@ -14,7 +14,8 @@ "JENKINS_JOB", "CIRCLECI_BUILD", "DAGGER_PIPELINE", - "TEAMCITY_PIPELINE" + "TEAMCITY_PIPELINE", + "TEKTON_PIPELINE" ], "title": "Runner Type", "type": "string" diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go index 67639c853..879c60cd3 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -48,6 +48,7 @@ const ( CraftingSchema_Runner_CIRCLECI_BUILD CraftingSchema_Runner_RunnerType = 5 CraftingSchema_Runner_DAGGER_PIPELINE CraftingSchema_Runner_RunnerType = 6 CraftingSchema_Runner_TEAMCITY_PIPELINE CraftingSchema_Runner_RunnerType = 7 + CraftingSchema_Runner_TEKTON_PIPELINE CraftingSchema_Runner_RunnerType = 8 ) // Enum value maps for CraftingSchema_Runner_RunnerType. @@ -61,6 +62,7 @@ var ( 5: "CIRCLECI_BUILD", 6: "DAGGER_PIPELINE", 7: "TEAMCITY_PIPELINE", + 8: "TEKTON_PIPELINE", } CraftingSchema_Runner_RunnerType_value = map[string]int32{ "RUNNER_TYPE_UNSPECIFIED": 0, @@ -71,6 +73,7 @@ var ( "CIRCLECI_BUILD": 5, "DAGGER_PIPELINE": 6, "TEAMCITY_PIPELINE": 7, + "TEKTON_PIPELINE": 8, } ) @@ -1789,7 +1792,7 @@ var File_workflowcontract_v1_crafting_schema_proto protoreflect.FileDescriptor const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\n" + - ")workflowcontract/v1/crafting_schema.proto\x12\x13workflowcontract.v1\x1a\x1bbuf/validate/validate.proto\"\x82\x0e\n" + + ")workflowcontract/v1/crafting_schema.proto\x12\x13workflowcontract.v1\x1a\x1bbuf/validate/validate.proto\"\x97\x0e\n" + "\x0eCraftingSchema\x122\n" + "\x0eschema_version\x18\x01 \x01(\tB\v\xbaH\x06r\x04\n" + "\x02v1\x18\x01R\rschemaVersion\x12N\n" + @@ -1798,9 +1801,9 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\x06runner\x18\x04 \x01(\v2*.workflowcontract.v1.CraftingSchema.RunnerB\x02\x18\x01R\x06runner\x12E\n" + "\vannotations\x18\x05 \x03(\v2\x1f.workflowcontract.v1.AnnotationB\x02\x18\x01R\vannotations\x12=\n" + "\bpolicies\x18\x06 \x01(\v2\x1d.workflowcontract.v1.PoliciesB\x02\x18\x01R\bpolicies\x12S\n" + - "\rpolicy_groups\x18\a \x03(\v2*.workflowcontract.v1.PolicyGroupAttachmentB\x02\x18\x01R\fpolicyGroups\x1a\x9e\x02\n" + + "\rpolicy_groups\x18\a \x03(\v2*.workflowcontract.v1.PolicyGroupAttachmentB\x02\x18\x01R\fpolicyGroups\x1a\xb3\x02\n" + "\x06Runner\x12W\n" + - "\x04type\x18\x01 \x01(\x0e25.workflowcontract.v1.CraftingSchema.Runner.RunnerTypeB\f\xbaH\a\x82\x01\x04\x10\x01 \x00\x18\x01R\x04type\"\xb6\x01\n" + + "\x04type\x18\x01 \x01(\x0e25.workflowcontract.v1.CraftingSchema.Runner.RunnerTypeB\f\xbaH\a\x82\x01\x04\x10\x01 \x00\x18\x01R\x04type\"\xcb\x01\n" + "\n" + "RunnerType\x12\x1b\n" + "\x17RUNNER_TYPE_UNSPECIFIED\x10\x00\x12\x11\n" + @@ -1810,7 +1813,8 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\vJENKINS_JOB\x10\x04\x12\x12\n" + "\x0eCIRCLECI_BUILD\x10\x05\x12\x13\n" + "\x0fDAGGER_PIPELINE\x10\x06\x12\x15\n" + - "\x11TEAMCITY_PIPELINE\x10\a:\x02\x18\x01\x1a\xf9\a\n" + + "\x11TEAMCITY_PIPELINE\x10\a\x12\x13\n" + + "\x0fTEKTON_PIPELINE\x10\b:\x02\x18\x01\x1a\xf9\a\n" + "\bMaterial\x12[\n" + "\x04type\x18\x01 \x01(\x0e29.workflowcontract.v1.CraftingSchema.Material.MaterialTypeB\f\xbaH\a\x82\x01\x04\x10\x01 \x00\x18\x01R\x04type\x12\x99\x01\n" + "\x04name\x18\x02 \x01(\tB\x84\x01\xbaH\x7f\xba\x01|\n" + diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto index 2f2039ea7..a653f88ad 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -63,6 +63,7 @@ message CraftingSchema { CIRCLECI_BUILD = 5; DAGGER_PIPELINE = 6; TEAMCITY_PIPELINE = 7; + TEKTON_PIPELINE = 8; } } diff --git a/pkg/attestation/crafter/runner.go b/pkg/attestation/crafter/runner.go index 56c32158c..2487a6fb7 100644 --- a/pkg/attestation/crafter/runner.go +++ b/pkg/attestation/crafter/runner.go @@ -1,5 +1,5 @@ // -// Copyright 2023 The Chainloop Authors. +// Copyright 2023-2025 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -85,6 +85,9 @@ var RunnerFactories = map[schemaapi.CraftingSchema_Runner_RunnerType]RunnerFacto schemaapi.CraftingSchema_Runner_TEAMCITY_PIPELINE: func(_ string, _ *zerolog.Logger) SupportedRunner { return runners.NewTeamCityPipeline() }, + schemaapi.CraftingSchema_Runner_TEKTON_PIPELINE: func(_ string, _ *zerolog.Logger) SupportedRunner { + return runners.NewTektonPipeline() + }, } // Load a specific runner diff --git a/pkg/attestation/crafter/runners/tektonpipeline.go b/pkg/attestation/crafter/runners/tektonpipeline.go new file mode 100644 index 000000000..7f67c4b47 --- /dev/null +++ b/pkg/attestation/crafter/runners/tektonpipeline.go @@ -0,0 +1,212 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runners + +import ( + "fmt" + "os" + "strings" + + schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" +) + +const ( + // Default path for Downward API labels + defaultLabelsPath = "/etc/podinfo/labels" + // Default Tekton dashboard URL + defaultDashboardURL = "https://dashboard.tekton.dev" +) + +type TektonPipeline struct { + // Path to the Downward API labels file (configurable for testing) + labelsPath string +} + +func NewTektonPipeline() *TektonPipeline { + return &TektonPipeline{ + labelsPath: defaultLabelsPath, + } +} + +func (r *TektonPipeline) ID() schemaapi.CraftingSchema_Runner_RunnerType { + return schemaapi.CraftingSchema_Runner_TEKTON_PIPELINE +} + +// CheckEnv detects if we're running in a Tekton environment +// by checking for the existence of Tekton-specific directories +func (r *TektonPipeline) CheckEnv() bool { + // Check for /tekton/results directory (most reliable indicator) + if _, err := os.Stat("/tekton/results"); err == nil { + return true + } + return false +} + +// ListEnvVars returns environment variables collected from Downward API labels +func (r *TektonPipeline) ListEnvVars() []*EnvVarDefinition { + // Parse labels and convert to environment variable definitions + labels := r.parseLabels() + if len(labels) == 0 { + return []*EnvVarDefinition{} + } + + var envVars []*EnvVarDefinition + + // Map Tekton labels to environment variables (all optional) + labelMappings := map[string]string{ + "tekton.dev/pipelineRun": "TEKTON_PIPELINE_RUN", + "tekton.dev/pipelineRunUID": "TEKTON_PIPELINE_RUN_UID", + "tekton.dev/pipeline": "TEKTON_PIPELINE", + "tekton.dev/taskRun": "TEKTON_TASKRUN_NAME", + "tekton.dev/taskRunUID": "TEKTON_TASKRUN_UID", + "tekton.dev/task": "TEKTON_TASK_NAME", + } + + for labelKey, envVarName := range labelMappings { + if _, exists := labels[labelKey]; exists { + envVars = append(envVars, &EnvVarDefinition{ + Name: envVarName, + Optional: true, + }) + } + } + + // Add namespace if available + if ns := r.getNamespace(); ns != "" { + envVars = append(envVars, &EnvVarDefinition{ + Name: "TEKTON_NAMESPACE", + Optional: true, + }) + } + + return envVars +} + +// parseLabels reads and parses the Downward API labels file +// Returns a map of label key-value pairs +func (r *TektonPipeline) parseLabels() map[string]string { + labels := make(map[string]string) + + data, err := os.ReadFile(r.labelsPath) + if err != nil { + return labels + } + + // Parse labels in format: key="value" + // Labels are separated by newlines + lines := strings.Split(string(data), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // Split on first = to get key and quoted value + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + + key := strings.TrimSpace(parts[0]) + value := strings.Trim(strings.TrimSpace(parts[1]), `"`) + labels[key] = value + } + + return labels +} + +func (r *TektonPipeline) RunURI() string { + labels := r.parseLabels() + namespace := r.getNamespace() + + if namespace == "" { + return "" + } + + // Get dashboard URL from environment variable or use default + dashboardURL := os.Getenv("TEKTON_DASHBOARD_URL") + if dashboardURL == "" { + dashboardURL = defaultDashboardURL + } + + // Priority 1: If we have PipelineRun context, construct PipelineRun URL + if pipelineRun, ok := labels["tekton.dev/pipelineRun"]; ok && pipelineRun != "" { + return fmt.Sprintf("%s/#/namespaces/%s/pipelineruns/%s", dashboardURL, namespace, pipelineRun) + } + + // Priority 2: If we have TaskRun context, construct TaskRun URL + if taskRun, ok := labels["tekton.dev/taskRun"]; ok && taskRun != "" { + return fmt.Sprintf("%s/#/namespaces/%s/taskruns/%s", dashboardURL, namespace, taskRun) + } + + return "" +} + +// getNamespace attempts to get the namespace from the service account +func (r *TektonPipeline) getNamespace() string { + // Read from service account (standard Kubernetes location) + if data, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil { + return strings.TrimSpace(string(data)) + } + + return "" +} + +func (r *TektonPipeline) ResolveEnvVars() (map[string]string, []*error) { + result := make(map[string]string) + labels := r.parseLabels() + + // Map Tekton labels to environment variable names + labelMappings := map[string]string{ + "tekton.dev/pipelineRun": "TEKTON_PIPELINE_RUN", + "tekton.dev/pipelineRunUID": "TEKTON_PIPELINE_RUN_UID", + "tekton.dev/pipeline": "TEKTON_PIPELINE", + "tekton.dev/taskRun": "TEKTON_TASKRUN_NAME", + "tekton.dev/taskRunUID": "TEKTON_TASKRUN_UID", + "tekton.dev/task": "TEKTON_TASK_NAME", + } + + for labelKey, envVarName := range labelMappings { + if value, ok := labels[labelKey]; ok && value != "" { + result[envVarName] = value + } + } + + // Add namespace if available + if ns := r.getNamespace(); ns != "" { + result["TEKTON_NAMESPACE"] = ns + } + + // No errors since all variables are optional + return result, nil +} + +func (r *TektonPipeline) WorkflowFilePath() string { + // Tekton doesn't have a single workflow file path concept + // Tasks and Pipelines are defined as Kubernetes resources + return "" +} + +func (r *TektonPipeline) IsAuthenticated() bool { + // No OIDC support initially + return false +} + +func (r *TektonPipeline) Environment() RunnerEnvironment { + // Could be enhanced to detect managed Tekton services (e.g., OpenShift Pipelines) + // For now, return Unknown + return Unknown +} diff --git a/pkg/attestation/crafter/runners/tektonpipeline_test.go b/pkg/attestation/crafter/runners/tektonpipeline_test.go new file mode 100644 index 000000000..f7767bda9 --- /dev/null +++ b/pkg/attestation/crafter/runners/tektonpipeline_test.go @@ -0,0 +1,254 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runners + +import ( + "os" + "path/filepath" + "testing" + + schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type tektonPipelineTestSuite struct { + suite.Suite + runner *TektonPipeline + tmpDir string +} + +func (s *tektonPipelineTestSuite) SetupTest() { + // Create a temporary directory for test files + tmpDir, err := os.MkdirTemp("", "tekton-test-*") + assert.NoError(s.T(), err) + s.tmpDir = tmpDir + + s.runner = NewTektonPipeline() + // Point to temp directory for tests + s.runner.labelsPath = filepath.Join(tmpDir, "labels") +} + +func (s *tektonPipelineTestSuite) TearDownTest() { + // Clean up temporary directory + if s.tmpDir != "" { + os.RemoveAll(s.tmpDir) + } +} + +func (s *tektonPipelineTestSuite) TestID() { + assert.Equal(s.T(), schemaapi.CraftingSchema_Runner_TEKTON_PIPELINE, s.runner.ID()) + assert.Equal(s.T(), "TEKTON_PIPELINE", s.runner.ID().String()) +} + +func (s *tektonPipelineTestSuite) TestCheckEnv() { + // CheckEnv should return false in normal test environment (no /tekton directory) + assert.False(s.T(), s.runner.CheckEnv()) +} + +func (s *tektonPipelineTestSuite) TestParseLabels_PipelineRun() { + // Create a labels file with PipelineRun context + labelsContent := `tekton.dev/pipeline="my-pipeline" +tekton.dev/pipelineRun="my-pipeline-run-123" +tekton.dev/pipelineRunUID="abc-123-def" +tekton.dev/pipelineTask="build-task" +tekton.dev/taskRun="my-pipeline-run-123-build-task-xyz" +tekton.dev/taskRunUID="xyz-789-uvw" +tekton.dev/task="build-task" +app.kubernetes.io/managed-by="tekton-pipelines" +` + err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) + assert.NoError(s.T(), err) + + labels := s.runner.parseLabels() + assert.Equal(s.T(), "my-pipeline", labels["tekton.dev/pipeline"]) + assert.Equal(s.T(), "my-pipeline-run-123", labels["tekton.dev/pipelineRun"]) + assert.Equal(s.T(), "abc-123-def", labels["tekton.dev/pipelineRunUID"]) + assert.Equal(s.T(), "my-pipeline-run-123-build-task-xyz", labels["tekton.dev/taskRun"]) +} + +func (s *tektonPipelineTestSuite) TestParseLabels_TaskRun() { + // Create a labels file with standalone TaskRun context + labelsContent := `tekton.dev/task="my-task" +tekton.dev/taskRun="my-taskrun-456" +tekton.dev/taskRunUID="def-456-ghi" +app.kubernetes.io/managed-by="tekton-pipelines" +` + err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) + assert.NoError(s.T(), err) + + labels := s.runner.parseLabels() + assert.Equal(s.T(), "my-task", labels["tekton.dev/task"]) + assert.Equal(s.T(), "my-taskrun-456", labels["tekton.dev/taskRun"]) + assert.Equal(s.T(), "def-456-ghi", labels["tekton.dev/taskRunUID"]) + // PipelineRun labels should not be present + _, hasPipelineRun := labels["tekton.dev/pipelineRun"] + assert.False(s.T(), hasPipelineRun) +} + +func (s *tektonPipelineTestSuite) TestParseLabels_NoFile() { + // When labels file doesn't exist, should return empty map + labels := s.runner.parseLabels() + assert.Empty(s.T(), labels) +} + +func (s *tektonPipelineTestSuite) TestListEnvVars_WithLabels() { + // Create a labels file + labelsContent := `tekton.dev/pipelineRun="my-run" +tekton.dev/taskRun="my-task-run" +` + err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) + assert.NoError(s.T(), err) + + envVars := s.runner.ListEnvVars() + assert.Greater(s.T(), len(envVars), 0) + + // All environment variables should be optional + for _, envVar := range envVars { + assert.True(s.T(), envVar.Optional, "Expected %s to be optional", envVar.Name) + } +} + +func (s *tektonPipelineTestSuite) TestListEnvVars_NoLabels() { + // When labels file doesn't exist, should return empty list + envVars := s.runner.ListEnvVars() + assert.Empty(s.T(), envVars) +} + +func (s *tektonPipelineTestSuite) TestResolveEnvVars_PipelineRun() { + // Create a labels file with PipelineRun context + labelsContent := `tekton.dev/pipeline="my-pipeline" +tekton.dev/pipelineRun="my-pipeline-run-123" +tekton.dev/pipelineRunUID="abc-123-def" +tekton.dev/taskRun="task-run-xyz" +` + err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) + assert.NoError(s.T(), err) + + resolved, errors := s.runner.ResolveEnvVars() + + assert.Nil(s.T(), errors) + assert.Equal(s.T(), "my-pipeline", resolved["TEKTON_PIPELINE"]) + assert.Equal(s.T(), "my-pipeline-run-123", resolved["TEKTON_PIPELINE_RUN"]) + assert.Equal(s.T(), "abc-123-def", resolved["TEKTON_PIPELINE_RUN_UID"]) + assert.Equal(s.T(), "task-run-xyz", resolved["TEKTON_TASKRUN_NAME"]) +} + +func (s *tektonPipelineTestSuite) TestResolveEnvVars_NoLabels() { + // When labels file doesn't exist, should return empty map with no errors + resolved, errors := s.runner.ResolveEnvVars() + + assert.Nil(s.T(), errors) + // Should be empty or only contain namespace if service account exists + assert.LessOrEqual(s.T(), len(resolved), 1) // Max 1 for namespace +} + +func (s *tektonPipelineTestSuite) TestRunURI_PipelineRun() { + // Create labels and namespace files + labelsContent := `tekton.dev/pipelineRun="my-pipeline-run-123" +` + err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) + assert.NoError(s.T(), err) + + // Mock namespace by creating service account namespace file + nsDir := filepath.Join(s.tmpDir, "run", "secrets", "kubernetes.io", "serviceaccount") + err = os.MkdirAll(nsDir, 0755) + assert.NoError(s.T(), err) + nsPath := filepath.Join(nsDir, "namespace") + err = os.WriteFile(nsPath, []byte("production"), 0600) + assert.NoError(s.T(), err) + + // Override the namespace path temporarily + // Since we can't easily mock os.ReadFile, we'll just test that labels are parsed correctly + labels := s.runner.parseLabels() + assert.Equal(s.T(), "my-pipeline-run-123", labels["tekton.dev/pipelineRun"]) + + // The actual URI construction would need namespace from service account which doesn't exist in tests + uri := s.runner.RunURI() + // Will be empty in test environment since service account file is in different location + assert.Equal(s.T(), "", uri) +} + +func (s *tektonPipelineTestSuite) TestRunURI_TaskRun() { + // Create a labels file with TaskRun only (no PipelineRun) + labelsContent := `tekton.dev/taskRun="my-taskrun-456" +` + err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) + assert.NoError(s.T(), err) + + labels := s.runner.parseLabels() + assert.Equal(s.T(), "my-taskrun-456", labels["tekton.dev/taskRun"]) +} + +func (s *tektonPipelineTestSuite) TestRunURI_PipelineRunPriority() { + // When both PipelineRun and TaskRun are present, verify labels are parsed correctly + labelsContent := `tekton.dev/pipelineRun="pipeline-run-123" +tekton.dev/taskRun="taskrun-456" +` + err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) + assert.NoError(s.T(), err) + + labels := s.runner.parseLabels() + assert.Equal(s.T(), "pipeline-run-123", labels["tekton.dev/pipelineRun"]) + assert.Equal(s.T(), "taskrun-456", labels["tekton.dev/taskRun"]) + // Priority logic is tested in RunURI implementation +} + +func (s *tektonPipelineTestSuite) TestRunURI_NoLabels() { + // Test with no labels file + uri := s.runner.RunURI() + assert.Equal(s.T(), "", uri) +} + +func (s *tektonPipelineTestSuite) TestRunURI_CustomDashboard() { + // Test custom dashboard URL via environment variable + labelsContent := `tekton.dev/pipelineRun="my-run" +` + err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) + assert.NoError(s.T(), err) + + s.T().Setenv("TEKTON_DASHBOARD_URL", "https://tekton.example.com") + + // Custom dashboard URL is tested in the implementation + // Actual URI would need namespace which isn't available in test environment + labels := s.runner.parseLabels() + assert.Equal(s.T(), "my-run", labels["tekton.dev/pipelineRun"]) +} + +func (s *tektonPipelineTestSuite) TestWorkflowFilePath() { + // Tekton doesn't have workflow file paths + assert.Equal(s.T(), "", s.runner.WorkflowFilePath()) +} + +func (s *tektonPipelineTestSuite) TestIsAuthenticated() { + // No OIDC support initially + assert.False(s.T(), s.runner.IsAuthenticated()) +} + +func (s *tektonPipelineTestSuite) TestEnvironment() { + // Should return Unknown + assert.Equal(s.T(), Unknown, s.runner.Environment()) +} + +func (s *tektonPipelineTestSuite) TestGetNamespace_NoServiceAccount() { + // Test with no service account file (normal test environment) + namespace := s.runner.getNamespace() + assert.Equal(s.T(), "", namespace) +} + +func TestTektonPipelineRunner(t *testing.T) { + suite.Run(t, new(tektonPipelineTestSuite)) +} From 08750b7e52855abbd4c36bd5531482a10f51682c Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Thu, 8 Jan 2026 09:42:08 +0100 Subject: [PATCH 2/3] remove downward API implementation Signed-off-by: Javier Rodriguez --- .../crafter/runners/tektonpipeline.go | 154 +---------- .../crafter/runners/tektonpipeline_test.go | 254 ------------------ 2 files changed, 4 insertions(+), 404 deletions(-) delete mode 100644 pkg/attestation/crafter/runners/tektonpipeline_test.go diff --git a/pkg/attestation/crafter/runners/tektonpipeline.go b/pkg/attestation/crafter/runners/tektonpipeline.go index 7f67c4b47..eb23f3103 100644 --- a/pkg/attestation/crafter/runners/tektonpipeline.go +++ b/pkg/attestation/crafter/runners/tektonpipeline.go @@ -16,29 +16,15 @@ package runners import ( - "fmt" "os" - "strings" schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" ) -const ( - // Default path for Downward API labels - defaultLabelsPath = "/etc/podinfo/labels" - // Default Tekton dashboard URL - defaultDashboardURL = "https://dashboard.tekton.dev" -) - -type TektonPipeline struct { - // Path to the Downward API labels file (configurable for testing) - labelsPath string -} +type TektonPipeline struct{} func NewTektonPipeline() *TektonPipeline { - return &TektonPipeline{ - labelsPath: defaultLabelsPath, - } + return &TektonPipeline{} } func (r *TektonPipeline) ID() schemaapi.CraftingSchema_Runner_RunnerType { @@ -55,158 +41,26 @@ func (r *TektonPipeline) CheckEnv() bool { return false } -// ListEnvVars returns environment variables collected from Downward API labels func (r *TektonPipeline) ListEnvVars() []*EnvVarDefinition { - // Parse labels and convert to environment variable definitions - labels := r.parseLabels() - if len(labels) == 0 { - return []*EnvVarDefinition{} - } - - var envVars []*EnvVarDefinition - - // Map Tekton labels to environment variables (all optional) - labelMappings := map[string]string{ - "tekton.dev/pipelineRun": "TEKTON_PIPELINE_RUN", - "tekton.dev/pipelineRunUID": "TEKTON_PIPELINE_RUN_UID", - "tekton.dev/pipeline": "TEKTON_PIPELINE", - "tekton.dev/taskRun": "TEKTON_TASKRUN_NAME", - "tekton.dev/taskRunUID": "TEKTON_TASKRUN_UID", - "tekton.dev/task": "TEKTON_TASK_NAME", - } - - for labelKey, envVarName := range labelMappings { - if _, exists := labels[labelKey]; exists { - envVars = append(envVars, &EnvVarDefinition{ - Name: envVarName, - Optional: true, - }) - } - } - - // Add namespace if available - if ns := r.getNamespace(); ns != "" { - envVars = append(envVars, &EnvVarDefinition{ - Name: "TEKTON_NAMESPACE", - Optional: true, - }) - } - - return envVars -} - -// parseLabels reads and parses the Downward API labels file -// Returns a map of label key-value pairs -func (r *TektonPipeline) parseLabels() map[string]string { - labels := make(map[string]string) - - data, err := os.ReadFile(r.labelsPath) - if err != nil { - return labels - } - - // Parse labels in format: key="value" - // Labels are separated by newlines - lines := strings.Split(string(data), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - // Split on first = to get key and quoted value - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { - continue - } - - key := strings.TrimSpace(parts[0]) - value := strings.Trim(strings.TrimSpace(parts[1]), `"`) - labels[key] = value - } - - return labels + return []*EnvVarDefinition{} } func (r *TektonPipeline) RunURI() string { - labels := r.parseLabels() - namespace := r.getNamespace() - - if namespace == "" { - return "" - } - - // Get dashboard URL from environment variable or use default - dashboardURL := os.Getenv("TEKTON_DASHBOARD_URL") - if dashboardURL == "" { - dashboardURL = defaultDashboardURL - } - - // Priority 1: If we have PipelineRun context, construct PipelineRun URL - if pipelineRun, ok := labels["tekton.dev/pipelineRun"]; ok && pipelineRun != "" { - return fmt.Sprintf("%s/#/namespaces/%s/pipelineruns/%s", dashboardURL, namespace, pipelineRun) - } - - // Priority 2: If we have TaskRun context, construct TaskRun URL - if taskRun, ok := labels["tekton.dev/taskRun"]; ok && taskRun != "" { - return fmt.Sprintf("%s/#/namespaces/%s/taskruns/%s", dashboardURL, namespace, taskRun) - } - - return "" -} - -// getNamespace attempts to get the namespace from the service account -func (r *TektonPipeline) getNamespace() string { - // Read from service account (standard Kubernetes location) - if data, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil { - return strings.TrimSpace(string(data)) - } - return "" } func (r *TektonPipeline) ResolveEnvVars() (map[string]string, []*error) { - result := make(map[string]string) - labels := r.parseLabels() - - // Map Tekton labels to environment variable names - labelMappings := map[string]string{ - "tekton.dev/pipelineRun": "TEKTON_PIPELINE_RUN", - "tekton.dev/pipelineRunUID": "TEKTON_PIPELINE_RUN_UID", - "tekton.dev/pipeline": "TEKTON_PIPELINE", - "tekton.dev/taskRun": "TEKTON_TASKRUN_NAME", - "tekton.dev/taskRunUID": "TEKTON_TASKRUN_UID", - "tekton.dev/task": "TEKTON_TASK_NAME", - } - - for labelKey, envVarName := range labelMappings { - if value, ok := labels[labelKey]; ok && value != "" { - result[envVarName] = value - } - } - - // Add namespace if available - if ns := r.getNamespace(); ns != "" { - result["TEKTON_NAMESPACE"] = ns - } - - // No errors since all variables are optional - return result, nil + return resolveEnvVars(r.ListEnvVars()) } func (r *TektonPipeline) WorkflowFilePath() string { - // Tekton doesn't have a single workflow file path concept - // Tasks and Pipelines are defined as Kubernetes resources return "" } func (r *TektonPipeline) IsAuthenticated() bool { - // No OIDC support initially return false } func (r *TektonPipeline) Environment() RunnerEnvironment { - // Could be enhanced to detect managed Tekton services (e.g., OpenShift Pipelines) - // For now, return Unknown return Unknown } diff --git a/pkg/attestation/crafter/runners/tektonpipeline_test.go b/pkg/attestation/crafter/runners/tektonpipeline_test.go deleted file mode 100644 index f7767bda9..000000000 --- a/pkg/attestation/crafter/runners/tektonpipeline_test.go +++ /dev/null @@ -1,254 +0,0 @@ -// -// Copyright 2025 The Chainloop Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package runners - -import ( - "os" - "path/filepath" - "testing" - - schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type tektonPipelineTestSuite struct { - suite.Suite - runner *TektonPipeline - tmpDir string -} - -func (s *tektonPipelineTestSuite) SetupTest() { - // Create a temporary directory for test files - tmpDir, err := os.MkdirTemp("", "tekton-test-*") - assert.NoError(s.T(), err) - s.tmpDir = tmpDir - - s.runner = NewTektonPipeline() - // Point to temp directory for tests - s.runner.labelsPath = filepath.Join(tmpDir, "labels") -} - -func (s *tektonPipelineTestSuite) TearDownTest() { - // Clean up temporary directory - if s.tmpDir != "" { - os.RemoveAll(s.tmpDir) - } -} - -func (s *tektonPipelineTestSuite) TestID() { - assert.Equal(s.T(), schemaapi.CraftingSchema_Runner_TEKTON_PIPELINE, s.runner.ID()) - assert.Equal(s.T(), "TEKTON_PIPELINE", s.runner.ID().String()) -} - -func (s *tektonPipelineTestSuite) TestCheckEnv() { - // CheckEnv should return false in normal test environment (no /tekton directory) - assert.False(s.T(), s.runner.CheckEnv()) -} - -func (s *tektonPipelineTestSuite) TestParseLabels_PipelineRun() { - // Create a labels file with PipelineRun context - labelsContent := `tekton.dev/pipeline="my-pipeline" -tekton.dev/pipelineRun="my-pipeline-run-123" -tekton.dev/pipelineRunUID="abc-123-def" -tekton.dev/pipelineTask="build-task" -tekton.dev/taskRun="my-pipeline-run-123-build-task-xyz" -tekton.dev/taskRunUID="xyz-789-uvw" -tekton.dev/task="build-task" -app.kubernetes.io/managed-by="tekton-pipelines" -` - err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) - assert.NoError(s.T(), err) - - labels := s.runner.parseLabels() - assert.Equal(s.T(), "my-pipeline", labels["tekton.dev/pipeline"]) - assert.Equal(s.T(), "my-pipeline-run-123", labels["tekton.dev/pipelineRun"]) - assert.Equal(s.T(), "abc-123-def", labels["tekton.dev/pipelineRunUID"]) - assert.Equal(s.T(), "my-pipeline-run-123-build-task-xyz", labels["tekton.dev/taskRun"]) -} - -func (s *tektonPipelineTestSuite) TestParseLabels_TaskRun() { - // Create a labels file with standalone TaskRun context - labelsContent := `tekton.dev/task="my-task" -tekton.dev/taskRun="my-taskrun-456" -tekton.dev/taskRunUID="def-456-ghi" -app.kubernetes.io/managed-by="tekton-pipelines" -` - err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) - assert.NoError(s.T(), err) - - labels := s.runner.parseLabels() - assert.Equal(s.T(), "my-task", labels["tekton.dev/task"]) - assert.Equal(s.T(), "my-taskrun-456", labels["tekton.dev/taskRun"]) - assert.Equal(s.T(), "def-456-ghi", labels["tekton.dev/taskRunUID"]) - // PipelineRun labels should not be present - _, hasPipelineRun := labels["tekton.dev/pipelineRun"] - assert.False(s.T(), hasPipelineRun) -} - -func (s *tektonPipelineTestSuite) TestParseLabels_NoFile() { - // When labels file doesn't exist, should return empty map - labels := s.runner.parseLabels() - assert.Empty(s.T(), labels) -} - -func (s *tektonPipelineTestSuite) TestListEnvVars_WithLabels() { - // Create a labels file - labelsContent := `tekton.dev/pipelineRun="my-run" -tekton.dev/taskRun="my-task-run" -` - err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) - assert.NoError(s.T(), err) - - envVars := s.runner.ListEnvVars() - assert.Greater(s.T(), len(envVars), 0) - - // All environment variables should be optional - for _, envVar := range envVars { - assert.True(s.T(), envVar.Optional, "Expected %s to be optional", envVar.Name) - } -} - -func (s *tektonPipelineTestSuite) TestListEnvVars_NoLabels() { - // When labels file doesn't exist, should return empty list - envVars := s.runner.ListEnvVars() - assert.Empty(s.T(), envVars) -} - -func (s *tektonPipelineTestSuite) TestResolveEnvVars_PipelineRun() { - // Create a labels file with PipelineRun context - labelsContent := `tekton.dev/pipeline="my-pipeline" -tekton.dev/pipelineRun="my-pipeline-run-123" -tekton.dev/pipelineRunUID="abc-123-def" -tekton.dev/taskRun="task-run-xyz" -` - err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) - assert.NoError(s.T(), err) - - resolved, errors := s.runner.ResolveEnvVars() - - assert.Nil(s.T(), errors) - assert.Equal(s.T(), "my-pipeline", resolved["TEKTON_PIPELINE"]) - assert.Equal(s.T(), "my-pipeline-run-123", resolved["TEKTON_PIPELINE_RUN"]) - assert.Equal(s.T(), "abc-123-def", resolved["TEKTON_PIPELINE_RUN_UID"]) - assert.Equal(s.T(), "task-run-xyz", resolved["TEKTON_TASKRUN_NAME"]) -} - -func (s *tektonPipelineTestSuite) TestResolveEnvVars_NoLabels() { - // When labels file doesn't exist, should return empty map with no errors - resolved, errors := s.runner.ResolveEnvVars() - - assert.Nil(s.T(), errors) - // Should be empty or only contain namespace if service account exists - assert.LessOrEqual(s.T(), len(resolved), 1) // Max 1 for namespace -} - -func (s *tektonPipelineTestSuite) TestRunURI_PipelineRun() { - // Create labels and namespace files - labelsContent := `tekton.dev/pipelineRun="my-pipeline-run-123" -` - err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) - assert.NoError(s.T(), err) - - // Mock namespace by creating service account namespace file - nsDir := filepath.Join(s.tmpDir, "run", "secrets", "kubernetes.io", "serviceaccount") - err = os.MkdirAll(nsDir, 0755) - assert.NoError(s.T(), err) - nsPath := filepath.Join(nsDir, "namespace") - err = os.WriteFile(nsPath, []byte("production"), 0600) - assert.NoError(s.T(), err) - - // Override the namespace path temporarily - // Since we can't easily mock os.ReadFile, we'll just test that labels are parsed correctly - labels := s.runner.parseLabels() - assert.Equal(s.T(), "my-pipeline-run-123", labels["tekton.dev/pipelineRun"]) - - // The actual URI construction would need namespace from service account which doesn't exist in tests - uri := s.runner.RunURI() - // Will be empty in test environment since service account file is in different location - assert.Equal(s.T(), "", uri) -} - -func (s *tektonPipelineTestSuite) TestRunURI_TaskRun() { - // Create a labels file with TaskRun only (no PipelineRun) - labelsContent := `tekton.dev/taskRun="my-taskrun-456" -` - err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) - assert.NoError(s.T(), err) - - labels := s.runner.parseLabels() - assert.Equal(s.T(), "my-taskrun-456", labels["tekton.dev/taskRun"]) -} - -func (s *tektonPipelineTestSuite) TestRunURI_PipelineRunPriority() { - // When both PipelineRun and TaskRun are present, verify labels are parsed correctly - labelsContent := `tekton.dev/pipelineRun="pipeline-run-123" -tekton.dev/taskRun="taskrun-456" -` - err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) - assert.NoError(s.T(), err) - - labels := s.runner.parseLabels() - assert.Equal(s.T(), "pipeline-run-123", labels["tekton.dev/pipelineRun"]) - assert.Equal(s.T(), "taskrun-456", labels["tekton.dev/taskRun"]) - // Priority logic is tested in RunURI implementation -} - -func (s *tektonPipelineTestSuite) TestRunURI_NoLabels() { - // Test with no labels file - uri := s.runner.RunURI() - assert.Equal(s.T(), "", uri) -} - -func (s *tektonPipelineTestSuite) TestRunURI_CustomDashboard() { - // Test custom dashboard URL via environment variable - labelsContent := `tekton.dev/pipelineRun="my-run" -` - err := os.WriteFile(s.runner.labelsPath, []byte(labelsContent), 0600) - assert.NoError(s.T(), err) - - s.T().Setenv("TEKTON_DASHBOARD_URL", "https://tekton.example.com") - - // Custom dashboard URL is tested in the implementation - // Actual URI would need namespace which isn't available in test environment - labels := s.runner.parseLabels() - assert.Equal(s.T(), "my-run", labels["tekton.dev/pipelineRun"]) -} - -func (s *tektonPipelineTestSuite) TestWorkflowFilePath() { - // Tekton doesn't have workflow file paths - assert.Equal(s.T(), "", s.runner.WorkflowFilePath()) -} - -func (s *tektonPipelineTestSuite) TestIsAuthenticated() { - // No OIDC support initially - assert.False(s.T(), s.runner.IsAuthenticated()) -} - -func (s *tektonPipelineTestSuite) TestEnvironment() { - // Should return Unknown - assert.Equal(s.T(), Unknown, s.runner.Environment()) -} - -func (s *tektonPipelineTestSuite) TestGetNamespace_NoServiceAccount() { - // Test with no service account file (normal test environment) - namespace := s.runner.getNamespace() - assert.Equal(s.T(), "", namespace) -} - -func TestTektonPipelineRunner(t *testing.T) { - suite.Run(t, new(tektonPipelineTestSuite)) -} From 0fec41893439aaa6c9e2b7212306f370c4a9a750 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Thu, 8 Jan 2026 11:23:18 +0100 Subject: [PATCH 3/3] update dates Signed-off-by: Javier Rodriguez --- app/cli/pkg/action/workflow_run_list.go | 2 +- app/cli/pkg/action/workflow_run_list_test.go | 2 +- pkg/attestation/crafter/runner.go | 2 +- pkg/attestation/crafter/runners/tektonpipeline.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/cli/pkg/action/workflow_run_list.go b/app/cli/pkg/action/workflow_run_list.go index 5d313212d..075294fbc 100644 --- a/app/cli/pkg/action/workflow_run_list.go +++ b/app/cli/pkg/action/workflow_run_list.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/app/cli/pkg/action/workflow_run_list_test.go b/app/cli/pkg/action/workflow_run_list_test.go index 1c12afc6f..fd452ef27 100644 --- a/app/cli/pkg/action/workflow_run_list_test.go +++ b/app/cli/pkg/action/workflow_run_list_test.go @@ -1,5 +1,5 @@ // -// Copyright 2023 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/attestation/crafter/runner.go b/pkg/attestation/crafter/runner.go index 2487a6fb7..cab1c1d91 100644 --- a/pkg/attestation/crafter/runner.go +++ b/pkg/attestation/crafter/runner.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/attestation/crafter/runners/tektonpipeline.go b/pkg/attestation/crafter/runners/tektonpipeline.go index eb23f3103..7b869cf06 100644 --- a/pkg/attestation/crafter/runners/tektonpipeline.go +++ b/pkg/attestation/crafter/runners/tektonpipeline.go @@ -1,5 +1,5 @@ // -// Copyright 2025 The Chainloop Authors. +// Copyright 2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License.