Skip to content

Commit

Permalink
Sync checksum between trusted resources and cluster resolver
Browse files Browse the repository at this point in the history
Prior to this, trusted resources computed the checksum of the task/pipeline object (after some preprocessing ).
However, the cluster resolver computed the checksum of the Spec instead of the pre-processed object, which made it incomplete.

This PR syncs up the checksum computation of the task/pipeline in the cluster resolver to be the same as what trusted resources does i.e. step1: preprocessing, step2: compute checksum of the object. It addresses issue #6958
  • Loading branch information
chitrangpatel authored and tekton-robot committed Aug 1, 2023
1 parent 51bfe94 commit 1f5c137
Show file tree
Hide file tree
Showing 15 changed files with 611 additions and 300 deletions.
57 changes: 57 additions & 0 deletions pkg/apis/pipeline/internal/checksum/checksum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package checksum

import (
"crypto/sha256"
"encoding/json"
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
// SignatureAnnotation is the key of signature in annotation map
SignatureAnnotation = "tekton.dev/signature"
)

// PrepareObjectMeta will remove annotations not configured from user side -- "kubectl-client-side-apply" and "kubectl.kubernetes.io/last-applied-configuration"
// (added when an object is created with `kubectl apply`) to avoid verification failure and extract the signature.
// Returns a copy of the input object metadata with the annotations removed and the object's signature,
// if it is present in the metadata.
func PrepareObjectMeta(in metav1.Object) metav1.ObjectMeta {
outMeta := metav1.ObjectMeta{}

// exclude the fields populated by system.
outMeta.Name = in.GetName()
outMeta.GenerateName = in.GetGenerateName()
outMeta.Namespace = in.GetNamespace()

if in.GetLabels() != nil {
outMeta.Labels = make(map[string]string)
for k, v := range in.GetLabels() {
outMeta.Labels[k] = v
}
}

outMeta.Annotations = make(map[string]string)
for k, v := range in.GetAnnotations() {
outMeta.Annotations[k] = v
}

// exclude the annotations added by other components
delete(outMeta.Annotations, "kubectl-client-side-apply")
delete(outMeta.Annotations, "kubectl.kubernetes.io/last-applied-configuration")
delete(outMeta.Annotations, SignatureAnnotation)

return outMeta
}

// ComputeSha256Checksum computes the sha256 checksum of the tekton object.
func ComputeSha256Checksum(obj interface{}) ([]byte, error) {
ts, err := json.Marshal(obj)
if err != nil {
return nil, fmt.Errorf("failed to marshal the object: %w", err)
}
h := sha256.New()
h.Write(ts)
return h.Sum(nil), nil
}
103 changes: 103 additions & 0 deletions pkg/apis/pipeline/internal/checksum/checksum_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
Copyright 2022 The Tekton 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 checksum

import (
"encoding/hex"
"testing"

"github.com/google/go-cmp/cmp"

"github.com/tektoncd/pipeline/test/diff"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestPrepareObjectMeta(t *testing.T) {
unsigned := metav1.ObjectMeta{
Name: "test-task",
Annotations: map[string]string{"foo": "bar"},
}
namespace := ""
signed := unsigned.DeepCopy()
sig := "tY805zV53PtwDarK3VD6dQPx5MbIgctNcg/oSle+MG0="
signed.Annotations = map[string]string{SignatureAnnotation: sig}

signedWithLabels := signed.DeepCopy()
signedWithLabels.Labels = map[string]string{"label": "foo"}

signedWithExtraAnnotations := signed.DeepCopy()
signedWithExtraAnnotations.Annotations["kubectl-client-side-apply"] = "client"
signedWithExtraAnnotations.Annotations["kubectl.kubernetes.io/last-applied-configuration"] = "config"

tcs := []struct {
name string
objectmeta *metav1.ObjectMeta
expected metav1.ObjectMeta
}{{
name: "Prepare signed objectmeta without labels",
objectmeta: signed,
expected: metav1.ObjectMeta{
Name: "test-task",
Namespace: namespace,
Annotations: map[string]string{},
},
}, {
name: "Prepare signed objectmeta with labels",
objectmeta: signedWithLabels,
expected: metav1.ObjectMeta{
Name: "test-task",
Namespace: namespace,
Labels: map[string]string{"label": "foo"},
Annotations: map[string]string{},
},
}, {
name: "Prepare signed objectmeta with extra annotations",
objectmeta: signedWithExtraAnnotations,
expected: metav1.ObjectMeta{
Name: "test-task",
Namespace: namespace,
Annotations: map[string]string{},
},
}, {
name: "resource without signature shouldn't fail",
objectmeta: &unsigned,
expected: metav1.ObjectMeta{
Name: "test-task",
Namespace: namespace,
Annotations: map[string]string{"foo": "bar"},
},
}}

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
task := PrepareObjectMeta(tc.objectmeta)
if d := cmp.Diff(task, tc.expected); d != "" {
t.Error(diff.PrintWantGot(d))
}
})
}
}

