Skip to content

Commit

Permalink
pipelinetask metadata
Browse files Browse the repository at this point in the history
Adding metadata to PipelineTask to allow specifying metadata when a
task is embedded using taskSpec. This metadata will be propogated
to taskRun and then to the pods.

```
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
  name: pipelinerun-with-taskspec-to-echo-greetings
spec:
  pipelineSpec:
    tasks:
      - name: echo-greetings
        metadata:
          labels: [ …]
...
```

Metadata is already supported as part of Tasks and Pipelines while
respective CRDs are created. But was not possible to specify with
embedded resources.
  • Loading branch information
pritidesai committed Jun 17, 2020
1 parent 648cff3 commit aff0b65
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 3 deletions.
7 changes: 7 additions & 0 deletions internal/builder/v1beta1/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ func PipelineTaskSpec(spec *v1beta1.TaskSpec) PipelineTaskOp {
}
}

// PipelineTaskMetadata sets the Metadata on a PipelineTask.
func PipelineTaskMetadata(metadata metav1.ObjectMeta) PipelineTaskOp {
return func(pt *v1beta1.PipelineTask) {
pt.ObjectMeta = metadata
}
}

// Retries sets the number of retries on a PipelineTask.
func Retries(retries int) PipelineTaskOp {
return func(pt *v1beta1.PipelineTask) {
Expand Down
39 changes: 39 additions & 0 deletions internal/builder/v1beta1/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,3 +393,42 @@ func TestPipelineRunWithPipelineSpec(t *testing.T) {
t.Fatalf("PipelineRun diff -want, +got: %s", diff)
}
}

func TestPipelineRunWithTaskSpec_TaskMetadata(t *testing.T) {
pipelineRun := tb.PipelineRun("pear", tb.PipelineRunNamespace("foo"),
tb.PipelineRunSpec("", tb.PipelineRunPipelineSpec(
tb.PipelineTask("a-task", "some-task",
tb.PipelineTaskMetadata(metav1.ObjectMeta{
Name: "a-task-name",
Labels: map[string]string{"label": "labelvalue"},
Annotations: map[string]string{"annotation": "annotationvalue"}},
))),
tb.PipelineRunServiceAccountName("sa"),
))

expectedPipelineRun := &v1beta1.PipelineRun{
ObjectMeta: metav1.ObjectMeta{
Name: "pear",
Namespace: "foo",
},
Spec: v1beta1.PipelineRunSpec{
PipelineRef: nil,
PipelineSpec: &v1beta1.PipelineSpec{
Tasks: []v1beta1.PipelineTask{{
Name: "a-task",
TaskRef: &v1beta1.TaskRef{Name: "some-task"},
ObjectMeta: metav1.ObjectMeta{
Name: "a-task-name",
Labels: map[string]string{"label": "labelvalue"},
Annotations: map[string]string{"annotation": "annotationvalue"}},
}},
},
ServiceAccountName: "sa",
Timeout: &metav1.Duration{Duration: 1 * time.Hour},
},
}

if diff := cmp.Diff(expectedPipelineRun, pipelineRun); diff != "" {
t.Fatalf("PipelineRun diff -want, +got: %s", diff)
}
}
25 changes: 25 additions & 0 deletions pkg/apis/pipeline/v1beta1/pipeline_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ limitations under the License.
package v1beta1

import (
"github.com/tektoncd/pipeline/pkg/apis/validate"
"github.com/tektoncd/pipeline/pkg/reconciler/pipeline/dag"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/pkg/apis"
)

// +genclient
Expand Down Expand Up @@ -93,6 +95,9 @@ type PipelineResult struct {
// PipelineTask defines a task in a Pipeline, passing inputs from both
// Params and from the output of previous tasks.
type PipelineTask struct {
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`

// Name is the name of this task within the context of a Pipeline. Name is
// used as a coordinate with the `from` and `runAfter` fields to establish
// the execution order of tasks relative to one another.
Expand Down Expand Up @@ -139,6 +144,17 @@ type PipelineTask struct {
Timeout *metav1.Duration `json:"timeout,omitempty"`
}

func (pt *PipelineTask) PipelineTaskMetadata() metav1.ObjectMeta {
return pt.ObjectMeta
}

func (pt *PipelineTask) ValidatePipelineTaskMetadata() *apis.FieldError {
if err := validate.ObjectMetadata(pt.GetObjectMeta()); err != nil {
return err.ViaField("[tasks|finally].metadata")
}
return nil
}

func (pt PipelineTask) HashKey() string {
return pt.Name
}
Expand Down Expand Up @@ -189,6 +205,15 @@ func (l PipelineTaskList) Items() []dag.Task {
return tasks
}

func (l PipelineTaskList) ValidatePipelineTasksMetadata() *apis.FieldError {
for _, t := range l {
if err := t.ValidatePipelineTaskMetadata(); err != nil {
return err
}
}
return nil
}

// PipelineTaskParam is used to provide arbitrary string parameters to a Task.
type PipelineTaskParam struct {
Name string `json:"name"`
Expand Down
8 changes: 8 additions & 0 deletions pkg/apis/pipeline/v1beta1/pipeline_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ func (ps *PipelineSpec) Validate(ctx context.Context) *apis.FieldError {
return apis.ErrGeneric("expected at least one, got none", "spec.description", "spec.params", "spec.resources", "spec.tasks", "spec.workspaces")
}

if err := PipelineTaskList(ps.Tasks).ValidatePipelineTasksMetadata(); err != nil {
return err
}

if err := PipelineTaskList(ps.Finally).ValidatePipelineTasksMetadata(); err != nil {
return err
}

// PipelineTask must have a valid unique label and at least one of taskRef or taskSpec should be specified
if err := validatePipelineTasks(ctx, ps.Tasks, ps.Finally); err != nil {
return err
Expand Down
62 changes: 62 additions & 0 deletions pkg/apis/pipeline/v1beta1/pipeline_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,31 @@ func TestPipelineSpec_Validate_Failure(t *testing.T) {
Name: "bar", TaskRef: &TaskRef{Name: "bar-task"}, RunAfter: []string{"foo"},
}},
},
}, {
name: "invalid pipeline spec - invalid metadata in pipeline tasks",
ps: &PipelineSpec{
Tasks: []PipelineTask{{
ObjectMeta: metav1.ObjectMeta{
Name: "taskWith.",
Labels: map[string]string{"label": "value"},
Annotations: map[string]string{"annotation": "value"}},
Name: "foo", TaskRef: &TaskRef{Name: "foo-task"},
}},
},
}, {
name: "invalid pipeline spec - invalid metadata in final tasks",
ps: &PipelineSpec{
Tasks: []PipelineTask{{
Name: "foo", TaskRef: &TaskRef{Name: "pipeline-task"},
}},
Finally: []PipelineTask{{
ObjectMeta: metav1.ObjectMeta{
Name: "finalTaskWith.",
Labels: map[string]string{"label": "value"},
Annotations: map[string]string{"annotation": "value"}},
Name: "bar", TaskRef: &TaskRef{Name: "final-task"},
}},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -1391,3 +1416,40 @@ func TestValidateFinalTasks_Failure(t *testing.T) {
})
}
}

func TestPipelineTaskList_ValidatePipelineTasksMetadata_Failure(t *testing.T) {
tests := []struct {
name string
pipelineTasks []PipelineTask
}{{
name: "invalid metadata - pipeline task has pipeline name with special character in metadata",
pipelineTasks: []PipelineTask{{
Name: "pipelinetask1",
}, {
Name: "pipelinetask2",
ObjectMeta: metav1.ObjectMeta{Name: "pipelinetask2"},
}, {
Name: "pipelinetask2",
ObjectMeta: metav1.ObjectMeta{Name: "special.CharacterPipelineTask"},
}},
}, {
name: "invalid metadata - pipeline task has too long pipeline name in metadata",
pipelineTasks: []PipelineTask{{
Name: "pipelinetask1",
}, {
Name: "pipelinetask2",
ObjectMeta: metav1.ObjectMeta{Name: "pipelineTaskNameTooLongWhichIsNotValidPipelineTaskNameShouldBeLessThan63Characters"},
}, {
Name: "pipelinetask3",
ObjectMeta: metav1.ObjectMeta{Name: "pipelinetask3"},
}},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := PipelineTaskList(tt.pipelineTasks).ValidatePipelineTasksMetadata()
if err == nil {
t.Errorf("PipelineTaskList.ValidatePipelineTasksMetadata() did not return error for invalid metadata: %s", tt.name)
}
})
}
}
16 changes: 14 additions & 2 deletions pkg/reconciler/pipelinerun/pipelinerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,14 +610,26 @@ func (c *Reconciler) createTaskRun(ctx context.Context, rprt *resources.Resolved
return c.PipelineClientSet.TektonV1beta1().TaskRuns(pr.Namespace).UpdateStatus(tr)
}

// Propagate labels from PipelineRun and PipelineTask to TaskRun.
labels := getTaskrunLabels(pr, rprt.PipelineTask.Name)
for key, value := range rprt.PipelineTask.PipelineTaskMetadata().Labels {
labels[key] = value
}

// Propagate annotations from PipelineRun and PipelineTask to TaskRun.
annotations := getTaskrunAnnotations(pr)
for key, value := range rprt.PipelineTask.PipelineTaskMetadata().Annotations {
annotations[key] = value
}

serviceAccountName, podTemplate := pr.GetTaskRunSpecs(rprt.PipelineTask.Name)
tr = &v1beta1.TaskRun{
ObjectMeta: metav1.ObjectMeta{
Name: rprt.TaskRunName,
Namespace: pr.Namespace,
OwnerReferences: []metav1.OwnerReference{pr.GetOwnerReference()},
Labels: getTaskrunLabels(pr, rprt.PipelineTask.Name),
Annotations: getTaskrunAnnotations(pr),
Labels: labels,
Annotations: annotations,
},
Spec: v1beta1.TaskRunSpec{
Params: rprt.PipelineTask.Params,
Expand Down
116 changes: 115 additions & 1 deletion pkg/reconciler/pipelinerun/pipelinerun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import (
taskrunresources "github.com/tektoncd/pipeline/pkg/reconciler/taskrun/resources"
ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing"
"github.com/tektoncd/pipeline/pkg/system"
test "github.com/tektoncd/pipeline/test"
"github.com/tektoncd/pipeline/test"
"github.com/tektoncd/pipeline/test/diff"
"github.com/tektoncd/pipeline/test/names"
"go.uber.org/zap"
Expand Down Expand Up @@ -3037,3 +3037,117 @@ func TestUpdatePipelineRunStatusFromTaskRuns(t *testing.T) {
})
}
}

// this test validates pipeline task metadata is embedded into task run
func TestReconcile_PipelineTaskMetadata(t *testing.T) {
names.TestingSeed()

prs := []*v1beta1.PipelineRun{
tb.PipelineRun("test-pipeline-run-success",
tb.PipelineRunNamespace("foo"),
tb.PipelineRunSpec("test-pipeline"),
),
}
ps := []*v1beta1.Pipeline{
tb.Pipeline("test-pipeline",
tb.PipelineNamespace("foo"),
tb.PipelineSpec(
tb.PipelineTask("task-without-metadata", "",
tb.PipelineTaskSpec(&v1beta1.TaskSpec{
Steps: []v1beta1.Step{{Container: corev1.Container{
Name: "mystep",
Image: "myimage"}}},
}),
),
tb.PipelineTask("task-with-metadata", "",
tb.PipelineTaskSpec(&v1beta1.TaskSpec{
Steps: []v1beta1.Step{{Container: corev1.Container{
Name: "mystep",
Image: "myimage"}}},
}),
tb.PipelineTaskMetadata(metav1.ObjectMeta{
Name: "foo",
Labels: map[string]string{"label1": "labelvalue1", "label2": "labelvalue2"},
Annotations: map[string]string{"annotation1": "value1", "annotation2": "value2"}}),
),
),
),
}

d := test.Data{
PipelineRuns: prs,
Pipelines: ps,
Tasks: nil,
ClusterTasks: nil,
PipelineResources: nil,
}

testAssets, cancel := getPipelineRunController(t, d)
defer cancel()
c := testAssets.Controller
clients := testAssets.Clients

if err := c.Reconciler.Reconcile(context.Background(), "foo/test-pipeline-run-success"); err != nil {
t.Fatalf("Error reconciling: %s", err)
}

if len(clients.Pipeline.Actions()) == 0 {
t.Fatalf("Expected client to have been used to create a TaskRun but it wasn't")
}

// Check that the PipelineRun was reconciled correctly
reconciledRun, err := clients.Pipeline.TektonV1beta1().PipelineRuns("foo").Get("test-pipeline-run-success", metav1.GetOptions{})
if err != nil {
t.Fatalf("Somehow had error getting reconciled run out of fake client: %s", err)
}

actualTaskRun := make(map[string]*v1beta1.TaskRun)
for _, a := range clients.Pipeline.Actions() {
if a.GetResource().Resource == "taskruns" {
t := a.(ktesting.CreateAction).GetObject().(*v1beta1.TaskRun)
actualTaskRun[t.Name] = t
}
}

// Check that the expected TaskRun was created
if len(actualTaskRun) != 2 {
t.Errorf("Expected two TaskRuns to be created, but found %d TaskRuns.", len(actualTaskRun))
}

expectedTaskRun := make(map[string]*v1beta1.TaskRun)
expectedTaskRun["test-pipeline-run-success-task-with-metadata-mz4c7"] = tb.TaskRun(
"test-pipeline-run-success-task-with-metadata-mz4c7",
tb.TaskRunNamespace("foo"),
tb.TaskRunOwnerReference("PipelineRun", "test-pipeline-run-success",
tb.OwnerReferenceAPIVersion("tekton.dev/v1beta1"),
tb.Controller, tb.BlockOwnerDeletion),
tb.TaskRunLabel("tekton.dev/pipeline", "test-pipeline"),
tb.TaskRunLabel("tekton.dev/pipelineRun", "test-pipeline-run-success"),
tb.TaskRunLabel(pipeline.GroupName+pipeline.PipelineTaskLabelKey, "task-with-metadata"),
tb.TaskRunLabel("label1", "labelvalue1"),
tb.TaskRunLabel("label2", "labelvalue2"),
tb.TaskRunAnnotation("annotation1", "value1"),
tb.TaskRunAnnotation("annotation2", "value2"),
tb.TaskRunSpec(tb.TaskRunTaskSpec(tb.Step("myimage", tb.StepName("mystep")))),
)

expectedTaskRun["test-pipeline-run-success-task-without-metadata-9l9zj"] = tb.TaskRun(
"test-pipeline-run-success-task-without-metadata-9l9zj",
tb.TaskRunNamespace("foo"),
tb.TaskRunOwnerReference("PipelineRun", "test-pipeline-run-success",
tb.OwnerReferenceAPIVersion("tekton.dev/v1beta1"),
tb.Controller, tb.BlockOwnerDeletion),
tb.TaskRunLabel("tekton.dev/pipeline", "test-pipeline"),
tb.TaskRunLabel("tekton.dev/pipelineRun", "test-pipeline-run-success"),
tb.TaskRunLabel(pipeline.GroupName+pipeline.PipelineTaskLabelKey, "task-without-metadata"),
tb.TaskRunSpec(tb.TaskRunTaskSpec(tb.Step("myimage", tb.StepName("mystep")))),
)

if d := cmp.Diff(actualTaskRun, expectedTaskRun); d != "" {
t.Fatalf("Expected TaskRuns to match, but got a mismatch: %s", d)
}

if len(reconciledRun.Status.TaskRuns) != 2 {
t.Errorf("Expected PipelineRun status to include both TaskRun status items that can run immediately: %v", reconciledRun.Status.TaskRuns)
}
}

0 comments on commit aff0b65

Please sign in to comment.