From 700bc4e1896f8d18f2039b3f2f094ae7e32bbb78 Mon Sep 17 00:00:00 2001 From: Chitrang Patel Date: Fri, 27 Oct 2023 12:41:25 -0400 Subject: [PATCH] Introduce Params and Results into StepActions CRD This PR introduces `params` and `results` into the `StepAction` CRD. It allows the `StepAction` to declare the params it needs and the results it will produce. The follow up PRs will contain the interaction of how a `Task` referencing the `StepAction` resolves them. All the tests were borrowed from `pkg/apis/pipeline/v1/...` that overlapped with `paramSpec` and `Results`. --- docs/pipeline-api.md | 332 ++++++++++ hack/ignored-openapi-violations.list | 6 + .../pipeline/v1alpha1/openapi_generated.go | 239 ++++++- pkg/apis/pipeline/v1alpha1/param_types.go | 256 ++++++++ .../pipeline/v1alpha1/param_types_test.go | 296 +++++++++ .../pipeline/v1alpha1/result_defaults_test.go | 95 +++ pkg/apis/pipeline/v1alpha1/result_types.go | 80 +++ .../pipeline/v1alpha1/result_validation.go | 76 +++ .../v1alpha1/result_validation_test.go | 124 ++++ .../pipeline/v1alpha1/stepaction_defaults.go | 11 + .../pipeline/v1alpha1/stepaction_types.go | 8 + .../v1alpha1/stepaction_validation.go | 250 ++++++++ .../v1alpha1/stepaction_validation_test.go | 606 +++++++++++++++++- pkg/apis/pipeline/v1alpha1/swagger.json | 130 ++++ .../v1alpha1/zz_generated.deepcopy.go | 131 ++++ 15 files changed, 2636 insertions(+), 4 deletions(-) create mode 100644 pkg/apis/pipeline/v1alpha1/param_types.go create mode 100644 pkg/apis/pipeline/v1alpha1/param_types_test.go create mode 100644 pkg/apis/pipeline/v1alpha1/result_defaults_test.go create mode 100644 pkg/apis/pipeline/v1alpha1/result_types.go create mode 100644 pkg/apis/pipeline/v1alpha1/result_validation.go create mode 100644 pkg/apis/pipeline/v1alpha1/result_validation_test.go diff --git a/docs/pipeline-api.md b/docs/pipeline-api.md index f100fbf2561..16f4cb440f5 100644 --- a/docs/pipeline-api.md +++ b/docs/pipeline-api.md @@ -6541,6 +6541,34 @@ string

If Script is not empty, the Step cannot have an Command and the Args will be passed to the Script.

+ + +Params
+ + +ParamSpecs + + + + +(Optional) +

Params is a list of input parameters required to run the stepAction. +Params must be supplied as inputs in Steps unless they declare a defaultvalue.

+ + + + +Results
+ + +[]StepActionResult + + + + +

Results are values that this StepAction can output

+ + @@ -6991,6 +7019,200 @@ HashAlgorithm

ModeType indicates the type of a mode for VerificationPolicy

+

ParamSpec +

+
+

ParamSpec defines arbitrary parameters needed beyond typed inputs (such as +resources). Parameter values are provided by users as inputs on a TaskRun +or PipelineRun.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name declares the name by which a parameter is referenced.

+
+type
+ + +ParamType + + +
+(Optional) +

Type is the user-specified type of the parameter. The possible types +are currently “string”, “array” and “object”, and “string” is the default.

+
+description
+ +string + +
+(Optional) +

Description is a user-facing description of the parameter that may be +used to populate a UI.

+
+properties
+ + +map[string]github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.PropertySpec + + +
+(Optional) +

Properties is the JSON Schema properties to support key-value pairs parameter.

+
+default
+ + +ParamValue + + +
+(Optional) +

Default is the value a parameter takes if no input value is supplied. If +default is set, a Task may be executed without a supplied value for the +parameter.

+
+

ParamSpecs +([]github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.ParamSpec alias)

+

+(Appears on:StepActionSpec) +

+
+

ParamSpecs is a list of ParamSpec

+
+

ParamType +(string alias)

+

+(Appears on:ParamSpec, ParamValue, PropertySpec) +

+
+

ParamType indicates the type of an input parameter; +Used to distinguish between a single string and an array of strings.

+
+

ParamValue +

+

+(Appears on:ParamSpec) +

+
+

ResultValue is a type alias of ParamValue

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+Type
+ + +ParamType + + +
+
+StringVal
+ +string + +
+

Represents the stored type of ParamValues.

+
+ArrayVal
+ +[]string + +
+
+ObjectVal
+ +map[string]string + +
+
+

PropertySpec +

+

+(Appears on:ParamSpec, StepActionResult) +

+
+

PropertySpec defines the struct for object keys

+
+ + + + + + + + + + + + + +
FieldDescription
+type
+ + +ParamType + + +
+

ResourcePattern

@@ -7024,6 +7246,18 @@ Hub resource: https://artifacthub.io/*, +

ResultsType +(string alias)

+

+(Appears on:StepActionResult) +

+
+

ResultsType indicates the type of a result; +Used to distinguish between a single string and an array of strings. +Note that there is ResultType used to find out whether a +RunResult is from a task result or not, which is different from +this ResultsType.

+

RunReason (string alias)

@@ -7206,6 +7440,76 @@ Refer Go’s ParseDuration documentation for expected format: StepActionResult + +

+(Appears on:StepActionSpec) +

+
+

StepActionResult used to describe the results of a task

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name the given name

+
+type
+ + +ResultsType + + +
+(Optional) +

Type is the user-specified type of the result. The possible type +is currently “string” and will support “array” in following work.

+
+properties
+ + +map[string]github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.PropertySpec + + +
+(Optional) +

Properties is the JSON Schema properties to support key-value pairs results.

+
+description
+ +string + +
+(Optional) +

Description is a human-readable description of the result

+

StepActionSpec

@@ -7301,6 +7605,34 @@ string

If Script is not empty, the Step cannot have an Command and the Args will be passed to the Script.

+ + +Params
+ + +ParamSpecs + + + + +(Optional) +

Params is a list of input parameters required to run the stepAction. +Params must be supplied as inputs in Steps unless they declare a defaultvalue.

+ + + + +Results
+ + +[]StepActionResult + + + + +

Results are values that this StepAction can output

+ +