func TestComputeSha256Checksum(t *testing.T) {
sha, err := ComputeSha256Checksum("hello")
if err != nil {
t.Fatalf("Could not marshal hello %v", err)
}
if d := cmp.Diff(hex.EncodeToString(sha), "5aa762ae383fbb727af3c7a36d4940a5b8c40a989452d2304fc958ff3f354e7a"); d != "" {
t.Error(diff.PrintWantGot(d))
}
}
22 changes: 22 additions & 0 deletions pkg/apis/pipeline/v1/pipeline_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package v1

import (
"github.com/tektoncd/pipeline/pkg/apis/pipeline"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/internal/checksum"
"github.com/tektoncd/pipeline/pkg/reconciler/pipeline/dag"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -70,6 +71,27 @@ func (*Pipeline) GetGroupVersionKind() schema.GroupVersionKind {
return SchemeGroupVersion.WithKind(pipeline.PipelineControllerName)
}

// Checksum computes the sha256 checksum of the pipeline object.
// Prior to computing the checksum, it performs some preprocessing on the
// metadata of the object where it removes system provided annotations.
// Only the name, namespace, generateName, user-provided labels and annotations
// and the pipelineSpec are included for the checksum computation.
func (p *Pipeline) Checksum() ([]byte, error) {
objectMeta := checksum.PrepareObjectMeta(p)
preprocessedPipeline := Pipeline{
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1",
Kind: "Pipeline"},
ObjectMeta: objectMeta,
Spec: p.Spec,
}
sha256Checksum, err := checksum.ComputeSha256Checksum(preprocessedPipeline)
if err != nil {
return nil, err
}
return sha256Checksum, nil
}

// PipelineSpec defines the desired state of Pipeline.
type PipelineSpec struct {
// DisplayName is a user-facing name of the pipeline that may be
Expand Down
55 changes: 55 additions & 0 deletions pkg/apis/pipeline/v1/pipeline_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ package v1

import (
"context"
"encoding/hex"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/tektoncd/pipeline/pkg/apis/config"
"github.com/tektoncd/pipeline/test/diff"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"knative.dev/pkg/apis"
Expand Down Expand Up @@ -969,3 +971,56 @@ func TestEmbeddedTask_IsCustomTask(t *testing.T) {
})
}
}

