diff --git a/examples/pipelineruns/demo-optional-resources.yaml b/examples/pipelineruns/demo-optional-resources.yaml new file mode 100644 index 00000000000..341db8a9b46 --- /dev/null +++ b/examples/pipelineruns/demo-optional-resources.yaml @@ -0,0 +1,130 @@ +apiVersion: tekton.dev/v1alpha1 +kind: Condition +metadata: + name: check-git-pipeline-resource +spec: + params: + - name: "path" + resources: + - name: git-repo + type: git + optional: true + check: + image: alpine + command: ["/bin/sh"] + args: ['-c', 'test -f $(resources.git-repo.path)/$(params.path)'] +--- + +apiVersion: tekton.dev/v1alpha1 +kind: Condition +metadata: + name: check-image-pipeline-resource +spec: + resources: + - name: built-image + type: image + optional: true + check: + image: alpine + command: ["/bin/sh"] + args: ['-c', 'test ! -z $(resources.built-image.url)'] +--- + +apiVersion: tekton.dev/v1alpha1 +kind: Task +metadata: + name: build-an-image +spec: + inputs: + resources: + - name: git-repo + type: git + optional: true + params: + - name: DOCKERFILE + description: The path to the dockerfile to build from GitHub Repo + default: "Dockerfile" + outputs: + resources: + - name: built-image + type: image + optional: true + steps: + - name: build-an-image + image: "gcr.io/kaniko-project/executor:latest" + command: + - /kaniko/executor + args: + - --dockerfile=$(inputs.params.DOCKERFILE) + - --destination=$(outputs.resources.built-image.url) +--- + +apiVersion: tekton.dev/v1alpha1 +kind: Pipeline +metadata: + name: demo-pipeline-to-build-an-image +spec: + resources: + - name: source-repo + type: git + optional: true + - name: web-image + type: image + optional: true + params: + - name: "path" + default: "README.md" + tasks: + - name: build-an-image + taskRef: + name: build-an-image + conditions: + - conditionRef: "check-git-pipeline-resource" + params: + - name: "path" + value: "$(params.path)" + resources: + - name: git-repo + resource: source-repo + - conditionRef: "check-image-pipeline-resource" + resources: + - name: built-image + resource: web-image + resources: + inputs: + - name: git-repo + resource: source-repo + outputs: + - name: built-image + resource: web-image + +--- + +apiVersion: tekton.dev/v1alpha1 +kind: PipelineRun +metadata: + name: demo-pipeline-run-1 +spec: + pipelineRef: + name: demo-pipeline-to-build-an-image + serviceAccountName: 'default' +--- + +apiVersion: tekton.dev/v1alpha1 +kind: PipelineRun +metadata: + name: demo-pipeline-run-2 +spec: + pipelineRef: + name: demo-pipeline-to-build-an-image + serviceAccountName: 'default' + resources: + - name: source-repo + resourceSpec: + type: git + params: + - name: revision + value: master + - name: url + value: https://github.com/tektoncd/pipeline +--- diff --git a/pkg/apis/pipeline/v1alpha1/pipeline_types.go b/pkg/apis/pipeline/v1alpha1/pipeline_types.go index 0c37dc103ac..7b23e8826d9 100644 --- a/pkg/apis/pipeline/v1alpha1/pipeline_types.go +++ b/pkg/apis/pipeline/v1alpha1/pipeline_types.go @@ -174,6 +174,10 @@ type PipelineDeclaredResource struct { Name string `json:"name"` // Type is the type of the PipelineResource. Type PipelineResourceType `json:"type"` + // Optional declares the resource as optional. + // optional: true - the resource is considered optional + // optional: false - the resource is considered required (default/equivalent of not specifying it) + Optional bool `json:"optional,omitempty"` } // PipelineConditionResource allows a Pipeline to declare how its DeclaredPipelineResources diff --git a/pkg/reconciler/pipelinerun/resources/conditionresolution.go b/pkg/reconciler/pipelinerun/resources/conditionresolution.go index 466a2a44aa9..a3722536939 100644 --- a/pkg/reconciler/pipelinerun/resources/conditionresolution.go +++ b/pkg/reconciler/pipelinerun/resources/conditionresolution.go @@ -19,7 +19,6 @@ package resources import ( "fmt" - "github.com/tektoncd/pipeline/pkg/apis/pipeline" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" corev1 "k8s.io/api/core/v1" diff --git a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go index b5401c2d3bc..1a618704eee 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go +++ b/pkg/reconciler/pipelinerun/resources/pipelinerunresolution.go @@ -187,15 +187,29 @@ func GetResourcesFromBindings(pr *v1alpha1.PipelineRun, getResource resources.Ge // ValidateResourceBindings validate that the PipelineResources declared in Pipeline p are bound in PipelineRun. func ValidateResourceBindings(p *v1alpha1.PipelineSpec, pr *v1alpha1.PipelineRun) error { required := make([]string, 0, len(p.Resources)) + optional := make([]string, 0, len(p.Resources)) for _, resource := range p.Resources { - required = append(required, resource.Name) + if resource.Optional { + // create a list of optional resources + optional = append(optional, resource.Name) + } else { + // create a list of required resources + required = append(required, resource.Name) + } } provided := make([]string, 0, len(pr.Spec.Resources)) for _, resource := range pr.Spec.Resources { provided = append(provided, resource.Name) } - if err := list.IsSame(required, provided); err != nil { - return fmt.Errorf("pipelineRun bound resources didn't match Pipeline: %w", err) + // verify that the list of required resources does exist in the provided resources + missing := list.DiffLeft(required, provided) + if len(missing) > 0 { + return fmt.Errorf("Pipeline's declared required resources are missing from the PipelineRun: %s", missing) + } + // verify that the list of provided resources does not have any extra resources (outside of required and optional resources combined) + extra := list.DiffLeft(provided, append(required, optional...)) + if len(extra) > 0 { + return fmt.Errorf("PipelineRun's declared resources didn't match usage in Pipeline: %s", extra) } return nil } @@ -426,11 +440,15 @@ func resolveConditionChecks(pt *v1alpha1.PipelineTask, taskRunStatus map[string] } conditionResources := map[string]*v1alpha1.PipelineResource{} for _, declared := range ptc.Resources { - r, ok := providedResources[declared.Resource] - if !ok { - return nil, fmt.Errorf("resources %s missing for condition %s in pipeline task %s", declared.Resource, cName, pt.Name) + if r, ok := providedResources[declared.Resource]; ok { + conditionResources[declared.Name] = r + } else { + for _, resource := range c.Spec.Resources { + if declared.Name == resource.Name && !resource.Optional { + return nil, fmt.Errorf("resources %s missing for condition %s in pipeline task %s", declared.Resource, cName, pt.Name) + } + } } - conditionResources[declared.Name] = r } rcc := ResolvedConditionCheck{ @@ -458,18 +476,34 @@ func ResolvePipelineTaskResources(pt v1alpha1.PipelineTask, ts *v1alpha1.TaskSpe } if pt.Resources != nil { for _, taskInput := range pt.Resources.Inputs { - resource, ok := providedResources[taskInput.Resource] - if !ok { - return nil, fmt.Errorf("pipelineTask tried to use input resource %s not present in declared resources", taskInput.Resource) + if resource, ok := providedResources[taskInput.Resource]; ok { + rtr.Inputs[taskInput.Name] = resource + } else { + if ts.Inputs != nil { + for _, r := range ts.Inputs.Resources { + if r.Name == taskInput.Name && !r.Optional { + return nil, fmt.Errorf("pipelineTask tried to use input resource %s not present in declared resources", taskInput.Resource) + } + } + } else { + return nil, fmt.Errorf("pipelineTask tried to use input resource %s not present in declared resources", taskInput.Resource) + } } - rtr.Inputs[taskInput.Name] = resource } for _, taskOutput := range pt.Resources.Outputs { - resource, ok := providedResources[taskOutput.Resource] - if !ok { - return nil, fmt.Errorf("pipelineTask tried to use output resource %s not present in declared resources", taskOutput.Resource) + if resource, ok := providedResources[taskOutput.Resource]; ok { + rtr.Outputs[taskOutput.Name] = resource + } else { + if ts.Outputs != nil { + for _, r := range ts.Outputs.Resources { + if r.Name == taskOutput.Name && !r.Optional { + return nil, fmt.Errorf("pipelineTask tried to use output resource %s not present in declared resources", taskOutput.Resource) + } + } + } else { + return nil, fmt.Errorf("pipelineTask tried to use output resource %s not present in declared resources", taskOutput.Resource) + } } - rtr.Outputs[taskOutput.Name] = resource } } return &rtr, nil