VerificationPolicySpec diff --git a/hack/ignored-openapi-violations.list b/hack/ignored-openapi-violations.list index 9badeae797c..c95137c260a 100644 --- a/hack/ignored-openapi-violations.list +++ b/hack/ignored-openapi-violations.list @@ -38,6 +38,12 @@ API rule violation: names_match,github.com/tektoncd/pipeline/pkg/apis/pipeline/v API rule violation: names_match,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1,StepTemplate,DeprecatedTerminationMessagePath API rule violation: names_match,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1,StepTemplate,DeprecatedTerminationMessagePolicy API rule violation: names_match,github.com/tektoncd/pipeline/pkg/apis/resolution/v1alpha1,ResolutionRequestSpec,Parameters +API rule violation: names_match,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1,ParamValue,ArrayVal +API rule violation: names_match,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1,ParamValue,ObjectVal +API rule violation: names_match,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1,ParamValue,StringVal +API rule violation: names_match,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1,ParamValue,Type +API rule violation: names_match,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1,StepActionSpec,Params +API rule violation: names_match,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1,StepActionSpec,Results API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1,RunSpec,Workspaces API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1,VerificationPolicySpec,Authorities API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1,VerificationPolicySpec,Resources diff --git a/pkg/apis/pipeline/v1alpha1/openapi_generated.go b/pkg/apis/pipeline/v1alpha1/openapi_generated.go index 876cb0bdc84..406ef76c04c 100644 --- a/pkg/apis/pipeline/v1alpha1/openapi_generated.go +++ b/pkg/apis/pipeline/v1alpha1/openapi_generated.go @@ -35,12 +35,16 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.Authority": schema_pkg_apis_pipeline_v1alpha1_Authority(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.EmbeddedRunSpec": schema_pkg_apis_pipeline_v1alpha1_EmbeddedRunSpec(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.KeyRef": schema_pkg_apis_pipeline_v1alpha1_KeyRef(ref), + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.ParamSpec": schema_pkg_apis_pipeline_v1alpha1_ParamSpec(ref), + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.ParamValue": schema_pkg_apis_pipeline_v1alpha1_ParamValue(ref), + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.PropertySpec": schema_pkg_apis_pipeline_v1alpha1_PropertySpec(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.ResourcePattern": schema_pkg_apis_pipeline_v1alpha1_ResourcePattern(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.Run": schema_pkg_apis_pipeline_v1alpha1_Run(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.RunList": schema_pkg_apis_pipeline_v1alpha1_RunList(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.RunSpec": schema_pkg_apis_pipeline_v1alpha1_RunSpec(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.StepAction": schema_pkg_apis_pipeline_v1alpha1_StepAction(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.StepActionList": schema_pkg_apis_pipeline_v1alpha1_StepActionList(ref), + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.StepActionResult": schema_pkg_apis_pipeline_v1alpha1_StepActionResult(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.StepActionSpec": schema_pkg_apis_pipeline_v1alpha1_StepActionSpec(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.VerificationPolicy": schema_pkg_apis_pipeline_v1alpha1_VerificationPolicy(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.VerificationPolicyList": schema_pkg_apis_pipeline_v1alpha1_VerificationPolicyList(ref), @@ -445,6 +449,147 @@ func schema_pkg_apis_pipeline_v1alpha1_KeyRef(ref common.ReferenceCallback) comm } } +func schema_pkg_apis_pipeline_v1alpha1_ParamSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ParamSpec defines arbitrary parameters needed beyond typed inputs (such as resources). Parameter values are provided by users as inputs on a TaskRun or PipelineRun.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name declares the name by which a parameter is referenced.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "Type is the user-specified type of the parameter. The possible types are currently \"string\", \"array\" and \"object\", and \"string\" is the default.", + Type: []string{"string"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Description: "Description is a user-facing description of the parameter that may be used to populate a UI.", + Type: []string{"string"}, + Format: "", + }, + }, + "properties": { + SchemaProps: spec.SchemaProps{ + Description: "Properties is the JSON Schema properties to support key-value pairs parameter.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.PropertySpec"), + }, + }, + }, + }, + }, + "default": { + SchemaProps: spec.SchemaProps{ + Description: "Default is the value a parameter takes if no input value is supplied. If default is set, a Task may be executed without a supplied value for the parameter.", + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.ParamValue"), + }, + }, + }, + Required: []string{"name"}, + }, + }, + Dependencies: []string{ + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.ParamValue", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.PropertySpec"}, + } +} + +func schema_pkg_apis_pipeline_v1alpha1_ParamValue(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ResultValue is a type alias of ParamValue", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "Type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "StringVal": { + SchemaProps: spec.SchemaProps{ + Description: "Represents the stored type of ParamValues.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "ArrayVal": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "ObjectVal": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"Type", "StringVal", "ArrayVal", "ObjectVal"}, + }, + }, + } +} + +func schema_pkg_apis_pipeline_v1alpha1_PropertySpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PropertySpec defines the struct for object keys", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + func schema_pkg_apis_pipeline_v1alpha1_ResourcePattern(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -747,6 +892,59 @@ func schema_pkg_apis_pipeline_v1alpha1_StepActionList(ref common.ReferenceCallba } } +func schema_pkg_apis_pipeline_v1alpha1_StepActionResult(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "StepActionResult used to describe the results of a task", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name the given name", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "Type is the user-specified type of the result. The possible type is currently \"string\" and will support \"array\" in following work.", + Type: []string{"string"}, + Format: "", + }, + }, + "properties": { + SchemaProps: spec.SchemaProps{ + Description: "Properties is the JSON Schema properties to support key-value pairs results.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.PropertySpec"), + }, + }, + }, + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Description: "Description is a human-readable description of the result", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name"}, + }, + }, + Dependencies: []string{ + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.PropertySpec"}, + } +} + func schema_pkg_apis_pipeline_v1alpha1_StepActionSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -829,11 +1027,50 @@ func schema_pkg_apis_pipeline_v1alpha1_StepActionSpec(ref common.ReferenceCallba Format: "", }, }, + "Params": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Params is a list of input parameters required to run the stepAction. Params must be supplied as inputs in Steps unless they declare a defaultvalue.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.ParamSpec"), + }, + }, + }, + }, + }, + "Results": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Results are values that this StepAction can output", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.StepActionResult"), + }, + }, + }, + }, + }, }, + Required: []string{"Results"}, }, }, Dependencies: []string{ - "k8s.io/api/core/v1.EnvVar"}, + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.ParamSpec", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.StepActionResult", "k8s.io/api/core/v1.EnvVar"}, } } diff --git a/pkg/apis/pipeline/v1alpha1/param_types.go b/pkg/apis/pipeline/v1alpha1/param_types.go new file mode 100644 index 00000000000..9069c5f47b5 --- /dev/null +++ b/pkg/apis/pipeline/v1alpha1/param_types.go @@ -0,0 +1,256 @@ +/* +Copyright 2019 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 v1alpha1 + +import ( + "context" + "encoding/json" + "fmt" + + "k8s.io/apimachinery/pkg/util/sets" + "knative.dev/pkg/apis" +) + +// ParamsPrefix is the prefix used in $(...) expressions referring to parameters +const ParamsPrefix = "params" + +// ParamSpec defines arbitrary parameters needed beyond typed inputs (such as +// resources). Parameter values are provided by users as inputs on a TaskRun +// or PipelineRun. +type ParamSpec struct { + // Name declares the name by which a parameter is referenced. + Name string `json:"name"` + // Type is the user-specified type of the parameter. The possible types + // are currently "string", "array" and "object", and "string" is the default. + // +optional + Type ParamType `json:"type,omitempty"` + // Description is a user-facing description of the parameter that may be + // used to populate a UI. + // +optional + Description string `json:"description,omitempty"` + // Properties is the JSON Schema properties to support key-value pairs parameter. + // +optional + Properties map[string]PropertySpec `json:"properties,omitempty"` + // Default is the value a parameter takes if no input value is supplied. If + // default is set, a Task may be executed without a supplied value for the + // parameter. + // +optional + Default *ParamValue `json:"default,omitempty"` +} + +// ParamSpecs is a list of ParamSpec +type ParamSpecs []ParamSpec + +// PropertySpec defines the struct for object keys +type PropertySpec struct { + Type ParamType `json:"type,omitempty"` +} + +// SetDefaults set the default type +func (pp *ParamSpec) SetDefaults(context.Context) { + if pp == nil { + return + } + + // Propagate inferred type to the parent ParamSpec's type, and default type to the PropertySpec's type + // The sequence to look at is type in ParamSpec -> properties -> type in default -> array/string/object value in default + // If neither `properties` or `default` section is provided, ParamTypeString will be the default type. + switch { + case pp.Type != "": + // If param type is provided by the author, do nothing but just set default type for PropertySpec in case `properties` section is provided. + pp.setDefaultsForProperties() + case pp.Properties != nil: + pp.Type = ParamTypeObject + // Also set default type for PropertySpec + pp.setDefaultsForProperties() + case pp.Default == nil: + // ParamTypeString is the default value (when no type can be inferred from the default value) + pp.Type = ParamTypeString + case pp.Default.Type != "": + pp.Type = pp.Default.Type + case pp.Default.ArrayVal != nil: + pp.Type = ParamTypeArray + case pp.Default.ObjectVal != nil: + pp.Type = ParamTypeObject + default: + pp.Type = ParamTypeString + } +} + +// getNames returns all the names of the declared parameters +func (ps ParamSpecs) getNames() []string { + var names []string + for _, p := range ps { + names = append(names, p.Name) + } + return names +} + +// sortByType splits the input params into string params, array params, and object params, in that order +func (ps ParamSpecs) sortByType() (ParamSpecs, ParamSpecs, ParamSpecs) { + var stringParams, arrayParams, objectParams ParamSpecs + for _, p := range ps { + switch p.Type { + case ParamTypeArray: + arrayParams = append(arrayParams, p) + case ParamTypeObject: + objectParams = append(objectParams, p) + case ParamTypeString: + fallthrough + default: + stringParams = append(stringParams, p) + } + } + return stringParams, arrayParams, objectParams +} + +// validateNoDuplicateNames returns an error if any of the params have the same name +func (ps ParamSpecs) validateNoDuplicateNames() *apis.FieldError { + names := ps.getNames() + seen := sets.String{} + dups := sets.String{} + var errs *apis.FieldError + for _, n := range names { + if seen.Has(n) { + dups.Insert(n) + } + seen.Insert(n) + } + for n := range dups { + errs = errs.Also(apis.ErrGeneric("parameter appears more than once", "").ViaFieldKey("params", n)) + } + return errs +} + +// setDefaultsForProperties sets default type for PropertySpec (string) if it's not specified +func (pp *ParamSpec) setDefaultsForProperties() { + for key, propertySpec := range pp.Properties { + if propertySpec.Type == "" { + pp.Properties[key] = PropertySpec{Type: ParamTypeString} + } + } +} + +// ParamType indicates the type of an input parameter; +// Used to distinguish between a single string and an array of strings. +type ParamType string + +// Valid ParamTypes: +const ( + ParamTypeString ParamType = "string" + ParamTypeArray ParamType = "array" + ParamTypeObject ParamType = "object" +) + +// AllParamTypes can be used for ParamType validation. +var AllParamTypes = []ParamType{ParamTypeString, ParamTypeArray, ParamTypeObject} + +// ParamValues is modeled after IntOrString in kubernetes/apimachinery: + +// ParamValue is a type that can hold a single string or string array. +// Used in JSON unmarshalling so that a single JSON field can accept +// either an individual string or an array of strings. +type ParamValue struct { + Type ParamType // Represents the stored type of ParamValues. + StringVal string + // +listType=atomic + ArrayVal []string + ObjectVal map[string]string +} + +// ArrayOrString is deprecated, this is to keep backward compatibility +// +// Deprecated: Use ParamValue instead. +type ArrayOrString = ParamValue + +// UnmarshalJSON implements the json.Unmarshaller interface. +func (paramValues *ParamValue) UnmarshalJSON(value []byte) error { + // ParamValues is used for Results Value as well, the results can be any kind of + // data so we need to check if it is empty. + if len(value) == 0 { + paramValues.Type = ParamTypeString + return nil + } + if value[0] == '[' { + // We're trying to Unmarshal to []string, but for cases like []int or other types + // of nested array which we don't support yet, we should continue and Unmarshal + // it to String. If the Type being set doesn't match what it actually should be, + // it will be captured by validation in reconciler. + // if failed to unmarshal to array, we will convert the value to string and marshal it to string + var a []string + if err := json.Unmarshal(value, &a); err == nil { + paramValues.Type = ParamTypeArray + paramValues.ArrayVal = a + return nil + } + } + if value[0] == '{' { + // if failed to unmarshal to map, we will convert the value to string and marshal it to string + var m map[string]string + if err := json.Unmarshal(value, &m); err == nil { + paramValues.Type = ParamTypeObject + paramValues.ObjectVal = m + return nil + } + } + + // By default we unmarshal to string + paramValues.Type = ParamTypeString + if err := json.Unmarshal(value, ¶mValues.StringVal); err == nil { + return nil + } + paramValues.StringVal = string(value) + + return nil +} + +// MarshalJSON implements the json.Marshaller interface. +func (paramValues ParamValue) MarshalJSON() ([]byte, error) { + switch paramValues.Type { + case ParamTypeString: + return json.Marshal(paramValues.StringVal) + case ParamTypeArray: + return json.Marshal(paramValues.ArrayVal) + case ParamTypeObject: + return json.Marshal(paramValues.ObjectVal) + default: + return []byte{}, fmt.Errorf("impossible ParamValues.Type: %q", paramValues.Type) + } +} + +// NewStructuredValues creates an ParamValues of type ParamTypeString or ParamTypeArray, based on +// how many inputs are given (>1 input will create an array, not string). +func NewStructuredValues(value string, values ...string) *ParamValue { + if len(values) > 0 { + return &ParamValue{ + Type: ParamTypeArray, + ArrayVal: append([]string{value}, values...), + } + } + return &ParamValue{ + Type: ParamTypeString, + StringVal: value, + } +} + +// NewArrayOrString is the deprecated, this is to keep backward compatibility +var NewArrayOrString = NewStructuredValues + +// NewObject creates an ParamValues of type ParamTypeObject using the provided key-value pairs +func NewObject(pairs map[string]string) *ParamValue { + return &ParamValue{ + Type: ParamTypeObject, + ObjectVal: pairs, + } +} diff --git a/pkg/apis/pipeline/v1alpha1/param_types_test.go b/pkg/apis/pipeline/v1alpha1/param_types_test.go new file mode 100644 index 00000000000..ea72ec8ce69 --- /dev/null +++ b/pkg/apis/pipeline/v1alpha1/param_types_test.go @@ -0,0 +1,296 @@ +/* +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 v1alpha1_test + +import ( + "bytes" + "context" + "encoding/json" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + v1alpha1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/test/diff" +) + +func TestParamSpec_SetDefaults(t *testing.T) { + tests := []struct { + name string + before *v1alpha1.ParamSpec + defaultsApplied *v1alpha1.ParamSpec + }{{ + name: "nil paramspec", + before: nil, + defaultsApplied: nil, + }, { + name: "inferred string type", + before: &v1alpha1.ParamSpec{ + Name: "parametername", + }, + defaultsApplied: &v1alpha1.ParamSpec{ + Name: "parametername", + Type: v1alpha1.ParamTypeString, + }, + }, { + name: "inferred type from default value - array", + before: &v1alpha1.ParamSpec{ + Name: "parametername", + Default: &v1alpha1.ParamValue{ + ArrayVal: []string{"array"}, + }, + }, + defaultsApplied: &v1alpha1.ParamSpec{ + Name: "parametername", + Type: v1alpha1.ParamTypeArray, + Default: &v1alpha1.ParamValue{ + ArrayVal: []string{"array"}, + }, + }, + }, { + name: "inferred type from default value - string", + before: &v1alpha1.ParamSpec{ + Name: "parametername", + Default: &v1alpha1.ParamValue{ + StringVal: "an", + }, + }, + defaultsApplied: &v1alpha1.ParamSpec{ + Name: "parametername", + Type: v1alpha1.ParamTypeString, + Default: &v1alpha1.ParamValue{ + StringVal: "an", + }, + }, + }, { + name: "inferred type from default value - object", + before: &v1alpha1.ParamSpec{ + Name: "parametername", + Default: &v1alpha1.ParamValue{ + ObjectVal: map[string]string{"url": "test", "path": "test"}, + }, + }, + defaultsApplied: &v1alpha1.ParamSpec{ + Name: "parametername", + Type: v1alpha1.ParamTypeObject, + Default: &v1alpha1.ParamValue{ + ObjectVal: map[string]string{"url": "test", "path": "test"}, + }, + }, + }, { + name: "inferred type from properties - PropertySpec type is not provided", + before: &v1alpha1.ParamSpec{ + Name: "parametername", + Properties: map[string]v1alpha1.PropertySpec{"key1": {}}, + }, + defaultsApplied: &v1alpha1.ParamSpec{ + Name: "parametername", + Type: v1alpha1.ParamTypeObject, + Properties: map[string]v1alpha1.PropertySpec{"key1": {Type: "string"}}, + }, + }, { + name: "inferred type from properties - PropertySpec type is provided", + before: &v1alpha1.ParamSpec{ + Name: "parametername", + Properties: map[string]v1alpha1.PropertySpec{"key2": {Type: "string"}}, + }, + defaultsApplied: &v1alpha1.ParamSpec{ + Name: "parametername", + Type: v1alpha1.ParamTypeObject, + Properties: map[string]v1alpha1.PropertySpec{"key2": {Type: "string"}}, + }, + }, { + name: "fully defined ParamSpec - array", + before: &v1alpha1.ParamSpec{ + Name: "parametername", + Type: v1alpha1.ParamTypeArray, + Description: "a description", + Default: &v1alpha1.ParamValue{ + ArrayVal: []string{"array"}, + }, + }, + defaultsApplied: &v1alpha1.ParamSpec{ + Name: "parametername", + Type: v1alpha1.ParamTypeArray, + Description: "a description", + Default: &v1alpha1.ParamValue{ + ArrayVal: []string{"array"}, + }, + }, + }, { + name: "fully defined ParamSpec - object", + before: &v1alpha1.ParamSpec{ + Name: "parametername", + Type: v1alpha1.ParamTypeObject, + Description: "a description", + Default: &v1alpha1.ParamValue{ + ObjectVal: map[string]string{"url": "test", "path": "test"}, + }, + }, + defaultsApplied: &v1alpha1.ParamSpec{ + Name: "parametername", + Type: v1alpha1.ParamTypeObject, + Description: "a description", + Default: &v1alpha1.ParamValue{ + ObjectVal: map[string]string{"url": "test", "path": "test"}, + }, + }, + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + tc.before.SetDefaults(ctx) + if d := cmp.Diff(tc.defaultsApplied, tc.before); d != "" { + t.Error(diff.PrintWantGot(d)) + } + }) + } +} + +type ParamValuesHolder struct { + AOrS v1alpha1.ParamValue `json:"val"` +} + +func TestParamValues_UnmarshalJSON(t *testing.T) { + cases := []struct { + input map[string]interface{} + result v1alpha1.ParamValue + }{ + { + input: map[string]interface{}{"val": 123}, + result: *v1alpha1.NewStructuredValues("123"), + }, + { + input: map[string]interface{}{"val": "123"}, + result: *v1alpha1.NewStructuredValues("123"), + }, + { + input: map[string]interface{}{"val": ""}, + result: *v1alpha1.NewStructuredValues(""), + }, + { + input: map[string]interface{}{"val": nil}, + result: v1alpha1.ParamValue{Type: v1alpha1.ParamTypeString, ArrayVal: nil}, + }, + { + input: map[string]interface{}{"val": []string{}}, + result: v1alpha1.ParamValue{Type: v1alpha1.ParamTypeArray, ArrayVal: []string{}}, + }, + { + input: map[string]interface{}{"val": []string{"oneelement"}}, + result: v1alpha1.ParamValue{Type: v1alpha1.ParamTypeArray, ArrayVal: []string{"oneelement"}}, + }, + { + input: map[string]interface{}{"val": []string{"multiple", "elements"}}, + result: v1alpha1.ParamValue{Type: v1alpha1.ParamTypeArray, ArrayVal: []string{"multiple", "elements"}}, + }, + { + input: map[string]interface{}{"val": map[string]string{"key1": "val1", "key2": "val2"}}, + result: v1alpha1.ParamValue{Type: v1alpha1.ParamTypeObject, ObjectVal: map[string]string{"key1": "val1", "key2": "val2"}}, + }, + } + + for _, c := range cases { + for _, opts := range []func(enc *json.Encoder){ + // Default encoding + func(enc *json.Encoder) {}, + // Multiline encoding + func(enc *json.Encoder) { enc.SetIndent("", " ") }, + } { + b := new(bytes.Buffer) + enc := json.NewEncoder(b) + opts(enc) + if err := enc.Encode(c.input); err != nil { + t.Fatalf("error encoding json: %v", err) + } + + var result ParamValuesHolder + if err := json.Unmarshal(b.Bytes(), &result); err != nil { + t.Errorf("Failed to unmarshal input '%v': %v", c.input, err) + } + if !reflect.DeepEqual(result.AOrS, c.result) { + t.Errorf("expected %+v, got %+v", c.result, result) + } + } + } +} + +func TestParamValues_UnmarshalJSON_Directly(t *testing.T) { + cases := []struct { + desc string + input string + expected v1alpha1.ParamValue + }{ + {desc: "empty value", input: ``, expected: *v1alpha1.NewStructuredValues("")}, + {desc: "int value", input: `1`, expected: *v1alpha1.NewStructuredValues("1")}, + {desc: "int array", input: `[1,2,3]`, expected: *v1alpha1.NewStructuredValues("[1,2,3]")}, + {desc: "nested array", input: `[1,\"2\",3]`, expected: *v1alpha1.NewStructuredValues(`[1,\"2\",3]`)}, + {desc: "string value", input: `hello`, expected: *v1alpha1.NewStructuredValues("hello")}, + {desc: "array value", input: `["hello","world"]`, expected: *v1alpha1.NewStructuredValues("hello", "world")}, + {desc: "object value", input: `{"hello":"world"}`, expected: *v1alpha1.NewObject(map[string]string{"hello": "world"})}, + } + + for _, c := range cases { + v := v1alpha1.ParamValue{} + if err := v.UnmarshalJSON([]byte(c.input)); err != nil { + t.Errorf("Failed to unmarshal input '%v': %v", c.input, err) + } + if !reflect.DeepEqual(v, c.expected) { + t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.expected, v) + } + } +} + +func TestParamValues_UnmarshalJSON_Error(t *testing.T) { + cases := []struct { + desc string + input string + }{ + {desc: "empty value", input: "{\"val\": }"}, + {desc: "wrong beginning value", input: "{\"val\": @}"}, + } + + for _, c := range cases { + var result ParamValuesHolder + if err := json.Unmarshal([]byte(c.input), &result); err == nil { + t.Errorf("Should return err but got nil '%v'", c.input) + } + } +} + +func TestParamValues_MarshalJSON(t *testing.T) { + cases := []struct { + input v1alpha1.ParamValue + result string + }{ + {*v1alpha1.NewStructuredValues("123"), "{\"val\":\"123\"}"}, + {*v1alpha1.NewStructuredValues("123", "1234"), "{\"val\":[\"123\",\"1234\"]}"}, + {*v1alpha1.NewStructuredValues("a", "a", "a"), "{\"val\":[\"a\",\"a\",\"a\"]}"}, + {*v1alpha1.NewObject(map[string]string{"key1": "var1", "key2": "var2"}), "{\"val\":{\"key1\":\"var1\",\"key2\":\"var2\"}}"}, + } + + for _, c := range cases { + input := ParamValuesHolder{c.input} + result, err := json.Marshal(&input) + if err != nil { + t.Errorf("Failed to marshal input '%v': %v", input, err) + } + if string(result) != c.result { + t.Errorf("Failed to marshal input '%v': expected: %+v, got %q", input, c.result, string(result)) + } + } +} diff --git a/pkg/apis/pipeline/v1alpha1/result_defaults_test.go b/pkg/apis/pipeline/v1alpha1/result_defaults_test.go new file mode 100644 index 00000000000..ccf39579402 --- /dev/null +++ b/pkg/apis/pipeline/v1alpha1/result_defaults_test.go @@ -0,0 +1,95 @@ +/* +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 v1alpha1_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + v1alpha1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/test/diff" +) + +func TestStepActionResult_SetDefaults(t *testing.T) { + tests := []struct { + name string + before *v1alpha1.StepActionResult + after *v1alpha1.StepActionResult + }{{ + name: "empty taskresult", + before: nil, + after: nil, + }, { + name: "inferred string type", + before: &v1alpha1.StepActionResult{ + Name: "resultname", + }, + after: &v1alpha1.StepActionResult{ + Name: "resultname", + Type: v1alpha1.ResultsTypeString, + }, + }, { + name: "string type specified not changed", + before: &v1alpha1.StepActionResult{ + Name: "resultname", + Type: v1alpha1.ResultsTypeString, + }, + after: &v1alpha1.StepActionResult{ + Name: "resultname", + Type: v1alpha1.ResultsTypeString, + }, + }, { + name: "array type specified not changed", + before: &v1alpha1.StepActionResult{ + Name: "resultname", + Type: v1alpha1.ResultsTypeArray, + }, + after: &v1alpha1.StepActionResult{ + Name: "resultname", + Type: v1alpha1.ResultsTypeArray, + }, + }, { + name: "inferred object type from properties - PropertySpec type is provided", + before: &v1alpha1.StepActionResult{ + Name: "resultname", + Properties: map[string]v1alpha1.PropertySpec{"key1": {v1alpha1.ParamTypeString}}, + }, + after: &v1alpha1.StepActionResult{ + Name: "resultname", + Type: v1alpha1.ResultsTypeObject, + Properties: map[string]v1alpha1.PropertySpec{"key1": {v1alpha1.ParamTypeString}}, + }, + }, { + name: "inferred type from properties - PropertySpec type is not provided", + before: &v1alpha1.StepActionResult{ + Name: "resultname", + Properties: map[string]v1alpha1.PropertySpec{"key1": {}}, + }, + after: &v1alpha1.StepActionResult{ + Name: "resultname", + Type: v1alpha1.ResultsTypeObject, + Properties: map[string]v1alpha1.PropertySpec{"key1": {v1alpha1.ParamTypeString}}, + }, + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + tc.before.SetDefaults(ctx) + if d := cmp.Diff(tc.after, tc.before); d != "" { + t.Error(diff.PrintWantGot(d)) + } + }) + } +} diff --git a/pkg/apis/pipeline/v1alpha1/result_types.go b/pkg/apis/pipeline/v1alpha1/result_types.go new file mode 100644 index 00000000000..380a1f751b9 --- /dev/null +++ b/pkg/apis/pipeline/v1alpha1/result_types.go @@ -0,0 +1,80 @@ +/* +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 v1alpha1 + +import ( + "context" +) + +// StepActionResult used to describe the results of a task +type StepActionResult struct { + // Name the given name + Name string `json:"name"` + + // Type is the user-specified type of the result. The possible type + // is currently "string" and will support "array" in following work. + // +optional + Type ResultsType `json:"type,omitempty"` + + // Properties is the JSON Schema properties to support key-value pairs results. + // +optional + Properties map[string]PropertySpec `json:"properties,omitempty"` + + // Description is a human-readable description of the result + // +optional + Description string `json:"description,omitempty"` +} + +// SetDefaults set the default type for StepActionResult +func (sar *StepActionResult) SetDefaults(context.Context) { + if sar == nil { + return + } + if sar.Type == "" { + if sar.Properties != nil { + // Set type to object if `properties` is given + sar.Type = ResultsTypeObject + } else { + // ResultsTypeString is the default value + sar.Type = ResultsTypeString + } + } + + // Set default type of object values to string + for key, propertySpec := range sar.Properties { + if propertySpec.Type == "" { + sar.Properties[key] = PropertySpec{Type: ParamType(ResultsTypeString)} + } + } +} + +// ResultValue is a type alias of ParamValue +type ResultValue = ParamValue + +// ResultsType indicates the type of a result; +// Used to distinguish between a single string and an array of strings. +// Note that there is ResultType used to find out whether a +// RunResult is from a task result or not, which is different from +// this ResultsType. +type ResultsType string + +// Valid ResultsType: +const ( + ResultsTypeString ResultsType = "string" + ResultsTypeArray ResultsType = "array" + ResultsTypeObject ResultsType = "object" +) + +// AllResultsTypes can be used for ResultsTypes validation. +var AllResultsTypes = []ResultsType{ResultsTypeString, ResultsTypeArray, ResultsTypeObject} diff --git a/pkg/apis/pipeline/v1alpha1/result_validation.go b/pkg/apis/pipeline/v1alpha1/result_validation.go new file mode 100644 index 00000000000..eb9c5ebe31b --- /dev/null +++ b/pkg/apis/pipeline/v1alpha1/result_validation.go @@ -0,0 +1,76 @@ +/* +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 v1alpha1 + +import ( + "context" + "fmt" + "regexp" + + "knative.dev/pkg/apis" +) + +const ( + // resultNameFormat Constant used to define the regex Result.Name should follow + resultNameFormat = `^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$` +) + +var resultNameFormatRegex = regexp.MustCompile(resultNameFormat) + +// validate implements apis.Validatable +func (sar StepActionResult) validate(ctx context.Context) (errs *apis.FieldError) { + if !resultNameFormatRegex.MatchString(sar.Name) { + return apis.ErrInvalidKeyName(sar.Name, "name", fmt.Sprintf("Name must consist of alphanumeric characters, '-', '_', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my-name', or 'my_name', regex used for validation is '%s')", resultNameFormat)) + } + + switch { + case sar.Type == ResultsTypeObject: + errs := validateObjectResult(sar) + return errs + case sar.Type == ResultsTypeArray: + return nil + // Resources created before the result. Type was introduced may not have Type set + // and should be considered valid + case sar.Type == "": + return nil + // By default, the result type is string + case sar.Type != ResultsTypeString: + return apis.ErrInvalidValue(sar.Type, "type", "type must be string") + } + + return nil +} + +// validateObjectResult validates the object result and check if the Properties is missing +// for Properties values it will check if the type is string. +func validateObjectResult(sar StepActionResult) (errs *apis.FieldError) { + if ParamType(sar.Type) == ParamTypeObject && sar.Properties == nil { + return apis.ErrMissingField(fmt.Sprintf("%s.properties", sar.Name)) + } + + invalidKeys := []string{} + for key, propertySpec := range sar.Properties { + if propertySpec.Type != ParamTypeString { + invalidKeys = append(invalidKeys, key) + } + } + + if len(invalidKeys) != 0 { + return &apis.FieldError{ + Message: fmt.Sprintf("The value type specified for these keys %v is invalid, the type must be string", invalidKeys), + Paths: []string{fmt.Sprintf("%s.properties", sar.Name)}, + } + } + return nil +} diff --git a/pkg/apis/pipeline/v1alpha1/result_validation_test.go b/pkg/apis/pipeline/v1alpha1/result_validation_test.go new file mode 100644 index 00000000000..81f667e963a --- /dev/null +++ b/pkg/apis/pipeline/v1alpha1/result_validation_test.go @@ -0,0 +1,124 @@ +/* +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 v1alpha1 + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/tektoncd/pipeline/test/diff" + "knative.dev/pkg/apis" +) + +func TestResultsValidate(t *testing.T) { + tests := []struct { + name string + Result StepActionResult + }{{ + name: "valid result type empty", + Result: StepActionResult{ + Name: "MY-RESULT", + Description: "my great result", + }, + }, { + name: "valid result type string", + Result: StepActionResult{ + Name: "MY-RESULT", + Type: ResultsTypeString, + Description: "my great result", + }, + }, { + name: "valid result type array", + Result: StepActionResult{ + Name: "MY-RESULT", + Type: ResultsTypeArray, + Description: "my great result", + }, + }, { + name: "valid result type object", + Result: StepActionResult{ + Name: "MY-RESULT", + Type: ResultsTypeObject, + Description: "my great result", + Properties: map[string]PropertySpec{"hello": {Type: ParamTypeString}}, + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + if err := tt.Result.validate(ctx); err != nil { + t.Errorf("TaskSpec.Validate() = %v", err) + } + }) + } +} + +func TestResultsValidateError(t *testing.T) { + tests := []struct { + name string + Result StepActionResult + expectedError apis.FieldError + }{{ + name: "invalid result type", + Result: StepActionResult{ + Name: "MY-RESULT", + Type: "wrong", + Description: "my great result", + }, + expectedError: apis.FieldError{ + Message: `invalid value: wrong`, + Paths: []string{"type"}, + Details: "type must be string", + }, + }, { + name: "invalid object properties type", + Result: StepActionResult{ + Name: "MY-RESULT", + Type: ResultsTypeObject, + Description: "my great result", + Properties: map[string]PropertySpec{"hello": {Type: "wrong type"}}, + }, + expectedError: apis.FieldError{ + Message: "The value type specified for these keys [hello] is invalid, the type must be string", + Paths: []string{"MY-RESULT.properties"}, + }, + }, { + name: "invalid object properties empty", + Result: StepActionResult{ + Name: "MY-RESULT", + Type: ResultsTypeObject, + Description: "my great result", + }, + expectedError: apis.FieldError{ + Message: "missing field(s)", + Paths: []string{"MY-RESULT.properties"}, + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.Result.validate(context.Background()) + if err == nil { + t.Fatalf("Expected an error, got nothing for %v", tt.Result) + } + if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" { + t.Errorf("TaskSpec.Validate() errors diff %s", diff.PrintWantGot(d)) + } + }) + } +} diff --git a/pkg/apis/pipeline/v1alpha1/stepaction_defaults.go b/pkg/apis/pipeline/v1alpha1/stepaction_defaults.go index 8b30d937e9c..b0471f66488 100644 --- a/pkg/apis/pipeline/v1alpha1/stepaction_defaults.go +++ b/pkg/apis/pipeline/v1alpha1/stepaction_defaults.go @@ -23,4 +23,15 @@ var _ apis.Defaultable = (*StepAction)(nil) // SetDefaults implements apis.Defaultable func (s *StepAction) SetDefaults(ctx context.Context) { + s.Spec.SetDefaults(ctx) +} + +// SetDefaults set any defaults for the StepAction spec +func (ss *StepActionSpec) SetDefaults(ctx context.Context) { + for i := range ss.Params { + ss.Params[i].SetDefaults(ctx) + } + for i := range ss.Results { + ss.Results[i].SetDefaults(ctx) + } } diff --git a/pkg/apis/pipeline/v1alpha1/stepaction_types.go b/pkg/apis/pipeline/v1alpha1/stepaction_types.go index 03bc5608642..27211040a0e 100644 --- a/pkg/apis/pipeline/v1alpha1/stepaction_types.go +++ b/pkg/apis/pipeline/v1alpha1/stepaction_types.go @@ -111,6 +111,14 @@ type StepActionSpec struct { // If Script is not empty, the Step cannot have an Command and the Args will be passed to the Script. // +optional Script string `json:"script,omitempty"` + // Params is a list of input parameters required to run the stepAction. + // Params must be supplied as inputs in Steps unless they declare a defaultvalue. + // +optional + // +listType=atomic + Params ParamSpecs + // Results are values that this StepAction can output + // +listType=atomic + Results []StepActionResult } // StepActionObject is implemented by StepAction diff --git a/pkg/apis/pipeline/v1alpha1/stepaction_validation.go b/pkg/apis/pipeline/v1alpha1/stepaction_validation.go index 6209026c1b1..19de8d62fb8 100644 --- a/pkg/apis/pipeline/v1alpha1/stepaction_validation.go +++ b/pkg/apis/pipeline/v1alpha1/stepaction_validation.go @@ -15,15 +15,30 @@ package v1alpha1 import ( "context" + "fmt" + "regexp" "strings" "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/validate" + "github.com/tektoncd/pipeline/pkg/substitution" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + "k8s.io/apimachinery/pkg/util/sets" "knative.dev/pkg/apis" "knative.dev/pkg/webhook/resourcesemantics" ) +const ( + // stringAndArrayVariableNameFormat is the regex to validate if string/array variable name format follows the following rules. + // - Must only contain alphanumeric characters, hyphens (-), underscores (_), and dots (.) + // - Must begin with a letter or an underscore (_) + stringAndArrayVariableNameFormat = "^[_a-zA-Z][_a-zA-Z0-9.-]*$" + + // objectVariableNameFormat is the regext used to validate object name and key names format + // The difference with the array or string name format is that object variable names shouldn't contain dots. + objectVariableNameFormat = "^[_a-zA-Z][_a-zA-Z0-9-]*$" +) + var _ apis.Validatable = (*StepAction)(nil) var _ resourcesemantics.VerbLimited = (*StepAction)(nil) @@ -32,6 +47,9 @@ func (s *StepAction) SupportedVerbs() []admissionregistrationv1.OperationType { return []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update} } +var stringAndArrayVariableNameFormatRegex = regexp.MustCompile(stringAndArrayVariableNameFormat) +var objectVariableNameFormatRegex = regexp.MustCompile(objectVariableNameFormat) + // Validate implements apis.Validatable func (s *StepAction) Validate(ctx context.Context) (errs *apis.FieldError) { errs = validate.ObjectMetadata(s.GetObjectMeta()).ViaField("metadata") @@ -58,5 +76,237 @@ func (ss *StepActionSpec) Validate(ctx context.Context) (errs *apis.FieldError) errs = errs.Also(config.ValidateEnabledAPIFields(ctx, "windows script support", config.AlphaAPIFields).ViaField("script")) } } + errs = errs.Also(validateUsageOfDeclaredParameters(ctx, *ss)) + errs = errs.Also(validateParameterTypes(ctx, ss.Params).ViaField("params")) + errs = errs.Also(validateParameterVariables(ctx, *ss, ss.Params)) + errs = errs.Also(validateStepActionResultsVariables(ctx, *ss)) + errs = errs.Also(validateResults(ctx, ss.Results).ViaField("results")) + return errs +} + +// validateUsageOfDeclaredParameters validates that all parameters referenced in the Task are declared by the Task. +func validateUsageOfDeclaredParameters(ctx context.Context, sas StepActionSpec) *apis.FieldError { + params := sas.Params + var errs *apis.FieldError + _, _, objectParams := params.sortByType() + allParameterNames := sets.NewString(params.getNames()...) + errs = errs.Also(validateStepActionVariables(ctx, sas, "params", allParameterNames)) + errs = errs.Also(validateObjectUsage(ctx, sas, objectParams)) + errs = errs.Also(validateObjectParamsHaveProperties(ctx, params)) + return errs +} + +// validateObjectParamsHaveProperties returns an error if any declared object params are missing properties +func validateObjectParamsHaveProperties(ctx context.Context, params ParamSpecs) *apis.FieldError { + var errs *apis.FieldError + for _, p := range params { + if p.Type == ParamTypeObject && p.Properties == nil { + errs = errs.Also(apis.ErrMissingField(fmt.Sprintf("%s.properties", p.Name))) + } + } + return errs +} + +func validateResults(ctx context.Context, results []StepActionResult) (errs *apis.FieldError) { + for index, result := range results { + errs = errs.Also(result.validate(ctx).ViaIndex(index)) + } + return errs +} + +// validateParameterTypes validates all the types within a slice of ParamSpecs +func validateParameterTypes(ctx context.Context, params []ParamSpec) (errs *apis.FieldError) { + for _, p := range params { + errs = errs.Also(p.validateType(ctx)) + } + return errs +} + +// validateType checks that the type of a ParamSpec is allowed and its default value matches that type +func (p ParamSpec) validateType(ctx context.Context) *apis.FieldError { + // Ensure param has a valid type. + validType := false + for _, allowedType := range AllParamTypes { + if p.Type == allowedType { + validType = true + } + } + if !validType { + return apis.ErrInvalidValue(p.Type, fmt.Sprintf("%s.type", p.Name)) + } + + // If a default value is provided, ensure its type matches param's declared type. + if (p.Default != nil) && (p.Default.Type != p.Type) { + return &apis.FieldError{ + Message: fmt.Sprintf( + "\"%v\" type does not match default value's type: \"%v\"", p.Type, p.Default.Type), + Paths: []string{ + fmt.Sprintf("%s.type", p.Name), + fmt.Sprintf("%s.default.type", p.Name), + }, + } + } + + // Check object type and its PropertySpec type + return p.validateObjectType(ctx) +} + +// validateObjectType checks that object type parameter does not miss the +// definition of `properties` section and the type of a PropertySpec is allowed. +// (Currently, only string is allowed) +func (p ParamSpec) validateObjectType(ctx context.Context) *apis.FieldError { + invalidKeys := []string{} + for key, propertySpec := range p.Properties { + if propertySpec.Type != ParamTypeString { + invalidKeys = append(invalidKeys, key) + } + } + + if len(invalidKeys) != 0 { + return &apis.FieldError{ + Message: fmt.Sprintf("The value type specified for these keys %v is invalid", invalidKeys), + Paths: []string{fmt.Sprintf("%s.properties", p.Name)}, + } + } + + return nil +} + +// validateParameterVariables validates all variables within a slice of ParamSpecs against a StepAction +func validateParameterVariables(ctx context.Context, sas StepActionSpec, params ParamSpecs) *apis.FieldError { + var errs *apis.FieldError + errs = errs.Also(params.validateNoDuplicateNames()) + stringParams, arrayParams, objectParams := params.sortByType() + stringParameterNames := sets.NewString(stringParams.getNames()...) + arrayParameterNames := sets.NewString(arrayParams.getNames()...) + errs = errs.Also(validateNameFormat(stringParameterNames.Insert(arrayParameterNames.List()...), objectParams)) + return errs.Also(validateStepActionArrayUsage(sas, "params", arrayParameterNames)) +} + +// validateObjectUsage validates the usage of individual attributes of an object param and the usage of the entire object +func validateObjectUsage(ctx context.Context, sas StepActionSpec, params ParamSpecs) (errs *apis.FieldError) { + objectParameterNames := sets.NewString() + for _, p := range params { + // collect all names of object type params + objectParameterNames.Insert(p.Name) + + // collect all keys for this object param + objectKeys := sets.NewString() + for key := range p.Properties { + objectKeys.Insert(key) + } + + // check if the object's key names are referenced correctly i.e. param.objectParam.key1 + errs = errs.Also(validateStepActionVariables(ctx, sas, fmt.Sprintf("params\\.%s", p.Name), objectKeys)) + } + + return errs.Also(validateStepActionObjectUsageAsWhole(sas, "params", objectParameterNames)) +} + +// validateStepActionObjectUsageAsWhole returns an error if the StepAction contains references to the entire input object params in fields where these references are prohibited +func validateStepActionObjectUsageAsWhole(sas StepActionSpec, prefix string, vars sets.String) *apis.FieldError { + errs := substitution.ValidateNoReferencesToEntireProhibitedVariables(sas.Image, prefix, vars).ViaField("image") + errs = errs.Also(substitution.ValidateNoReferencesToEntireProhibitedVariables(sas.Script, prefix, vars).ViaField("script")) + for i, cmd := range sas.Command { + errs = errs.Also(substitution.ValidateNoReferencesToEntireProhibitedVariables(cmd, prefix, vars).ViaFieldIndex("command", i)) + } + for i, arg := range sas.Args { + errs = errs.Also(substitution.ValidateNoReferencesToEntireProhibitedVariables(arg, prefix, vars).ViaFieldIndex("args", i)) + } + for _, env := range sas.Env { + errs = errs.Also(substitution.ValidateNoReferencesToEntireProhibitedVariables(env.Value, prefix, vars).ViaFieldKey("env", env.Name)) + } + return errs +} + +// validateStepActionArrayUsage returns an error if the Step contains references to the input array params in fields where these references are prohibited +func validateStepActionArrayUsage(sas StepActionSpec, prefix string, arrayParamNames sets.String) *apis.FieldError { + errs := substitution.ValidateNoReferencesToProhibitedVariables(sas.Image, prefix, arrayParamNames).ViaField("image") + errs = errs.Also(substitution.ValidateNoReferencesToProhibitedVariables(sas.Script, prefix, arrayParamNames).ViaField("script")) + for i, cmd := range sas.Command { + errs = errs.Also(substitution.ValidateVariableReferenceIsIsolated(cmd, prefix, arrayParamNames).ViaFieldIndex("command", i)) + } + for i, arg := range sas.Args { + errs = errs.Also(substitution.ValidateVariableReferenceIsIsolated(arg, prefix, arrayParamNames).ViaFieldIndex("args", i)) + } + for _, env := range sas.Env { + errs = errs.Also(substitution.ValidateNoReferencesToProhibitedVariables(env.Value, prefix, arrayParamNames).ViaFieldKey("env", env.Name)) + } + return errs +} + +// validateNameFormat validates that the name format of all param types follows the rules +func validateNameFormat(stringAndArrayParams sets.String, objectParams []ParamSpec) (errs *apis.FieldError) { + // checking string or array name format + // ---- + invalidStringAndArrayNames := []string{} + // Converting to sorted list here rather than just looping map keys + // because we want the order of items in vars to be deterministic for purpose of unit testing + for _, name := range stringAndArrayParams.List() { + if !stringAndArrayVariableNameFormatRegex.MatchString(name) { + invalidStringAndArrayNames = append(invalidStringAndArrayNames, name) + } + } + + if len(invalidStringAndArrayNames) != 0 { + errs = errs.Also(&apis.FieldError{ + Message: fmt.Sprintf("The format of following array and string variable names is invalid: %s", invalidStringAndArrayNames), + Paths: []string{"params"}, + Details: "String/Array Names: \nMust only contain alphanumeric characters, hyphens (-), underscores (_), and dots (.)\nMust begin with a letter or an underscore (_)", + }) + } + + // checking object name and key name format + // ----- + invalidObjectNames := map[string][]string{} + for _, obj := range objectParams { + // check object param name + if !objectVariableNameFormatRegex.MatchString(obj.Name) { + invalidObjectNames[obj.Name] = []string{} + } + + // check key names + for k := range obj.Properties { + if !objectVariableNameFormatRegex.MatchString(k) { + invalidObjectNames[obj.Name] = append(invalidObjectNames[obj.Name], k) + } + } + } + + if len(invalidObjectNames) != 0 { + errs = errs.Also(&apis.FieldError{ + Message: fmt.Sprintf("Object param name and key name format is invalid: %s", invalidObjectNames), + Paths: []string{"params"}, + Details: "Object Names: \nMust only contain alphanumeric characters, hyphens (-), underscores (_) \nMust begin with a letter or an underscore (_)", + }) + } + + return errs +} + +// validateStepActionVariables returns an error if the StepAction contains references to any unknown variables +func validateStepActionVariables(ctx context.Context, sas StepActionSpec, prefix string, vars sets.String) *apis.FieldError { + errs := substitution.ValidateNoReferencesToUnknownVariables(sas.Image, prefix, vars).ViaField("image") + errs = errs.Also(substitution.ValidateNoReferencesToUnknownVariables(sas.Script, prefix, vars).ViaField("script")) + for i, cmd := range sas.Command { + errs = errs.Also(substitution.ValidateNoReferencesToUnknownVariables(cmd, prefix, vars).ViaFieldIndex("command", i)) + } + for i, arg := range sas.Args { + errs = errs.Also(substitution.ValidateNoReferencesToUnknownVariables(arg, prefix, vars).ViaFieldIndex("args", i)) + } + for _, env := range sas.Env { + errs = errs.Also(substitution.ValidateNoReferencesToUnknownVariables(env.Value, prefix, vars).ViaFieldKey("env", env.Name)) + } + return errs +} + +// validateStepActionResultsVariables validates if the results referenced in step script are defined in task results +func validateStepActionResultsVariables(ctx context.Context, sas StepActionSpec) (errs *apis.FieldError) { + results := sas.Results + resultsNames := sets.NewString() + for _, r := range results { + resultsNames.Insert(r.Name) + } + errs = errs.Also(substitution.ValidateNoReferencesToUnknownVariables(sas.Script, "results", resultsNames).ViaField("script")) return errs } diff --git a/pkg/apis/pipeline/v1alpha1/stepaction_validation_test.go b/pkg/apis/pipeline/v1alpha1/stepaction_validation_test.go index 72871f81555..fa4e7f147d9 100644 --- a/pkg/apis/pipeline/v1alpha1/stepaction_validation_test.go +++ b/pkg/apis/pipeline/v1alpha1/stepaction_validation_test.go @@ -15,6 +15,7 @@ package v1alpha1_test import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" @@ -64,6 +65,8 @@ func TestStepActionSpecValidate(t *testing.T) { Args []string Script string Env []corev1.EnvVar + Params []v1alpha1.ParamSpec + Results []v1alpha1.StepActionResult } tests := []struct { name string @@ -91,6 +94,161 @@ func TestStepActionSpecValidate(t *testing.T) { Value: "/tekton/home", }}, }, + }, { + name: "valid params type explicit", + fields: fields{ + Image: "myimage", + Params: []v1alpha1.ParamSpec{{ + Name: "stringParam", + Type: v1alpha1.ParamTypeString, + Description: "param", + Default: v1alpha1.NewStructuredValues("default"), + }, { + Name: "objectParam", + Type: v1alpha1.ParamTypeObject, + Description: "param", + Properties: map[string]v1alpha1.PropertySpec{ + "key1": {}, + "key2": {}, + }, + Default: v1alpha1.NewObject(map[string]string{ + "key1": "var1", + "key2": "var2", + }), + }, { + Name: "objectParamWithoutDefault", + Type: v1alpha1.ParamTypeObject, + Description: "param", + Properties: map[string]v1alpha1.PropertySpec{ + "key1": {}, + "key2": {}, + }, + }, { + Name: "objectParamWithDefaultPartialKeys", + Type: v1alpha1.ParamTypeObject, + Description: "param", + Properties: map[string]v1alpha1.PropertySpec{ + "key1": {}, + "key2": {}, + }, + Default: v1alpha1.NewObject(map[string]string{ + "key1": "default", + }), + }}, + }, + }, { + name: "valid string param usage", + fields: fields{ + Image: "url", + Params: []v1alpha1.ParamSpec{{ + Name: "baz", + }, { + Name: "foo-is-baz", + }}, + Args: []string{"--flag=$(params.baz) && $(params.foo-is-baz)"}, + }, + }, { + name: "valid array param usage", + fields: fields{ + Image: "url", + Params: []v1alpha1.ParamSpec{{ + Name: "baz", + Type: v1alpha1.ParamTypeArray, + }, { + Name: "foo-is-baz", + Type: v1alpha1.ParamTypeArray, + }}, + Command: []string{"$(params.foo-is-baz)"}, + Args: []string{"$(params.baz)", "middle string", "$(params.foo-is-baz)"}, + }, + }, { + name: "valid object param usage", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "gitrepo", + Type: v1alpha1.ParamTypeObject, + Properties: map[string]v1alpha1.PropertySpec{ + "url": {}, + "commit": {}, + }, + }}, + Image: "some-git-image", + Args: []string{"-url=$(params.gitrepo.url)", "-commit=$(params.gitrepo.commit)"}, + }, + }, { + name: "valid star array usage", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "baz", + Type: v1alpha1.ParamTypeArray, + }, { + Name: "foo-is-baz", + Type: v1alpha1.ParamTypeArray, + }}, + Image: "myimage", + Command: []string{"$(params.foo-is-baz)"}, + Args: []string{"$(params.baz[*])", "middle string", "$(params.foo-is-baz[*])"}, + }, + }, { + name: "valid step with parameterized script", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "baz", + }, { + Name: "foo-is-baz", + }}, + Image: "my-image", + Script: ` + #!/usr/bin/env bash + hello $(params.baz)`, + }, + }, { + name: "valid result", + fields: fields{ + Image: "my-image", + Args: []string{"arg"}, + Results: []v1alpha1.StepActionResult{{ + Name: "MY-RESULT", + Description: "my great result", + }}, + }, + }, { + name: "valid result type string", + fields: fields{ + Image: "my-image", + Args: []string{"arg"}, + Results: []v1alpha1.StepActionResult{{ + Name: "MY-RESULT", + Type: "string", + Description: "my great result", + }}, + }, + }, { + name: "valid result type array", + fields: fields{ + Image: "my-image", + Args: []string{"arg"}, + Results: []v1alpha1.StepActionResult{{ + Name: "MY-RESULT", + Type: v1alpha1.ResultsTypeArray, + Description: "my great result", + }}, + }, + }, { + name: "valid result type object", + fields: fields{ + Image: "my-image", + Args: []string{"arg"}, + Results: []v1alpha1.StepActionResult{{ + Name: "MY-RESULT", + Type: v1alpha1.ResultsTypeObject, + Description: "my great result", + Properties: map[string]v1alpha1.PropertySpec{ + "url": {"string"}, + "commit": {"string"}, + }, + }}, + }, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -100,8 +258,12 @@ func TestStepActionSpecValidate(t *testing.T) { Args: tt.fields.Args, Script: tt.fields.Script, Env: tt.fields.Env, + Params: tt.fields.Params, + Results: tt.fields.Results, } - if err := sa.Validate(context.Background()); err != nil { + ctx := context.Background() + sa.SetDefaults(ctx) + if err := sa.Validate(ctx); err != nil { t.Errorf("StepActionSpec.Validate() = %v", err) } }) @@ -115,6 +277,8 @@ func TestStepActionValidateError(t *testing.T) { Args []string Script string Env []corev1.EnvVar + Params []v1alpha1.ParamSpec + Results []v1alpha1.StepActionResult } tests := []struct { name string @@ -123,12 +287,117 @@ func TestStepActionValidateError(t *testing.T) { }{{ name: "inexistent image field", fields: fields{ - Args: []string{"--flag=$(params.inexistent)"}, + Args: []string{"flag"}, }, expectedError: apis.FieldError{ Message: `missing field(s)`, Paths: []string{"spec.Image"}, }, + }, { + name: "object used in a string field", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "gitrepo", + Type: v1alpha1.ParamTypeObject, + Properties: map[string]v1alpha1.PropertySpec{ + "url": {}, + "commit": {}, + }, + }}, + Image: "$(params.gitrepo)", + Args: []string{"echo"}, + }, + expectedError: apis.FieldError{ + Message: `variable type invalid in "$(params.gitrepo)"`, + Paths: []string{"spec.image"}, + }, + }, { + name: "object star used in a string field", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "gitrepo", + Type: v1alpha1.ParamTypeObject, + Properties: map[string]v1alpha1.PropertySpec{ + "url": {}, + "commit": {}, + }, + }}, + Image: "$(params.gitrepo[*])", + Args: []string{"echo"}, + }, + expectedError: apis.FieldError{ + Message: `variable type invalid in "$(params.gitrepo[*])"`, + Paths: []string{"spec.image"}, + }, + }, { + name: "object used in a field that can accept array type", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "gitrepo", + Type: v1alpha1.ParamTypeObject, + Properties: map[string]v1alpha1.PropertySpec{ + "url": {}, + "commit": {}, + }, + }}, + Image: "myimage", + Args: []string{"$(params.gitrepo)"}, + }, + expectedError: apis.FieldError{ + Message: `variable type invalid in "$(params.gitrepo)"`, + Paths: []string{"spec.args[0]"}, + }, + }, { + name: "object star used in a field that can accept array type", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "gitrepo", + Type: v1alpha1.ParamTypeObject, + Properties: map[string]v1alpha1.PropertySpec{ + "url": {}, + "commit": {}, + }, + }}, + Image: "some-git-image", + Args: []string{"$(params.gitrepo[*])"}, + }, + expectedError: apis.FieldError{ + Message: `variable type invalid in "$(params.gitrepo[*])"`, + Paths: []string{"spec.args[0]"}, + }, + }, { + name: "non-existent individual key of an object param is used in task step", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "gitrepo", + Type: v1alpha1.ParamTypeObject, + Properties: map[string]v1alpha1.PropertySpec{ + "url": {}, + "commit": {}, + }, + }}, + Image: "some-git-image", + Args: []string{"$(params.gitrepo.non-exist-key)"}, + }, + expectedError: apis.FieldError{ + Message: `non-existent variable in "$(params.gitrepo.non-exist-key)"`, + Paths: []string{"spec.args[0]"}, + }, + }, { + name: "Inexistent param variable with existing", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "foo", + Description: "param", + Default: v1alpha1.NewStructuredValues("default"), + }}, + Image: "myimage", + Args: []string{"$(params.foo) && $(params.inexistent)"}, + }, + expectedError: apis.FieldError{ + Message: `non-existent variable in "$(params.foo) && $(params.inexistent)"`, + Paths: []string{"spec.args[0]"}, + }, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -140,9 +409,12 @@ func TestStepActionValidateError(t *testing.T) { Args: tt.fields.Args, Script: tt.fields.Script, Env: tt.fields.Env, + Params: tt.fields.Params, + Results: tt.fields.Results, }, } ctx := context.Background() + sa.SetDefaults(ctx) err := sa.Validate(ctx) if err == nil { t.Fatalf("Expected an error, got nothing for %v", sa) @@ -161,6 +433,8 @@ func TestStepActionSpecValidateError(t *testing.T) { Args []string Script string Env []corev1.EnvVar + Params []v1alpha1.ParamSpec + Results []v1alpha1.StepActionResult } tests := []struct { name string @@ -169,7 +443,7 @@ func TestStepActionSpecValidateError(t *testing.T) { }{{ name: "inexistent image field", fields: fields{ - Args: []string{"--flag=$(params.inexistent)"}, + Args: []string{"flag"}, }, expectedError: apis.FieldError{ Message: `missing field(s)`, @@ -196,6 +470,329 @@ func TestStepActionSpecValidateError(t *testing.T) { Message: `windows script support requires "enable-api-fields" feature gate to be "alpha" but it is "beta"`, Paths: []string{}, }, + }, { + name: "step script refers to nonexistent result", + fields: fields{ + Image: "my-image", + Script: ` + #!/usr/bin/env bash + date | tee $(results.non-exist.path)`, + Results: []v1alpha1.StepActionResult{{Name: "a-result"}}, + }, + expectedError: apis.FieldError{ + Message: `non-existent variable in "\n\t\t\t#!/usr/bin/env bash\n\t\t\tdate | tee $(results.non-exist.path)"`, + Paths: []string{"script"}, + }, + }, { + name: "invalid param name format", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "_validparam1", + Description: "valid param name format", + }, { + Name: "valid_param2", + Description: "valid param name format", + }, { + Name: "", + Description: "invalid param name format", + }, { + Name: "a^b", + Description: "invalid param name format", + }, { + Name: "0ab", + Description: "invalid param name format", + }, { + Name: "f oo", + Description: "invalid param name format", + }}, + Image: "myImage", + }, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("The format of following array and string variable names is invalid: %s", []string{"", "0ab", "a^b", "f oo"}), + Paths: []string{"params"}, + Details: "String/Array Names: \nMust only contain alphanumeric characters, hyphens (-), underscores (_), and dots (.)\nMust begin with a letter or an underscore (_)", + }, + }, { + name: "invalid object param format - object param name and key name shouldn't contain dots.", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "invalid.name1", + Description: "object param name contains dots", + Properties: map[string]v1alpha1.PropertySpec{ + "invalid.key1": {}, + "mykey2": {}, + }, + }}, + Image: "myImage", + }, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("Object param name and key name format is invalid: %v", map[string][]string{ + "invalid.name1": {"invalid.key1"}, + }), + Paths: []string{"params"}, + Details: "Object Names: \nMust only contain alphanumeric characters, hyphens (-), underscores (_) \nMust begin with a letter or an underscore (_)", + }, + }, { + name: "duplicated param names", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "foo", + Type: v1alpha1.ParamTypeString, + Description: "parameter", + Default: v1alpha1.NewStructuredValues("value1"), + }, { + Name: "foo", + Type: v1alpha1.ParamTypeString, + Description: "parameter", + Default: v1alpha1.NewStructuredValues("value2"), + }}, + Image: "myImage", + }, + expectedError: apis.FieldError{ + Message: `parameter appears more than once`, + Paths: []string{"params[foo]"}, + }, + }, { + name: "invalid param type", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "validparam", + Type: v1alpha1.ParamTypeString, + Description: "parameter", + Default: v1alpha1.NewStructuredValues("default"), + }, { + Name: "param-with-invalid-type", + Type: "invalidtype", + Description: "invalidtypedesc", + Default: v1alpha1.NewStructuredValues("default"), + }}, + Image: "myImage", + }, + expectedError: apis.FieldError{ + Message: `invalid value: invalidtype`, + Paths: []string{"params.param-with-invalid-type.type"}, + }, + }, { + name: "param mismatching default/type 1", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "task", + Type: v1alpha1.ParamTypeArray, + Description: "param", + Default: v1alpha1.NewStructuredValues("default"), + }}, + Image: "myImage", + }, + expectedError: apis.FieldError{ + Message: `"array" type does not match default value's type: "string"`, + Paths: []string{"params.task.type", "params.task.default.type"}, + }, + }, { + name: "param mismatching default/type 2", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "task", + Type: v1alpha1.ParamTypeString, + Description: "param", + Default: v1alpha1.NewStructuredValues("default", "array"), + }}, + Image: "myImage", + }, + expectedError: apis.FieldError{ + Message: `"string" type does not match default value's type: "array"`, + Paths: []string{"params.task.type", "params.task.default.type"}, + }, + }, { + name: "param mismatching default/type 3", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "task", + Type: v1alpha1.ParamTypeArray, + Description: "param", + Default: v1alpha1.NewObject(map[string]string{ + "key1": "var1", + "key2": "var2", + }), + }}, + Image: "myImage", + }, + expectedError: apis.FieldError{ + Message: `"array" type does not match default value's type: "object"`, + Paths: []string{"params.task.type", "params.task.default.type"}, + }, + }, { + name: "param mismatching default/type 4", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "task", + Type: v1alpha1.ParamTypeObject, + Description: "param", + Properties: map[string]v1alpha1.PropertySpec{"key1": {}}, + Default: v1alpha1.NewStructuredValues("var"), + }}, + Image: "myImage", + }, + expectedError: apis.FieldError{ + Message: `"object" type does not match default value's type: "string"`, + Paths: []string{"params.task.type", "params.task.default.type"}, + }, + }, { + name: "PropertySpec type is set with unsupported type", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "task", + Type: v1alpha1.ParamTypeObject, + Description: "param", + Properties: map[string]v1alpha1.PropertySpec{ + "key1": {Type: "number"}, + "key2": {Type: "string"}, + }, + }}, + Image: "myImage", + }, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("The value type specified for these keys %v is invalid", []string{"key1"}), + Paths: []string{"params.task.properties"}, + }, + }, { + name: "Properties is missing", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "task", + Type: v1alpha1.ParamTypeObject, + Description: "param", + }}, + Image: "myImage", + }, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("missing field(s)"), + Paths: []string{"task.properties"}, + }, + }, { + name: "array used in unaccepted field", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "baz", + Type: v1alpha1.ParamTypeArray, + }, { + Name: "foo-is-baz", + Type: v1alpha1.ParamTypeArray, + }}, + Image: "$(params.baz)", + Command: []string{"$(params.foo-is-baz)"}, + Args: []string{"$(params.baz)", "middle string", "url"}, + }, + expectedError: apis.FieldError{ + Message: `variable type invalid in "$(params.baz)"`, + Paths: []string{"image"}, + }, + }, { + name: "array star used in unaccepted field", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "baz", + Type: v1alpha1.ParamTypeArray, + }, { + Name: "foo-is-baz", + Type: v1alpha1.ParamTypeArray, + }}, + Image: "$(params.baz[*])", + Command: []string{"$(params.foo-is-baz)"}, + Args: []string{"$(params.baz)", "middle string", "url"}, + }, + expectedError: apis.FieldError{ + Message: `variable type invalid in "$(params.baz[*])"`, + Paths: []string{"image"}, + }, + }, { + name: "array star used illegaly in script field", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "baz", + Type: v1alpha1.ParamTypeArray, + }, { + Name: "foo-is-baz", + Type: v1alpha1.ParamTypeArray, + }}, + Script: "$(params.baz[*])", + Image: "my-image", + }, + expectedError: apis.FieldError{ + Message: `variable type invalid in "$(params.baz[*])"`, + Paths: []string{"script"}, + }, + }, { + name: "array not properly isolated", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "baz", + Type: v1alpha1.ParamTypeArray, + }, { + Name: "foo-is-baz", + Type: v1alpha1.ParamTypeArray, + }}, + Image: "someimage", + Command: []string{"$(params.foo-is-baz)"}, + Args: []string{"not isolated: $(params.baz)", "middle string", "url"}, + }, + expectedError: apis.FieldError{ + Message: `variable is not properly isolated in "not isolated: $(params.baz)"`, + Paths: []string{"args[0]"}, + }, + }, { + name: "array star not properly isolated", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "baz", + Type: v1alpha1.ParamTypeArray, + }, { + Name: "foo-is-baz", + Type: v1alpha1.ParamTypeArray, + }}, + Image: "someimage", + Command: []string{"$(params.foo-is-baz)"}, + Args: []string{"not isolated: $(params.baz[*])", "middle string", "url"}, + }, + expectedError: apis.FieldError{ + Message: `variable is not properly isolated in "not isolated: $(params.baz[*])"`, + Paths: []string{"args[0]"}, + }, + }, { + name: "inferred array not properly isolated", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "baz", + Default: v1alpha1.NewStructuredValues("implied", "array", "type"), + }, { + Name: "foo-is-baz", + Default: v1alpha1.NewStructuredValues("implied", "array", "type"), + }}, + Image: "someimage", + Command: []string{"$(params.foo-is-baz)"}, + Args: []string{"not isolated: $(params.baz)", "middle string", "url"}, + }, + expectedError: apis.FieldError{ + Message: `variable is not properly isolated in "not isolated: $(params.baz)"`, + Paths: []string{"args[0]"}, + }, + }, { + name: "inferred array star not properly isolated", + fields: fields{ + Params: []v1alpha1.ParamSpec{{ + Name: "baz", + Default: v1alpha1.NewStructuredValues("implied", "array", "type"), + }, { + Name: "foo-is-baz", + Default: v1alpha1.NewStructuredValues("implied", "array", "type"), + }}, + Image: "someimage", + Command: []string{"$(params.foo-is-baz)"}, + Args: []string{"not isolated: $(params.baz[*])", "middle string", "url"}, + }, + expectedError: apis.FieldError{ + Message: `variable is not properly isolated in "not isolated: $(params.baz[*])"`, + Paths: []string{"args[0]"}, + }, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -205,8 +802,11 @@ func TestStepActionSpecValidateError(t *testing.T) { Args: tt.fields.Args, Script: tt.fields.Script, Env: tt.fields.Env, + Params: tt.fields.Params, + Results: tt.fields.Results, } ctx := context.Background() + sa.SetDefaults(ctx) err := sa.Validate(ctx) if err == nil { t.Fatalf("Expected an error, got nothing for %v", sa) diff --git a/pkg/apis/pipeline/v1alpha1/swagger.json b/pkg/apis/pipeline/v1alpha1/swagger.json index 46e98e44f28..3797c5e7042 100644 --- a/pkg/apis/pipeline/v1alpha1/swagger.json +++ b/pkg/apis/pipeline/v1alpha1/swagger.json @@ -212,6 +212,85 @@ } } }, + "v1alpha1.ParamSpec": { + "description": "ParamSpec defines arbitrary parameters needed beyond typed inputs (such as resources). Parameter values are provided by users as inputs on a TaskRun or PipelineRun.", + "type": "object", + "required": [ + "name" + ], + "properties": { + "default": { + "description": "Default is the value a parameter takes if no input value is supplied. If default is set, a Task may be executed without a supplied value for the parameter.", + "$ref": "#/definitions/v1alpha1.ParamValue" + }, + "description": { + "description": "Description is a user-facing description of the parameter that may be used to populate a UI.", + "type": "string" + }, + "name": { + "description": "Name declares the name by which a parameter is referenced.", + "type": "string", + "default": "" + }, + "properties": { + "description": "Properties is the JSON Schema properties to support key-value pairs parameter.", + "type": "object", + "additionalProperties": { + "default": {}, + "$ref": "#/definitions/v1alpha1.PropertySpec" + } + }, + "type": { + "description": "Type is the user-specified type of the parameter. The possible types are currently \"string\", \"array\" and \"object\", and \"string\" is the default.", + "type": "string" + } + } + }, + "v1alpha1.ParamValue": { + "description": "ResultValue is a type alias of ParamValue", + "type": "object", + "required": [ + "Type", + "StringVal", + "ArrayVal", + "ObjectVal" + ], + "properties": { + "ArrayVal": { + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "ObjectVal": { + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "StringVal": { + "description": "Represents the stored type of ParamValues.", + "type": "string", + "default": "" + }, + "Type": { + "type": "string", + "default": "" + } + } + }, + "v1alpha1.PropertySpec": { + "description": "PropertySpec defines the struct for object keys", + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, "v1alpha1.ResourcePattern": { "description": "ResourcePattern defines the pattern of the resource source", "type": "object", @@ -384,10 +463,61 @@ } } }, + "v1alpha1.StepActionResult": { + "description": "StepActionResult used to describe the results of a task", + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "Description is a human-readable description of the result", + "type": "string" + }, + "name": { + "description": "Name the given name", + "type": "string", + "default": "" + }, + "properties": { + "description": "Properties is the JSON Schema properties to support key-value pairs results.", + "type": "object", + "additionalProperties": { + "default": {}, + "$ref": "#/definitions/v1alpha1.PropertySpec" + } + }, + "type": { + "description": "Type is the user-specified type of the result. The possible type is currently \"string\" and will support \"array\" in following work.", + "type": "string" + } + } + }, "v1alpha1.StepActionSpec": { "description": "StepActionSpec contains the actionable components of a step.", "type": "object", + "required": [ + "Results" + ], "properties": { + "Params": { + "description": "Params is a list of input parameters required to run the stepAction. Params must be supplied as inputs in Steps unless they declare a defaultvalue.", + "type": "array", + "items": { + "default": {}, + "$ref": "#/definitions/v1alpha1.ParamSpec" + }, + "x-kubernetes-list-type": "atomic" + }, + "Results": { + "description": "Results are values that this StepAction can output", + "type": "array", + "items": { + "default": {}, + "$ref": "#/definitions/v1alpha1.StepActionResult" + }, + "x-kubernetes-list-type": "atomic" + }, "args": { "description": "Arguments to the entrypoint. The image's CMD is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", "type": "array", diff --git a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go index 494ace1360c..aba7f58ae09 100644 --- a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go @@ -90,6 +90,100 @@ func (in *KeyRef) DeepCopy() *KeyRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ParamSpec) DeepCopyInto(out *ParamSpec) { + *out = *in + if in.Properties != nil { + in, out := &in.Properties, &out.Properties + *out = make(map[string]PropertySpec, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Default != nil { + in, out := &in.Default, &out.Default + *out = new(ParamValue) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ParamSpec. +func (in *ParamSpec) DeepCopy() *ParamSpec { + if in == nil { + return nil + } + out := new(ParamSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ParamSpecs) DeepCopyInto(out *ParamSpecs) { + { + in := &in + *out = make(ParamSpecs, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ParamSpecs. +func (in ParamSpecs) DeepCopy() ParamSpecs { + if in == nil { + return nil + } + out := new(ParamSpecs) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ParamValue) DeepCopyInto(out *ParamValue) { + *out = *in + if in.ArrayVal != nil { + in, out := &in.ArrayVal, &out.ArrayVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ObjectVal != nil { + in, out := &in.ObjectVal, &out.ObjectVal + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ParamValue. +func (in *ParamValue) DeepCopy() *ParamValue { + if in == nil { + return nil + } + out := new(ParamValue) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PropertySpec) DeepCopyInto(out *PropertySpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PropertySpec. +func (in *PropertySpec) DeepCopy() *PropertySpec { + if in == nil { + return nil + } + out := new(PropertySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResourcePattern) DeepCopyInto(out *ResourcePattern) { *out = *in @@ -277,6 +371,29 @@ func (in *StepActionList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StepActionResult) DeepCopyInto(out *StepActionResult) { + *out = *in + if in.Properties != nil { + in, out := &in.Properties, &out.Properties + *out = make(map[string]PropertySpec, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StepActionResult. +func (in *StepActionResult) DeepCopy() *StepActionResult { + if in == nil { + return nil + } + out := new(StepActionResult) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StepActionSpec) DeepCopyInto(out *StepActionSpec) { *out = *in @@ -297,6 +414,20 @@ func (in *StepActionSpec) DeepCopyInto(out *StepActionSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Params != nil { + in, out := &in.Params, &out.Params + *out = make(ParamSpecs, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Results != nil { + in, out := &in.Results, &out.Results + *out = make([]StepActionResult, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return }