From 765bf8a5b495b56b31e4dc86fd8a784e15547155 Mon Sep 17 00:00:00 2001 From: Chuang Wang Date: Tue, 25 Oct 2022 14:35:38 -0700 Subject: [PATCH] Set ConfigSource in clusterresolver Related to https://github.com/tektoncd/pipeline/issues/5522 Prior, a field named Source was introduced to ResolutionRequest status to record the source where the remote resource came from. And the individual resolvers need to implement the Source function to set the correct source value. But the method in clusterresolver returns a nil value. Now, we return correct source value with the 3 subfields: url, digest and entrypoint - url: [Kubernetes CRD namespace-scoped resource URI](https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-uris) appended with UID. Example: /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME@UID. - digest: hex-encoded sha256 checksum of the in-cluster resource - entrypoint: ***empty*** because the path is already available in url field. Signed-off-by: Chuang Wang --- docs/cluster-resolver.md | 54 ++++++++++ pkg/resolution/resolver/cluster/resolver.go | 52 +++++++-- .../resolver/cluster/resolver_test.go | 100 ++++++++++++++---- 3 files changed, 182 insertions(+), 24 deletions(-) diff --git a/docs/cluster-resolver.md b/docs/cluster-resolver.md index daaab693396..3e39702a0be 100644 --- a/docs/cluster-resolver.md +++ b/docs/cluster-resolver.md @@ -74,6 +74,60 @@ spec: value: namespace-containing-pipeline ``` +## `ResolutionRequest` Status +`ResolutionRequest.Status.Source` field captures the source where the remote resource came from. It includes the 3 subfields: `url`, `digest` and `entrypoint`. +- `url`: url is the unique full identifier for the resource in the cluster. It is in the format of `@`. Resource URI part is the namespace-scoped uri i.e. `/apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME`. See [K8s Resource URIs](https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-uris) for more details. +- `digest`: hex-encoded sha256 checksum of the content in the in-cluster resource's spec field. The reason why it's the checksum of the spec content rather than the whole object is because the metadata of in-cluster resources might be modified i.e. annotations. Therefore, the checksum of the spec content should be sufficient for source verifiers to verify if things have been changed maliciously even though the metadata is modified with good intentions. +- `entrypoint`: ***empty*** because the path information is already available in the url field. + +Example: +- TaskRun Resolution + +```yaml +apiVersion: tekton.dev/v1beta1 +kind: TaskRun +metadata: + name: cluster-demo +spec: + taskRef: + resolver: cluster + params: + - name: kind + value: task + - name: name + value: a-simple-task + - name: namespace + value: default +``` + + +- `ResolutionRequest` +```yaml +apiVersion: resolution.tekton.dev/v1beta1 +kind: ResolutionRequest +metadata: + labels: + resolution.tekton.dev/type: cluster + name: cluster-7a04be6baa3eeedd232542036b7f3b2d + namespace: default + ownerReferences: ... +spec: + params: + - name: kind + value: task + - name: name + value: a-simple-task + - name: namespace + value: default +status: + annotations: ... + conditions: ... + data: xxx + source: + digest: + sha256: 245b1aa918434cc8195b4d4d026f2e43df09199e2ed31d4dfd9c2cbea1c7ce54 + uri: /apis/tekton.dev/v1beta1/namespaces/default/task/a-simple-task@3b82d8c4-f89e-47ea-a49d-3be0dca4c038 +``` --- Except as otherwise noted, the content of this page is licensed under the diff --git a/pkg/resolution/resolver/cluster/resolver.go b/pkg/resolution/resolver/cluster/resolver.go index b2ff8e14d38..69cc10757bf 100644 --- a/pkg/resolution/resolver/cluster/resolver.go +++ b/pkg/resolution/resolver/cluster/resolver.go @@ -18,11 +18,14 @@ package cluster import ( "context" + "crypto/sha256" + "encoding/hex" "errors" "fmt" "strings" resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" clientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" pipelineclient "github.com/tektoncd/pipeline/pkg/client/injection/client" @@ -101,6 +104,8 @@ func (r *Resolver) Resolve(ctx context.Context, origParams []pipelinev1beta1.Par } var data []byte + var spec []byte + var uid string groupVersion := pipelinev1beta1.SchemeGroupVersion.String() switch params[KindParam] { @@ -110,6 +115,7 @@ func (r *Resolver) Resolve(ctx context.Context, origParams []pipelinev1beta1.Par logger.Infof("failed to load task %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) return nil, err } + uid = string(task.UID) task.Kind = "Task" task.APIVersion = groupVersion data, err = yaml.Marshal(task) @@ -117,12 +123,19 @@ func (r *Resolver) Resolve(ctx context.Context, origParams []pipelinev1beta1.Par logger.Infof("failed to marshal task %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) return nil, err } + + spec, err = yaml.Marshal(task.Spec) + if err != nil { + logger.Infof("failed to marshal the spec of the task %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) + return nil, err + } case "pipeline": pipeline, err := r.pipelineClientSet.TektonV1beta1().Pipelines(params[NamespaceParam]).Get(ctx, params[NameParam], metav1.GetOptions{}) if err != nil { logger.Infof("failed to load pipeline %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) return nil, err } + uid = string(pipeline.UID) pipeline.Kind = "Pipeline" pipeline.APIVersion = groupVersion data, err = yaml.Marshal(pipeline) @@ -130,15 +143,23 @@ func (r *Resolver) Resolve(ctx context.Context, origParams []pipelinev1beta1.Par logger.Infof("failed to marshal pipeline %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) return nil, err } + + spec, err = yaml.Marshal(pipeline.Spec) + if err != nil { + logger.Infof("failed to marshal the spec of the pipeline %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) + return nil, err + } default: logger.Infof("unknown or invalid resource kind %s", params[KindParam]) return nil, fmt.Errorf("unknown or invalid resource kind %s", params[KindParam]) } return &ResolvedClusterResource{ - Content: data, - Name: params[NameParam], - Namespace: params[NamespaceParam], + Content: data, + Spec: spec, + Name: params[NameParam], + Namespace: params[NamespaceParam], + Identifier: fmt.Sprintf("/apis/%s/namespaces/%s/%s/%s@%s", groupVersion, params[NamespaceParam], params[KindParam], params[NameParam], uid), }, nil } @@ -161,9 +182,19 @@ func (r *Resolver) isDisabled(ctx context.Context) bool { // ResolvedClusterResource implements framework.ResolvedResource and returns // the resolved file []byte data and an annotation map for any metadata. type ResolvedClusterResource struct { - Content []byte - Name string + // Content is the actual resolved resource data. + Content []byte + // Spec is the data in the resolved task/pipeline CRD spec. + Spec []byte + // Name is the resolved resource name in the cluster + Name string + // Namespace is the namespace in the cluster under which the resolved resource was created. Namespace string + // Identifier is the unique identifier for the resource in the cluster. + // It is in the format of @. + // Resource URI is the namespace-scoped uri i.e. /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME. + // https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-uris + Identifier string } var _ framework.ResolvedResource = &ResolvedClusterResource{} @@ -184,7 +215,16 @@ func (r *ResolvedClusterResource) Annotations() map[string]string { // Source is the source reference of the remote data that records where the remote // file came from including the url, digest and the entrypoint. func (r ResolvedClusterResource) Source() *pipelinev1beta1.ConfigSource { - return nil + h := sha256.New() + h.Write(r.Spec) + sha256CheckSum := hex.EncodeToString(h.Sum(nil)) + + return &v1beta1.ConfigSource{ + URI: r.Identifier, + Digest: map[string]string{ + "sha256": sha256CheckSum, + }, + } } func populateParamsWithDefaults(ctx context.Context, origParams []pipelinev1beta1.Param) (map[string]string, error) { diff --git a/pkg/resolution/resolver/cluster/resolver_test.go b/pkg/resolution/resolver/cluster/resolver_test.go index 187a2c9f79b..059e0070ced 100644 --- a/pkg/resolution/resolver/cluster/resolver_test.go +++ b/pkg/resolution/resolver/cluster/resolver_test.go @@ -19,6 +19,9 @@ package cluster import ( "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" "errors" "testing" "time" @@ -36,6 +39,7 @@ import ( "github.com/tektoncd/pipeline/test/diff" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + duckv1 "knative.dev/pkg/apis/duck/v1" "knative.dev/pkg/system" "sigs.k8s.io/yaml" @@ -188,6 +192,7 @@ func TestResolve(t *testing.T) { Name: "example-task", Namespace: "task-ns", ResourceVersion: "00002", + UID: "a123", }, TypeMeta: metav1.TypeMeta{ Kind: string(pipelinev1beta1.NamespacedTaskKind), @@ -205,12 +210,17 @@ func TestResolve(t *testing.T) { if err != nil { t.Fatalf("couldn't marshal task: %v", err) } + taskSpec, err := yaml.Marshal(exampleTask.Spec) + if err != nil { + t.Fatalf("couldn't marshal task spec: %v", err) + } examplePipeline := &pipelinev1beta1.Pipeline{ ObjectMeta: metav1.ObjectMeta{ Name: "example-pipeline", Namespace: defaultNS, ResourceVersion: "00001", + UID: "b123", }, TypeMeta: metav1.TypeMeta{ Kind: "Pipeline", @@ -230,6 +240,10 @@ func TestResolve(t *testing.T) { if err != nil { t.Fatalf("couldn't marshal pipeline: %v", err) } + pipelineSpec, err := yaml.Marshal(examplePipeline.Spec) + if err != nil { + t.Fatalf("couldn't marshal pipeline spec: %v", err) + } testCases := []struct { name string @@ -242,27 +256,71 @@ func TestResolve(t *testing.T) { expectedErr error }{ { - name: "successful task", - kind: "task", - resourceName: exampleTask.Name, - namespace: exampleTask.Namespace, - expectedStatus: internal.CreateResolutionRequestStatusWithData(taskAsYAML), + name: "successful task", + kind: "task", + resourceName: exampleTask.Name, + namespace: exampleTask.Namespace, + expectedStatus: &v1beta1.ResolutionRequestStatus{ + Status: duckv1.Status{}, + ResolutionRequestStatusFields: v1beta1.ResolutionRequestStatusFields{ + Data: base64.StdEncoding.Strict().EncodeToString(taskAsYAML), + Source: &pipelinev1beta1.ConfigSource{ + URI: "/apis/tekton.dev/v1beta1/namespaces/task-ns/task/example-task@a123", + Digest: map[string]string{ + "sha256": sha256CheckSum(taskSpec), + }, + }, + }, + }, }, { - name: "successful pipeline", - kind: "pipeline", - resourceName: examplePipeline.Name, - namespace: examplePipeline.Namespace, - expectedStatus: internal.CreateResolutionRequestStatusWithData(pipelineAsYAML), + name: "successful pipeline", + kind: "pipeline", + resourceName: examplePipeline.Name, + namespace: examplePipeline.Namespace, + expectedStatus: &v1beta1.ResolutionRequestStatus{ + Status: duckv1.Status{}, + ResolutionRequestStatusFields: v1beta1.ResolutionRequestStatusFields{ + Data: base64.StdEncoding.Strict().EncodeToString(pipelineAsYAML), + Source: &pipelinev1beta1.ConfigSource{ + URI: "/apis/tekton.dev/v1beta1/namespaces/pipeline-ns/pipeline/example-pipeline@b123", + Digest: map[string]string{ + "sha256": sha256CheckSum(pipelineSpec), + }, + }, + }, + }, }, { - name: "default namespace", - kind: "pipeline", - resourceName: examplePipeline.Name, - expectedStatus: internal.CreateResolutionRequestStatusWithData(pipelineAsYAML), + name: "default namespace", + kind: "pipeline", + resourceName: examplePipeline.Name, + expectedStatus: &v1beta1.ResolutionRequestStatus{ + Status: duckv1.Status{}, + ResolutionRequestStatusFields: v1beta1.ResolutionRequestStatusFields{ + Data: base64.StdEncoding.Strict().EncodeToString(pipelineAsYAML), + Source: &pipelinev1beta1.ConfigSource{ + URI: "/apis/tekton.dev/v1beta1/namespaces/pipeline-ns/pipeline/example-pipeline@b123", + Digest: map[string]string{ + "sha256": sha256CheckSum(pipelineSpec), + }, + }, + }, + }, }, { - name: "default kind", - resourceName: exampleTask.Name, - namespace: exampleTask.Namespace, - expectedStatus: internal.CreateResolutionRequestStatusWithData(taskAsYAML), + name: "default kind", + resourceName: exampleTask.Name, + namespace: exampleTask.Namespace, + expectedStatus: &v1beta1.ResolutionRequestStatus{ + Status: duckv1.Status{}, + ResolutionRequestStatusFields: v1beta1.ResolutionRequestStatusFields{ + Data: base64.StdEncoding.Strict().EncodeToString(taskAsYAML), + Source: &pipelinev1beta1.ConfigSource{ + URI: "/apis/tekton.dev/v1beta1/namespaces/task-ns/task/example-task@a123", + Digest: map[string]string{ + "sha256": sha256CheckSum(taskSpec), + }, + }, + }, + }, }, { name: "no such task", kind: "task", @@ -407,3 +465,9 @@ func createRequest(kind, name, namespace string) *v1beta1.ResolutionRequest { func resolverContext() context.Context { return frtesting.ContextWithClusterResolverEnabled(context.Background()) } + +func sha256CheckSum(input []byte) string { + h := sha256.New() + h.Write(input) + return hex.EncodeToString(h.Sum(nil)) +}