func TestPipelineChecksum(t *testing.T) {
tests := []struct {
name string
pipeline *Pipeline
}{{
name: "pipeline ignore uid",
pipeline: &Pipeline{
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1",
Kind: "Pipeline"},
ObjectMeta: metav1.ObjectMeta{
Name: "pipeline",
Namespace: "pipeline-ns",
UID: "abc",
Labels: map[string]string{"label": "foo"},
Annotations: map[string]string{"foo": "bar"},
},
Spec: PipelineSpec{},
},
}, {
name: "pipeline ignore system annotations",
pipeline: &Pipeline{
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1",
Kind: "Pipeline"},
ObjectMeta: metav1.ObjectMeta{
Name: "pipeline",
Namespace: "pipeline-ns",
UID: "abc",
Labels: map[string]string{"label": "foo"},
Annotations: map[string]string{
"foo": "bar",
"kubectl-client-side-apply": "client",
"kubectl.kubernetes.io/last-applied-configuration": "config",
},
},
Spec: PipelineSpec{},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sha, err := tt.pipeline.Checksum()
if err != nil {
t.Fatalf("Error computing checksuum: %v", err)
}

if d := cmp.Diff(hex.EncodeToString(sha), "98bc732636b8fbc08f3d353932147e4eff4e667f0c1af675656a48efdc8178e3"); d != "" {
t.Error(diff.PrintWantGot(d))
}
})
}
}
22 changes: 22 additions & 0 deletions pkg/apis/pipeline/v1/task_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package v1

import (
"github.com/tektoncd/pipeline/pkg/apis/pipeline"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/internal/checksum"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand Down Expand Up @@ -52,6 +53,27 @@ func (*Task) GetGroupVersionKind() schema.GroupVersionKind {
return SchemeGroupVersion.WithKind(pipeline.TaskControllerName)
}

// Checksum computes the sha256 checksum of the task object.
// Prior to computing the checksum, it performs some preprocessing on the
// metadata of the object where it removes system provided annotations.
// Only the name, namespace, generateName, user-provided labels and annotations
// and the taskSpec are included for the checksum computation.
func (t *Task) Checksum() ([]byte, error) {
objectMeta := checksum.PrepareObjectMeta(t)
preprocessedTask := Task{
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1",
Kind: "Task"},
ObjectMeta: objectMeta,
Spec: t.Spec,
}
sha256Checksum, err := checksum.ComputeSha256Checksum(preprocessedTask)
if err != nil {
return nil, err
}
return sha256Checksum, nil
}

// TaskSpec defines the desired state of Task.
type TaskSpec struct {
// Params is a list of input parameters required to run the task. Params
Expand Down
90 changes: 90 additions & 0 deletions pkg/apis/pipeline/v1/task_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
Copyright 2023 The Tekton 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 v1_test

import (
"encoding/hex"
"testing"

"github.com/google/go-cmp/cmp"
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"github.com/tektoncd/pipeline/test/diff"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestTask_Checksum(t *testing.T) {
tests := []struct {
name string
task *v1.Task
}{{
name: "task ignore uid",
task: &v1.Task{
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1",
Kind: "Task"},
ObjectMeta: metav1.ObjectMeta{
Name: "task",
Namespace: "task-ns",
UID: "abc",
Labels: map[string]string{"label": "foo"},
Annotations: map[string]string{"foo": "bar"},
},
Spec: v1.TaskSpec{
Steps: []v1.Step{{
Image: "ubuntu",
Name: "echo",
}},
},
},
}, {
name: "task ignore system annotations",
task: &v1.Task{
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1",
Kind: "Task"},
ObjectMeta: metav1.ObjectMeta{
Name: "task",
Namespace: "task-ns",
UID: "abc",
Labels: map[string]string{"label": "foo"},
Annotations: map[string]string{
"foo": "bar",
"kubectl-client-side-apply": "client",
"kubectl.kubernetes.io/last-applied-configuration": "config",
},
},
Spec: v1.TaskSpec{
Steps: []v1.Step{{
Image: "ubuntu",
Name: "echo",
}},
},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sha, err := tt.task.Checksum()
if err != nil {
t.Fatalf("Error computing checksuum: %v", err)
}

if d := cmp.Diff(hex.EncodeToString(sha), "0cf41a775529eaaa55ff115eebe5db01a3b6bf2f4b924606888736274ceb267a"); d != "" {
t.Error(diff.PrintWantGot(d))
}
})
}
}
Loading

0 comments on commit 1f5c137

Please sign in to comment.