diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0be9e971b..da8155215 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -148,17 +148,18 @@ jobs: env: TASK_REPO: quay.io/conforma/tekton-task IMAGE_REPO: quay.io/conforma/cli - TASKS: "tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml" + TASKS: "tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml tasks/collect-keyless-params/0.1/collect-keyless-params.yaml" run: make task-bundle-snapshot TASK_REPO=$TASK_REPO TASK_TAG=$TAG ADD_TASK_TAG="$TAG_TIMESTAMP snapshot" TASKS=<( yq e ".spec.steps[].image? = \"$IMAGE_REPO:$TAG\"" $TASKS | yq 'select(. != null)') - name: Registry login (quay.io/enterprise-contract) run: podman login -u ${{ secrets.BUNDLE_PUSH_USER_EC }} -p ${{ secrets.BUNDLE_PUSH_PASS_EC }} quay.io + # This repo is deprecated and probably no one uses it any more. At some point we should stop updating it. - name: Create and push the tekton bundle (quay.io/enterprise-contract/ec-task-bundle) env: TASK_REPO: quay.io/enterprise-contract/ec-task-bundle IMAGE_REPO: quay.io/enterprise-contract/ec-cli - TASKS: "tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml" + TASKS: "tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml tasks/collect-keyless-params/0.1/collect-keyless-params.yaml" run: make task-bundle-snapshot TASK_REPO=$TASK_REPO TASK_TAG=$TAG ADD_TASK_TAG="$TAG_TIMESTAMP" TASKS=<( yq e ".spec.steps[].image? = \"$IMAGE_REPO:$TAG\"" $TASKS | yq 'select(. != null)') - name: Download statistics diff --git a/.tekton/cli-main-pull-request.yaml b/.tekton/cli-main-pull-request.yaml index e3e6068c9..3ade75d6a 100644 --- a/.tekton/cli-main-pull-request.yaml +++ b/.tekton/cli-main-pull-request.yaml @@ -298,7 +298,10 @@ spec: - name: IMAGE value: $(params.output-image).bundle - name: CONTEXT - value: tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml + value: >- + tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml + tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml + tasks/collect-keyless-params/0.1/collect-keyless-params.yaml - name: STEPS_IMAGE value: $(params.bundle-cli-ref-repo)@$(tasks.build-image-index.results.IMAGE_DIGEST) - name: URL diff --git a/.tekton/cli-main-push.yaml b/.tekton/cli-main-push.yaml index 833c6d672..8fbd77664 100644 --- a/.tekton/cli-main-push.yaml +++ b/.tekton/cli-main-push.yaml @@ -300,7 +300,10 @@ spec: - name: IMAGE value: $(params.output-image).bundle - name: CONTEXT - value: tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml + value: >- + tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml + tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml + tasks/collect-keyless-params/0.1/collect-keyless-params.yaml - name: STEPS_IMAGE value: $(params.bundle-cli-ref-repo)@$(tasks.build-image-index.results.IMAGE_DIGEST) - name: URL diff --git a/acceptance/kubernetes/kind/kubernetes.go b/acceptance/kubernetes/kind/kubernetes.go index 1752d60cf..0535ad82b 100644 --- a/acceptance/kubernetes/kind/kubernetes.go +++ b/acceptance/kubernetes/kind/kubernetes.go @@ -32,6 +32,8 @@ import ( pipeline "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" tekton "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/typed/pipeline/v1" v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + 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" @@ -189,6 +191,137 @@ func (k *kindCluster) CreateNamedSnapshot(ctx context.Context, name string, spec return k.createSnapshot(ctx, snapshot) } +// CreateConfigMap creates a ConfigMap with the given name and namespace with the provided content +// Also creates necessary RBAC permissions for cross-namespace access +func (k *kindCluster) CreateConfigMap(ctx context.Context, name, namespace, content string) error { + var data map[string]string + + // Parse JSON content and extract individual fields as ConfigMap data keys + if strings.HasPrefix(strings.TrimSpace(content), "{") { + // Parse JSON content + var jsonData map[string]interface{} + if err := json.Unmarshal([]byte(content), &jsonData); err != nil { + return fmt.Errorf("failed to parse JSON content: %w", err) + } + + // Convert to string map for ConfigMap data + data = make(map[string]string) + for key, value := range jsonData { + if value != nil { + data[key] = fmt.Sprintf("%v", value) + } + } + } else { + // For non-JSON content, store as-is under a single key + data = map[string]string{ + "content": content, + } + } + + configMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: data, + } + + // Create the ConfigMap (or update if it already exists) + if _, err := k.client.CoreV1().ConfigMaps(namespace).Create(ctx, configMap, metav1.CreateOptions{}); err != nil { + if apierrors.IsAlreadyExists(err) { + // ConfigMap exists, so get the existing one to retrieve its ResourceVersion + existing, err := k.client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get existing ConfigMap: %w", err) + } + // Set the ResourceVersion from the existing ConfigMap + configMap.ResourceVersion = existing.ResourceVersion + // Now update with the proper ResourceVersion + if _, err := k.client.CoreV1().ConfigMaps(namespace).Update(ctx, configMap, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("failed to update existing ConfigMap: %w", err) + } + } else { + return err + } + } + + // Create RBAC permissions for cross-namespace ConfigMap access + // This allows any service account to read ConfigMaps from any namespace + if err := k.ensureConfigMapRBAC(ctx); err != nil { + return fmt.Errorf("failed to create RBAC permissions: %w", err) + } + + return nil +} + +// ensureConfigMapRBAC creates necessary RBAC permissions for ConfigMap access across namespaces +func (k *kindCluster) ensureConfigMapRBAC(ctx context.Context) error { + // Create ClusterRole for ConfigMap reading (idempotent) + clusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "acceptance-configmap-reader", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"get", "list"}, + }, + }, + } + + if _, err := k.client.RbacV1().ClusterRoles().Create(ctx, clusterRole, metav1.CreateOptions{}); err != nil { + // Ignore error if ClusterRole already exists + if !strings.Contains(err.Error(), "already exists") { + return fmt.Errorf("failed to create ClusterRole: %w", err) + } + } + + // Create ClusterRoleBinding for all service accounts (idempotent) + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "acceptance-configmap-reader-binding", + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "acceptance-configmap-reader", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "Group", + Name: "system:serviceaccounts", + APIGroup: "rbac.authorization.k8s.io", + }, + }, + } + + if _, err := k.client.RbacV1().ClusterRoleBindings().Create(ctx, clusterRoleBinding, metav1.CreateOptions{}); err != nil { + // Ignore error if ClusterRoleBinding already exists + if !strings.Contains(err.Error(), "already exists") { + return fmt.Errorf("failed to create ClusterRoleBinding: %w", err) + } + } + + return nil +} + +// CreateNamedNamespace creates a namespace with the specified name +func (k *kindCluster) CreateNamedNamespace(ctx context.Context, name string) error { + _, err := k.client.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }, metav1.CreateOptions{}) + + // Ignore error if namespace already exists + if err != nil && strings.Contains(err.Error(), "already exists") { + return nil + } + + return err +} + // CreateNamespace creates a randomly-named namespace for the test to execute in // and stores it in the test context func (k *kindCluster) CreateNamespace(ctx context.Context) (context.Context, error) { diff --git a/acceptance/kubernetes/kubernetes.go b/acceptance/kubernetes/kubernetes.go index eb5507a84..56c92cba1 100644 --- a/acceptance/kubernetes/kubernetes.go +++ b/acceptance/kubernetes/kubernetes.go @@ -174,6 +174,26 @@ func createNamedSnapshot(ctx context.Context, name string, specification *godog. return c.cluster.CreateNamedSnapshot(ctx, name, specification.Content) } +func createConfigMap(ctx context.Context, name, namespace string, content *godog.DocString) error { + c := testenv.FetchState[ClusterState](ctx) + + if err := mustBeUp(ctx, *c); err != nil { + return err + } + + return c.cluster.CreateConfigMap(ctx, name, namespace, content.Content) +} + +func createNamedNamespace(ctx context.Context, name string) error { + c := testenv.FetchState[ClusterState](ctx) + + if err := mustBeUp(ctx, *c); err != nil { + return err + } + + return c.cluster.CreateNamedNamespace(ctx, name) +} + func createNamedSnapshotWithManyComponents(ctx context.Context, name string, amount int, key string) (context.Context, error) { c := testenv.FetchState[ClusterState](ctx) @@ -493,6 +513,8 @@ func AddStepsTo(sc *godog.ScenarioContext) { sc.Step(`^the task results should match the snapshot$`, taskResultsShouldMatchTheSnapshot) sc.Step(`^the task result "([^"]*)" should equal "([^"]*)"$`, taskResultShouldEqual) sc.Step(`^policy configuration named "([^"]*)" with (\d+) policy sources from "([^"]*)"(?:, patched with)$`, createNamedPolicyWithManySources) + sc.Step(`^a namespace named "([^"]*)" exists$`, createNamedNamespace) + sc.Step(`^a ConfigMap "([^"]*)" in namespace "([^"]*)" with content:$`, createConfigMap) // stop usage of the cluster once a test is done, godog will call this // function on failure and on the last step, so more than once if the // failure is not on the last step and once if there was no failure or the diff --git a/acceptance/kubernetes/stub/stub.go b/acceptance/kubernetes/stub/stub.go index e5c61e065..ca3100a2f 100644 --- a/acceptance/kubernetes/stub/stub.go +++ b/acceptance/kubernetes/stub/stub.go @@ -135,6 +135,14 @@ func (s stubCluster) CreateNamedSnapshot(ctx context.Context, name string, speci }))).WithHeaders(map[string]string{"Content-Type": "application/json"}).WithStatus(200))) } +func (s stubCluster) CreateConfigMap(_ context.Context, _, _, _ string) error { + return errors.New("ConfigMap creation is not supported when using the stub Kubernetes") +} + +func (s stubCluster) CreateNamedNamespace(_ context.Context, _ string) error { + return errors.New("Named namespace creation is not supported when using the stub Kubernetes") +} + func (s stubCluster) CreatePolicy(_ context.Context, _ string) error { return errors.New("use `Given policy configuration named \"\" with specification` when using the stub Kubernetes") } diff --git a/acceptance/kubernetes/types/types.go b/acceptance/kubernetes/types/types.go index c747a5144..4c4dd62d8 100644 --- a/acceptance/kubernetes/types/types.go +++ b/acceptance/kubernetes/types/types.go @@ -31,6 +31,8 @@ type Cluster interface { AwaitUntilTaskIsDone(context.Context) (bool, error) TaskInfo(context.Context) (*TaskInfo, error) CreateNamedSnapshot(context.Context, string, string) error + CreateNamedNamespace(context.Context, string) error + CreateConfigMap(context.Context, string, string, string) error Registry(context.Context) (string, error) BuildSnapshotArtifact(context.Context, string) (context.Context, error) } diff --git a/docs/modules/ROOT/pages/collect-keyless-params.adoc b/docs/modules/ROOT/pages/collect-keyless-params.adoc new file mode 100644 index 000000000..c5f182322 --- /dev/null +++ b/docs/modules/ROOT/pages/collect-keyless-params.adoc @@ -0,0 +1,41 @@ += collect-keyless-params + +Version: 0.1 + +== Synopsis + +Tekton task to collect Konflux configuration parameters related to +keyless signing using cosign. The task attempts to read the "cluster-config" +ConfigMap in the "konflux-info" namespace to extract signing parameters. + +In case the ConfigMap is not found, the task will output empty strings for all parameters, +allowing the pipeline to continue without signing parameters. + + +== Params +[horizontal] + +*configMapName* (`string`):: The name of the ConfigMap to read signing parameters from ++ +*Default*: `cluster-config` +*configMapNamespace* (`string`):: The namespace where the ConfigMap is located ++ +*Default*: `konflux-info` + +== Results + +[horizontal] +*keylessSigningEnabled*:: A flag indicating whether keyless signing is enabled based on the presence of signing parameters. + +*defaultOIDCIssuer*:: A default OIDC issuer URL to be used for signing. + +*buildIdentityRegexp*:: A regular expression to extract build identity from the OIDC token claims, if applicable. + +*tektonChainsIdentity*:: The Tekton Chains identity from the OIDC token claims, if applicable. + +*fulcioUrl*:: The URL of the Fulcio certificate authority. + +*rekorUrl*:: The URL of the Rekor transparency log. + +*tufUrl*:: The URL of the TUF repository. + diff --git a/docs/modules/ROOT/partials/tasks_nav.adoc b/docs/modules/ROOT/partials/tasks_nav.adoc index 6ba57f992..6c1bd1f76 100644 --- a/docs/modules/ROOT/partials/tasks_nav.adoc +++ b/docs/modules/ROOT/partials/tasks_nav.adoc @@ -1,4 +1,5 @@ * xref:tasks.adoc[Tekton Tasks] +** xref:collect-keyless-params.adoc[collect-keyless-params] ** xref:verify-conforma-konflux-ta.adoc[verify-conforma-konflux-ta] ** xref:verify-conforma-vsa-release-ta.adoc[verify-conforma-vsa-release-ta] ** xref:verify-enterprise-contract.adoc[verify-enterprise-contract] diff --git a/features/__snapshots__/task_validate_image.snap b/features/__snapshots__/task_validate_image.snap index 91a7a9e69..76541eb8b 100755 --- a/features/__snapshots__/task_validate_image.snap +++ b/features/__snapshots__/task_validate_image.snap @@ -453,3 +453,83 @@ true "TEST_OUTPUT": "{\"timestamp\":\"${TIMESTAMP}\",\"namespace\":\"\",\"successes\":5,\"failures\":0,\"warnings\":0,\"result\":\"SUCCESS\"}\n" } --- + +[Collect keyless signing parameters from ConfigMap:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config +ConfigMap found, extracting keyless signing parameters +results.keylessSigningEnabled: true +results.defaultOIDCIssuer: https://kubernetes.default.svc +results.buildIdentityRegexp: ^https://kubernetes.io/namespaces/[a-z0-9-]+-tenant/serviceaccounts/build-pipeline-[a-z0-9-]+$ +results.tektonChainsIdentity: https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller +results.fulcioUrl: https://fulcio.internal.svc +results.rekorUrl: https://rekor.internal.svc +results.tufUrl: https://tuf.internal.svc + +--- + +[Collect keyless signing parameters from ConfigMap with external url fallback:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config-1 +ConfigMap found, extracting keyless signing parameters +results.keylessSigningEnabled: true +results.defaultOIDCIssuer: https://kubernetes.default.svc +results.buildIdentityRegexp: ^https://kubernetes.io/namespaces/[a-z0-9-]+-tenant/serviceaccounts/build-pipeline-[a-z0-9-]+$ +results.tektonChainsIdentity: https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller +results.fulcioUrl: https://fulcio.example.com +results.rekorUrl: https://rekor.example.com +results.tufUrl: https://tuf.example.com + +--- + +[Collect keyless signing parameters from ConfigMap with keyless signing disabled:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config-2 +ConfigMap found, extracting keyless signing parameters +enableKeylessSigning is not set, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.buildIdentityRegexp: +results.tektonChainsIdentity: +results.fulcioUrl: +results.rekorUrl: +results.tufUrl: + +--- + +[Collect keyless signing parameters when there is a malformed ConfigMap:collect-signing-params - 1] +Reading ConfigMap konflux-info/cluster-config-3 +ConfigMap found, extracting keyless signing parameters +enableKeylessSigning is not set, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.buildIdentityRegexp: +results.tektonChainsIdentity: +results.fulcioUrl: +results.rekorUrl: +results.tufUrl: + +--- + +[Collect keyless signing parameters when the ConfigMap does not exist:collect-signing-params - 1] +Reading ConfigMap konflux-info/doesnt-exist-config +ConfigMap not found, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.buildIdentityRegexp: +results.tektonChainsIdentity: +results.fulcioUrl: +results.rekorUrl: +results.tufUrl: + +--- + +[Collect keyless signing parameters when the namespace does not exist:collect-signing-params - 1] +Reading ConfigMap doesnt-exist-namespace/whatever +ConfigMap not found, using default empty values +results.keylessSigningEnabled: false +results.defaultOIDCIssuer: +results.buildIdentityRegexp: +results.tektonChainsIdentity: +results.fulcioUrl: +results.rekorUrl: +results.tufUrl: + +--- diff --git a/features/task_validate_image.feature b/features/task_validate_image.feature index de205d33b..d5204e1a0 100644 --- a/features/task_validate_image.feature +++ b/features/task_validate_image.feature @@ -412,3 +412,158 @@ Feature: Verify Enterprise Contract Tekton Tasks Then the task should succeed And the task logs for step "report-json" should match the snapshot And the task results should match the snapshot + + Scenario: Collect keyless signing parameters from ConfigMap + Given a working namespace + And a namespace named "konflux-info" exists + # See realistic data here: + # https://github.com/redhat-appstudio/tsf-cli/blob/84561ca6c9/installer/charts/tsf-konflux/templates/konflux.yaml#L51-L65 + # Note: These scenarios might run in parallel so let's use a different config map + # for each scenario so we don't have to worry about them clashing with each other + And a ConfigMap "cluster-config" in namespace "konflux-info" with content: + # tufExternalUrl should be ignored here because tufInternalUrl takes precedence + ``` + { + "enableKeylessSigning": true, + "defaultOIDCIssuer": "https://kubernetes.default.svc", + "buildIdentityRegexp": "^https://kubernetes.io/namespaces/[a-z0-9-]+-tenant/serviceaccounts/build-pipeline-[a-z0-9-]+$", + "tektonChainsIdentity": "https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller", + "fulcioInternalUrl": "https://fulcio.internal.svc", + "rekorInternalUrl": "https://rekor.internal.svc", + "tufInternalUrl": "https://tuf.internal.svc", + "tufExternalUrl": "https://tuf.example.com" + } + ``` + When version 0.1 of the task named "collect-keyless-params" is run with parameters: + | configMapName | cluster-config | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "keylessSigningEnabled" should equal "true" + And the task result "defaultOIDCIssuer" should equal "https://kubernetes.default.svc" + And the task result "buildIdentityRegexp" should equal "^https://kubernetes.io/namespaces/[a-z0-9-]+-tenant/serviceaccounts/build-pipeline-[a-z0-9-]+$" + And the task result "tektonChainsIdentity" should equal "https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller" + And the task result "fulcioUrl" should equal "https://fulcio.internal.svc" + And the task result "rekorUrl" should equal "https://rekor.internal.svc" + And the task result "tufUrl" should equal "https://tuf.internal.svc" + + Scenario: Collect keyless signing parameters from ConfigMap with external url fallback + Given a working namespace + And a namespace named "konflux-info" exists + # Note: These scenarios might run in parallel so let's use a different config map + # for each scenario so we don't have to worry about them clashing with each other + And a ConfigMap "cluster-config-1" in namespace "konflux-info" with content: + # fulcioInternalUrl should be ignored here because it's blank + ``` + { + "enableKeylessSigning": true, + "defaultOIDCIssuer": "https://kubernetes.default.svc", + "buildIdentityRegexp": "^https://kubernetes.io/namespaces/[a-z0-9-]+-tenant/serviceaccounts/build-pipeline-[a-z0-9-]+$", + "tektonChainsIdentity": "https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller", + "fulcioInternalUrl": "", + "fulcioExternalUrl": "https://fulcio.example.com", + "rekorExternalUrl": "https://rekor.example.com", + "tufExternalUrl": "https://tuf.example.com" + } + ``` + When version 0.1 of the task named "collect-keyless-params" is run with parameters: + | configMapName | cluster-config-1 | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "keylessSigningEnabled" should equal "true" + And the task result "defaultOIDCIssuer" should equal "https://kubernetes.default.svc" + And the task result "buildIdentityRegexp" should equal "^https://kubernetes.io/namespaces/[a-z0-9-]+-tenant/serviceaccounts/build-pipeline-[a-z0-9-]+$" + And the task result "tektonChainsIdentity" should equal "https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller" + And the task result "fulcioUrl" should equal "https://fulcio.example.com" + And the task result "rekorUrl" should equal "https://rekor.example.com" + And the task result "tufUrl" should equal "https://tuf.example.com" + + Scenario: Collect keyless signing parameters from ConfigMap with keyless signing disabled + Given a working namespace + And a namespace named "konflux-info" exists + # Note: These scenarios might run in parallel so let's use a different config map + # for each scenario so we don't have to worry about them clashing with each other + And a ConfigMap "cluster-config-2" in namespace "konflux-info" with content: + # Because enableKeylessSigning is false, all the other values are ignored here + ``` + { + "enableKeylessSigning": false, + "defaultOIDCIssuer": "https://kubernetes.default.svc", + "buildIdentityRegexp": "^https://kubernetes.io/namespaces/[a-z0-9-]+-tenant/serviceaccounts/build-pipeline-[a-z0-9-]+$", + "tektonChainsIdentity": "https://kubernetes.io/namespaces/openshift-pipelines/serviceaccounts/tekton-chains-controller", + "fulcioInternalUrl": "https://fulcio.internal.svc", + "rekorExternalUrl": "https://rekor.example.com", + "tufExternalUrl": "https://tuf.example.com" + } + ``` + When version 0.1 of the task named "collect-keyless-params" is run with parameters: + | configMapName | cluster-config-2 | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "keylessSigningEnabled" should equal "false" + And the task result "defaultOIDCIssuer" should equal "" + And the task result "buildIdentityRegexp" should equal "" + And the task result "tektonChainsIdentity" should equal "" + And the task result "fulcioUrl" should equal "" + And the task result "rekorUrl" should equal "" + And the task result "tufUrl" should equal "" + + Scenario: Collect keyless signing parameters when there is a malformed ConfigMap + Given a working namespace + And a namespace named "konflux-info" exists + # Note: These scenarios might run in parallel so let's use a different config map + # for each scenario so we don't have to worry about them clashing with each other + And a ConfigMap "cluster-config-3" in namespace "konflux-info" with content: + ``` + {"foo": "bar"} + ``` + When version 0.1 of the task named "collect-keyless-params" is run with parameters: + | configMapName | cluster-config-3 | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "keylessSigningEnabled" should equal "false" + And the task result "defaultOIDCIssuer" should equal "" + And the task result "buildIdentityRegexp" should equal "" + And the task result "tektonChainsIdentity" should equal "" + And the task result "fulcioUrl" should equal "" + And the task result "rekorUrl" should equal "" + And the task result "tufUrl" should equal "" + + Scenario: Collect keyless signing parameters when the ConfigMap does not exist + Given a working namespace + And a namespace named "konflux-info" exists + # Note: These scenarios might run in parallel so let's use a different config map + # for each scenario so we don't have to worry about them clashing with each other. + # Creating a config map deliberately so we are sure the rbac is created. (I might + # be wrong but I think it could matter if this secenario runs before any of the + # others.) + And a ConfigMap "cluster-config-4" in namespace "konflux-info" with content: + ``` + {"foo": "bar"} + ``` + When version 0.1 of the task named "collect-keyless-params" is run with parameters: + | configMapNamespace | konflux-info | + | configMapName | doesnt-exist-config | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "keylessSigningEnabled" should equal "false" + And the task result "defaultOIDCIssuer" should equal "" + And the task result "buildIdentityRegexp" should equal "" + And the task result "tektonChainsIdentity" should equal "" + And the task result "fulcioUrl" should equal "" + And the task result "rekorUrl" should equal "" + And the task result "tufUrl" should equal "" + + Scenario: Collect keyless signing parameters when the namespace does not exist + Given a working namespace + When version 0.1 of the task named "collect-keyless-params" is run with parameters: + | configMapNamespace | doesnt-exist-namespace | + | configMapName | whatever | + Then the task should succeed + And the task logs for step "collect-signing-params" should match the snapshot + And the task result "keylessSigningEnabled" should equal "false" + And the task result "defaultOIDCIssuer" should equal "" + And the task result "buildIdentityRegexp" should equal "" + And the task result "tektonChainsIdentity" should equal "" + And the task result "fulcioUrl" should equal "" + And the task result "rekorUrl" should equal "" + And the task result "tufUrl" should equal "" diff --git a/tasks/collect-keyless-params/0.1/collect-keyless-params.yaml b/tasks/collect-keyless-params/0.1/collect-keyless-params.yaml new file mode 100644 index 000000000..e8a2dacef --- /dev/null +++ b/tasks/collect-keyless-params/0.1/collect-keyless-params.yaml @@ -0,0 +1,189 @@ +--- +# Copyright The Conforma Contributors +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: collect-keyless-params + annotations: + tekton.dev/displayName: Collect Keyless Signing Parameters + tekton.dev/pipelines.minVersion: "0.12.1" + tekton.dev/tags: ec, keyless, signing, configuration + labels: + app.kubernetes.io/version: "0.1" + +spec: + description: | + Tekton task to collect Konflux configuration parameters related to + keyless signing using cosign. The task attempts to read the "cluster-config" + ConfigMap in the "konflux-info" namespace to extract signing parameters. + + In case the ConfigMap is not found, the task will output empty strings for all parameters, + allowing the pipeline to continue without signing parameters. + + params: + - name: configMapName + type: string + description: The name of the ConfigMap to read signing parameters from + default: cluster-config + + - name: configMapNamespace + type: string + description: The namespace where the ConfigMap is located + default: konflux-info + + results: + - name: keylessSigningEnabled + type: string + description: | + A flag indicating whether keyless signing is enabled based on the presence of signing parameters. + + - name: defaultOIDCIssuer + type: string + description: | + A default OIDC issuer URL to be used for signing. + + - name: buildIdentityRegexp + type: string + description: | + A regular expression to extract build identity from the OIDC token claims, if applicable. + + - name: tektonChainsIdentity + type: string + description: | + The Tekton Chains identity from the OIDC token claims, if applicable. + + - name: fulcioUrl + type: string + description: | + The URL of the Fulcio certificate authority. + + - name: rekorUrl + type: string + description: | + The URL of the Rekor transparency log. + + - name: tufUrl + type: string + description: | + The URL of the TUF repository. + + stepTemplate: + securityContext: + runAsUser: 1001 + + steps: + - name: collect-signing-params + image: quay.io/conforma/cli:latest + + computeResources: + limits: + memory: 128Mi + cpu: 50m + requests: + memory: 128Mi + cpu: 50m + + env: + - name: configMapNamespace + value: "$(params.configMapNamespace)" + - name: configMapName + value: "$(params.configMapName)" + + script: | + #!/bin/bash + set -euo pipefail + + # Default value is "false" + keylessSigningEnabled="false" + + # Default values are empty strings + defaultOIDCIssuer="" + buildIdentityRegexp="" + tektonChainsIdentity="" + fulcioUrl="" + rekorUrl="" + tufUrl="" + + # Read from the ConfigMap + echo "Reading ConfigMap ${configMapNamespace}/${configMapName}" + KFLX_CONFIG_PATH='/tmp/cluster-config.json' + + if kubectl get configmap "${configMapName}" -n "${configMapNamespace}" -o json --ignore-not-found > "${KFLX_CONFIG_PATH}"; then + if [ -s "${KFLX_CONFIG_PATH}" ]; then + echo "ConfigMap found, extracting keyless signing parameters" + + # First we read "keylessSigningEnabled" + keylessSigningEnabled=$(jq -r '.data.enableKeylessSigning // "false"' "$KFLX_CONFIG_PATH") + + if [ "$keylessSigningEnabled" = "true" ]; then + # If that is set to "true" then read the other values + defaultOIDCIssuer=$(jq -r '.data.defaultOIDCIssuer // ""' "$KFLX_CONFIG_PATH") + buildIdentityRegexp=$(jq -r '.data.buildIdentityRegexp // ""' "$KFLX_CONFIG_PATH") + tektonChainsIdentity=$(jq -r '.data.tektonChainsIdentity // ""' "$KFLX_CONFIG_PATH") + + # For each of these we prefer the internal url if its present + fulcioUrl=$(jq -r '.data.fulcioInternalUrl // ""' "$KFLX_CONFIG_PATH") + if [ -z "$fulcioUrl" ]; then + fulcioUrl=$(jq -r '.data.fulcioExternalUrl // ""' "$KFLX_CONFIG_PATH") + fi + + rekorUrl=$(jq -r '.data.rekorInternalUrl // ""' "$KFLX_CONFIG_PATH") + if [ -z "$rekorUrl" ]; then + rekorUrl=$(jq -r '.data.rekorExternalUrl // ""' "$KFLX_CONFIG_PATH") + fi + + tufUrl=$(jq -r '.data.tufInternalUrl // ""' "$KFLX_CONFIG_PATH") + if [ -z "$tufUrl" ]; then + tufUrl=$(jq -r '.data.tufExternalUrl // ""' "$KFLX_CONFIG_PATH") + fi + + else + # Otherwise we ignore the rest of the ConfigMap + echo "enableKeylessSigning is not set, using default empty values" + + fi + else + # Because we used --ignore-not-found this doesn't produce an error + echo "ConfigMap not found, using default empty values" + + fi + + else + # Some error other than "not found" + # (Stderr from kubectl should be visible in the task log.) + echo "Problem reading ConfigMap, using default empty values" + + fi + + # Write to task results + echo -n "$keylessSigningEnabled" > "$(results.keylessSigningEnabled.path)" + echo -n "$defaultOIDCIssuer" > "$(results.defaultOIDCIssuer.path)" + echo -n "$buildIdentityRegexp" > "$(results.buildIdentityRegexp.path)" + echo -n "$tektonChainsIdentity" > "$(results.tektonChainsIdentity.path)" + echo -n "$fulcioUrl" > "$(results.fulcioUrl.path)" + echo -n "$rekorUrl" > "$(results.rekorUrl.path)" + echo -n "$tufUrl" > "$(results.tufUrl.path)" + + # Output for troubleshooting/debugging + echo "results.keylessSigningEnabled: $keylessSigningEnabled" + echo "results.defaultOIDCIssuer: $defaultOIDCIssuer" + echo "results.buildIdentityRegexp: $buildIdentityRegexp" + echo "results.tektonChainsIdentity: $tektonChainsIdentity" + echo "results.fulcioUrl: $fulcioUrl" + echo "results.rekorUrl: $rekorUrl" + echo "results.tufUrl: $tufUrl"