ResultsType indicates the type of a result;
@@ -13333,6 +13352,25 @@ Params
Params declares parameters passed to this step action.
+
+
+results
+
+
+[]StepResult
+
+
+ |
+
+(Optional)
+ Results declares StepResults produced by the step.
+This is field is at an alpha stability level and gated by “enable-step-actions” feature flag.
+It can be used in an inlined step when used to store results to
+$(step.results.resultName.path). When referencing StepActions,
+it cannot be used. The results declared by the StepActions will be stored here instead.
+Using it along side referencing StepActions will result in a validation error.
+ |
+
StepOutputConfig
@@ -13365,6 +13403,76 @@ string
+StepResult
+
+
+(Appears on:Step)
+
+
+
StepResult used to describe the results of a step action
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+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/v1beta1.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
+ |
+
+
+
StepState
diff --git a/pkg/apis/pipeline/v1/container_types.go b/pkg/apis/pipeline/v1/container_types.go
index a3cfa06384a..edca691571a 100644
--- a/pkg/apis/pipeline/v1/container_types.go
+++ b/pkg/apis/pipeline/v1/container_types.go
@@ -142,6 +142,15 @@ type Step struct {
// +optional
// +listType=atomic
Params Params `json:"params,omitempty"`
+ // Results declares StepResults produced by the step.
+ // This is field is at an alpha stability level and gated by "enable-step-actions" feature flag.
+ // It can be used in an inlined step when used to store results to
+ // $(step.results.resultName.path). When referencing StepActions,
+ // it cannot be used. The results declared by the StepActions will be stored here instead.
+ // Using it along side referencing StepActions will result in a validation error.
+ // +optional
+ // +listType=atomic
+ Results []StepResult `json:"results,omitempty"`
}
// Ref can be used to refer to a specific instance of a StepAction.
diff --git a/pkg/apis/pipeline/v1/openapi_generated.go b/pkg/apis/pipeline/v1/openapi_generated.go
index e16eee08c82..2d33ca1cdb9 100644
--- a/pkg/apis/pipeline/v1/openapi_generated.go
+++ b/pkg/apis/pipeline/v1/openapi_generated.go
@@ -70,6 +70,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.SkippedTask": schema_pkg_apis_pipeline_v1_SkippedTask(ref),
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Step": schema_pkg_apis_pipeline_v1_Step(ref),
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepOutputConfig": schema_pkg_apis_pipeline_v1_StepOutputConfig(ref),
+ "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepResult": schema_pkg_apis_pipeline_v1_StepResult(ref),
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepState": schema_pkg_apis_pipeline_v1_StepState(ref),
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepTemplate": schema_pkg_apis_pipeline_v1_StepTemplate(ref),
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Task": schema_pkg_apis_pipeline_v1_Task(ref),
@@ -2985,12 +2986,31 @@ func schema_pkg_apis_pipeline_v1_Step(ref common.ReferenceCallback) common.OpenA
},
},
},
+ "results": {
+ VendorExtensible: spec.VendorExtensible{
+ Extensions: spec.Extensions{
+ "x-kubernetes-list-type": "atomic",
+ },
+ },
+ SchemaProps: spec.SchemaProps{
+ Description: "Results declares StepResults produced by the step. This is field is at an alpha stability level and gated by \"enable-step-actions\" feature flag. It can be used in an inlined step when used to store results to $(step.results.resultName.path). When referencing StepActions, it cannot be used. The results declared by the StepActions will be stored here instead. Using it along side referencing StepActions will result in a validation error.",
+ 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/v1.StepResult"),
+ },
+ },
+ },
+ },
+ },
},
Required: []string{"name"},
},
},
Dependencies: []string{
- "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Param", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Ref", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepOutputConfig", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.WorkspaceUsage", "k8s.io/api/core/v1.EnvFromSource", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.ResourceRequirements", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.VolumeDevice", "k8s.io/api/core/v1.VolumeMount", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration"},
+ "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Param", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Ref", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepOutputConfig", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepResult", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.WorkspaceUsage", "k8s.io/api/core/v1.EnvFromSource", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.ResourceRequirements", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.VolumeDevice", "k8s.io/api/core/v1.VolumeMount", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration"},
}
}
@@ -3014,6 +3034,59 @@ func schema_pkg_apis_pipeline_v1_StepOutputConfig(ref common.ReferenceCallback)
}
}
+func schema_pkg_apis_pipeline_v1_StepResult(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "StepResult used to describe the results of a step action",
+ 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/v1.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/v1.PropertySpec"},
+ }
+}
+
func schema_pkg_apis_pipeline_v1_StepState(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
diff --git a/pkg/apis/pipeline/v1/result_defaults.go b/pkg/apis/pipeline/v1/result_defaults.go
index 7fc6733b459..51dc7bd7e80 100644
--- a/pkg/apis/pipeline/v1/result_defaults.go
+++ b/pkg/apis/pipeline/v1/result_defaults.go
@@ -37,3 +37,26 @@ func (tr *TaskResult) SetDefaults(context.Context) {
}
}
}
+
+// SetDefaults set the default type for StepResult
+func (sr *StepResult) SetDefaults(context.Context) {
+ if sr == nil {
+ return
+ }
+ if sr.Type == "" {
+ if sr.Properties != nil {
+ // Set type to object if `properties` is given
+ sr.Type = ResultsTypeObject
+ } else {
+ // ResultsTypeString is the default value
+ sr.Type = ResultsTypeString
+ }
+ }
+
+ // Set default type of object values to string
+ for key, propertySpec := range sr.Properties {
+ if propertySpec.Type == "" {
+ sr.Properties[key] = PropertySpec{Type: ParamType(ResultsTypeString)}
+ }
+ }
+}
diff --git a/pkg/apis/pipeline/v1/result_defaults_test.go b/pkg/apis/pipeline/v1/result_defaults_test.go
index 2a160da3671..a611983dec1 100644
--- a/pkg/apis/pipeline/v1/result_defaults_test.go
+++ b/pkg/apis/pipeline/v1/result_defaults_test.go
@@ -93,3 +93,75 @@ func TestTaskResult_SetDefaults(t *testing.T) {
})
}
}
+
+func TestStepResult_SetDefaults(t *testing.T) {
+ tests := []struct {
+ name string
+ before *v1.StepResult
+ after *v1.StepResult
+ }{{
+ name: "empty taskresult",
+ before: nil,
+ after: nil,
+ }, {
+ name: "inferred string type",
+ before: &v1.StepResult{
+ Name: "resultname",
+ },
+ after: &v1.StepResult{
+ Name: "resultname",
+ Type: v1.ResultsTypeString,
+ },
+ }, {
+ name: "string type specified not changed",
+ before: &v1.StepResult{
+ Name: "resultname",
+ Type: v1.ResultsTypeString,
+ },
+ after: &v1.StepResult{
+ Name: "resultname",
+ Type: v1.ResultsTypeString,
+ },
+ }, {
+ name: "array type specified not changed",
+ before: &v1.StepResult{
+ Name: "resultname",
+ Type: v1.ResultsTypeArray,
+ },
+ after: &v1.StepResult{
+ Name: "resultname",
+ Type: v1.ResultsTypeArray,
+ },
+ }, {
+ name: "inferred object type from properties - PropertySpec type is provided",
+ before: &v1.StepResult{
+ Name: "resultname",
+ Properties: map[string]v1.PropertySpec{"key1": {Type: v1.ParamTypeString}},
+ },
+ after: &v1.StepResult{
+ Name: "resultname",
+ Type: v1.ResultsTypeObject,
+ Properties: map[string]v1.PropertySpec{"key1": {Type: v1.ParamTypeString}},
+ },
+ }, {
+ name: "inferred type from properties - PropertySpec type is not provided",
+ before: &v1.StepResult{
+ Name: "resultname",
+ Properties: map[string]v1.PropertySpec{"key1": {}},
+ },
+ after: &v1.StepResult{
+ Name: "resultname",
+ Type: v1.ResultsTypeObject,
+ Properties: map[string]v1.PropertySpec{"key1": {Type: v1.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/v1/result_types.go b/pkg/apis/pipeline/v1/result_types.go
index 59083ede906..99a576f7abb 100644
--- a/pkg/apis/pipeline/v1/result_types.go
+++ b/pkg/apis/pipeline/v1/result_types.go
@@ -38,6 +38,25 @@ type TaskResult struct {
Value *ResultValue `json:"value,omitempty"`
}
+// StepResult used to describe the results of a step action
+type StepResult 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"`
+}
+
// TaskRunResult used to describe the results of a task
type TaskRunResult struct {
// Name the given name
diff --git a/pkg/apis/pipeline/v1/result_validation.go b/pkg/apis/pipeline/v1/result_validation.go
index 7122e18ef60..ec391042b70 100644
--- a/pkg/apis/pipeline/v1/result_validation.go
+++ b/pkg/apis/pipeline/v1/result_validation.go
@@ -111,6 +111,50 @@ func (tr TaskResult) validateValue(ctx context.Context) (errs *apis.FieldError)
return errs
}
+// Validate implements apis.Validatable
+func (sr StepResult) Validate(ctx context.Context) (errs *apis.FieldError) {
+ if !resultNameFormatRegex.MatchString(sr.Name) {
+ return apis.ErrInvalidKeyName(sr.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 sr.Type == ResultsTypeObject:
+ return validateObjectStepResult(sr)
+ case sr.Type == ResultsTypeArray:
+ return nil
+ // The Type is string by default if it is empty.
+ case sr.Type == "":
+ return nil
+ case sr.Type == ResultsTypeString:
+ return nil
+ default:
+ return apis.ErrInvalidValue(sr.Type, "type", fmt.Sprintf("invalid type %s", sr.Type))
+ }
+}
+
+// validateObjectStepResult validates the object result and check if the Properties is missing
+// for Properties values it will check if the type is string.
+func validateObjectStepResult(sr StepResult) (errs *apis.FieldError) {
+ if ParamType(sr.Type) == ParamTypeObject && sr.Properties == nil {
+ return apis.ErrMissingField(fmt.Sprintf("%s.properties", sr.Name))
+ }
+
+ invalidKeys := []string{}
+ for key, propertySpec := range sr.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", sr.Name)},
+ }
+ }
+ return nil
+}
+
// ExtractStepResultName extracts the step name and result name from a string matching
// formtat $(steps..results.).
// If a match is not found, an error is retured.
diff --git a/pkg/apis/pipeline/v1/result_validation_test.go b/pkg/apis/pipeline/v1/result_validation_test.go
index 664c09f54fa..4026e0b923c 100644
--- a/pkg/apis/pipeline/v1/result_validation_test.go
+++ b/pkg/apis/pipeline/v1/result_validation_test.go
@@ -331,3 +331,112 @@ func TestExtractStepResultNameError(t *testing.T) {
})
}
}
+
+func TestStepResultsValidate(t *testing.T) {
+ tests := []struct {
+ name string
+ Result v1.StepResult
+ }{{
+ name: "valid result type empty",
+ Result: v1.StepResult{
+ Name: "MY-RESULT",
+ Description: "my great result",
+ },
+ }, {
+ name: "valid result type string",
+ Result: v1.StepResult{
+ Name: "MY-RESULT",
+ Type: v1.ResultsTypeString,
+ Description: "my great result",
+ },
+ }, {
+ name: "valid result type array",
+ Result: v1.StepResult{
+ Name: "MY-RESULT",
+ Type: v1.ResultsTypeArray,
+ Description: "my great result",
+ },
+ }, {
+ name: "valid result type object",
+ Result: v1.StepResult{
+ Name: "MY-RESULT",
+ Type: v1.ResultsTypeObject,
+ Description: "my great result",
+ Properties: map[string]v1.PropertySpec{"hello": {Type: v1.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 TestStepResultsValidateError(t *testing.T) {
+ tests := []struct {
+ name string
+ Result v1.StepResult
+ expectedError apis.FieldError
+ }{{
+ name: "invalid result name",
+ Result: v1.StepResult{
+ Name: "_MY-RESULT",
+ Type: v1.ResultsTypeString,
+ Description: "wrong name",
+ },
+ expectedError: apis.FieldError{
+ Message: `invalid key name "_MY-RESULT"`,
+ Paths: []string{"name"},
+ Details: "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 '^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$')",
+ },
+ }, {
+ name: "invalid result type",
+ Result: v1.StepResult{
+ Name: "MY-RESULT",
+ Type: "wrong",
+ Description: "my great result",
+ },
+ expectedError: apis.FieldError{
+ Message: `invalid value: wrong`,
+ Paths: []string{"type"},
+ Details: "invalid type wrong",
+ },
+ }, {
+ name: "invalid object properties type",
+ Result: v1.StepResult{
+ Name: "MY-RESULT",
+ Type: v1.ResultsTypeObject,
+ Description: "my great result",
+ Properties: map[string]v1.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: v1.StepResult{
+ Name: "MY-RESULT",
+ Type: v1.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/v1/swagger.json b/pkg/apis/pipeline/v1/swagger.json
index 23b19292f3b..d6fa25ab9c8 100644
--- a/pkg/apis/pipeline/v1/swagger.json
+++ b/pkg/apis/pipeline/v1/swagger.json
@@ -1476,6 +1476,15 @@
"description": "Contains the reference to an existing StepAction.",
"$ref": "#/definitions/v1.Ref"
},
+ "results": {
+ "description": "Results declares StepResults produced by the step. This is field is at an alpha stability level and gated by \"enable-step-actions\" feature flag. It can be used in an inlined step when used to store results to $(step.results.resultName.path). When referencing StepActions, it cannot be used. The results declared by the StepActions will be stored here instead. Using it along side referencing StepActions will result in a validation error.",
+ "type": "array",
+ "items": {
+ "default": {},
+ "$ref": "#/definitions/v1.StepResult"
+ },
+ "x-kubernetes-list-type": "atomic"
+ },
"script": {
"description": "Script is the contents of an executable file to execute.\n\nIf Script is not empty, the Step cannot have an Command and the Args will be passed to the Script.",
"type": "string"
@@ -1543,6 +1552,36 @@
}
}
},
+ "v1.StepResult": {
+ "description": "StepResult used to describe the results of a step action",
+ "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/v1.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"
+ }
+ }
+ },
"v1.StepState": {
"description": "StepState reports the results of running a step in a Task.",
"type": "object",
diff --git a/pkg/apis/pipeline/v1/task_validation.go b/pkg/apis/pipeline/v1/task_validation.go
index 7fb4c70be9d..1711eae77f3 100644
--- a/pkg/apis/pipeline/v1/task_validation.go
+++ b/pkg/apis/pipeline/v1/task_validation.go
@@ -259,16 +259,24 @@ func validateSteps(ctx context.Context, steps []Step) (errs *apis.FieldError) {
names := sets.NewString()
for idx, s := range steps {
errs = errs.Also(validateStep(ctx, s, names).ViaIndex(idx))
+ if s.Results != nil {
+ errs = errs.Also(ValidateStepResultsVariables(ctx, s.Results, s.Script).ViaIndex(idx))
+ errs = errs.Also(ValidateStepResults(ctx, s.Results).ViaIndex(idx).ViaField("results"))
+ }
}
return errs
}
+func isCreateOrUpdate(ctx context.Context) bool {
+ return apis.IsInCreate(ctx) || apis.IsInUpdate(ctx)
+}
+
func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.FieldError) {
if s.Ref != nil {
- if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableStepActions {
+ if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableStepActions && isCreateOrUpdate(ctx) {
return apis.ErrGeneric("feature flag %s should be set to true to reference StepActions in Steps.", config.EnableStepActions)
}
- errs = s.Ref.Validate(ctx)
+ errs = errs.Also(s.Ref.Validate(ctx))
if s.Image != "" {
errs = errs.Also(&apis.FieldError{
Message: "image cannot be used with Ref",
@@ -305,6 +313,12 @@ func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.Fi
Paths: []string{"volumeMounts"},
})
}
+ if len(s.Results) > 0 {
+ errs = errs.Also(&apis.FieldError{
+ Message: "results cannot be used with Ref",
+ Paths: []string{"results"},
+ })
+ }
} else {
if len(s.Params) > 0 {
errs = errs.Also(&apis.FieldError{
@@ -312,6 +326,11 @@ func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.Fi
Paths: []string{"params"},
})
}
+ if len(s.Results) > 0 {
+ if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableStepActions && isCreateOrUpdate(ctx) {
+ return apis.ErrGeneric("feature flag %s should be set to true in order to use Results in Steps.", config.EnableStepActions)
+ }
+ }
if s.Image == "" {
errs = errs.Also(apis.ErrMissingField("Image"))
}
@@ -673,3 +692,22 @@ func (ts *TaskSpec) GetIndexingReferencesToArrayParams() sets.String {
}
return sets.NewString(arrayIndexParamRefs...)
}
+
+// ValidateStepResults validates that all of the declared StepResults are valid.
+func ValidateStepResults(ctx context.Context, results []StepResult) (errs *apis.FieldError) {
+ for index, result := range results {
+ errs = errs.Also(result.Validate(ctx).ViaIndex(index))
+ }
+ return errs
+}
+
+// ValidateStepResultsVariables validates if the StepResults referenced in step script are defined in step's results.
+func ValidateStepResultsVariables(ctx context.Context, results []StepResult, script string) (errs *apis.FieldError) {
+ resultsNames := sets.NewString()
+ for _, r := range results {
+ resultsNames.Insert(r.Name)
+ }
+ errs = errs.Also(substitution.ValidateNoReferencesToUnknownVariables(script, "step.results", resultsNames).ViaField("script"))
+ errs = errs.Also(substitution.ValidateNoReferencesToUnknownVariables(script, "results", resultsNames).ViaField("script"))
+ return errs
+}
diff --git a/pkg/apis/pipeline/v1/task_validation_test.go b/pkg/apis/pipeline/v1/task_validation_test.go
index 5bc80e018fc..9da4d19774d 100644
--- a/pkg/apis/pipeline/v1/task_validation_test.go
+++ b/pkg/apis/pipeline/v1/task_validation_test.go
@@ -1413,26 +1413,82 @@ func TestTaskSpecValidateError(t *testing.T) {
}
}
-func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
+func TestTaskSpecValidateErrorWithStepActionRef_CreateUpdateEvent(t *testing.T) {
tests := []struct {
- name string
- Steps []v1.Step
- enableStepActions bool
- expectedError apis.FieldError
+ name string
+ Steps []v1.Step
+ isCreate bool
+ isUpdate bool
+ expectedError apis.FieldError
}{{
- name: "invalid Task Spec - enableStepActions not on",
+ name: "is create ctx",
Steps: []v1.Step{{
- Image: "image",
Ref: &v1.Ref{
Name: "stepAction",
},
}},
- enableStepActions: false,
+ isCreate: true,
+ isUpdate: false,
expectedError: apis.FieldError{
Message: "feature flag %s should be set to true to reference StepActions in Steps.",
Paths: []string{"steps[0].enable-step-actions"},
},
}, {
+ name: "is update ctx",
+ Steps: []v1.Step{{
+ Ref: &v1.Ref{
+ Name: "stepAction",
+ },
+ }},
+ isCreate: false,
+ isUpdate: true,
+ expectedError: apis.FieldError{
+ Message: "feature flag %s should be set to true to reference StepActions in Steps.",
+ Paths: []string{"steps[0].enable-step-actions"},
+ },
+ }, {
+ name: "ctx is not create or update",
+ Steps: []v1.Step{{
+ Ref: &v1.Ref{
+ Name: "stepAction",
+ },
+ }},
+ isCreate: false,
+ isUpdate: false,
+ expectedError: apis.FieldError{},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ts := v1.TaskSpec{
+ Steps: tt.Steps,
+ }
+ ctx := config.ToContext(context.Background(), &config.Config{
+ FeatureFlags: &config.FeatureFlags{
+ EnableStepActions: false,
+ },
+ })
+ if tt.isCreate {
+ ctx = apis.WithinCreate(ctx)
+ }
+ if tt.isUpdate {
+ ctx = apis.WithinUpdate(ctx, apis.GetBaseline(ctx))
+ }
+ ts.SetDefaults(ctx)
+ err := ts.Validate(ctx)
+ if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" {
+ t.Errorf("TaskSpec.Validate() errors diff %s", diff.PrintWantGot(d))
+ }
+ })
+ }
+}
+
+func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
+ tests := []struct {
+ name string
+ Steps []v1.Step
+ expectedError apis.FieldError
+ }{{
name: "Cannot use image with Ref",
Steps: []v1.Step{{
Ref: &v1.Ref{
@@ -1440,7 +1496,6 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
},
Image: "foo",
}},
- enableStepActions: true,
expectedError: apis.FieldError{
Message: "image cannot be used with Ref",
Paths: []string{"steps[0].image"},
@@ -1453,7 +1508,6 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
},
Command: []string{"foo"},
}},
- enableStepActions: true,
expectedError: apis.FieldError{
Message: "command cannot be used with Ref",
Paths: []string{"steps[0].command"},
@@ -1466,7 +1520,6 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
},
Args: []string{"foo"},
}},
- enableStepActions: true,
expectedError: apis.FieldError{
Message: "args cannot be used with Ref",
Paths: []string{"steps[0].args"},
@@ -1479,7 +1532,6 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
},
Script: "echo hi",
}},
- enableStepActions: true,
expectedError: apis.FieldError{
Message: "script cannot be used with Ref",
Paths: []string{"steps[0].script"},
@@ -1495,7 +1547,6 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
Value: "value1",
}},
}},
- enableStepActions: true,
expectedError: apis.FieldError{
Message: "env cannot be used with Ref",
Paths: []string{"steps[0].env"},
@@ -1508,7 +1559,6 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
Name: "param",
}},
}},
- enableStepActions: true,
expectedError: apis.FieldError{
Message: "params cannot be used without Ref",
Paths: []string{"steps[0].params"},
@@ -1524,11 +1574,24 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
MountPath: "/registry-config",
}},
}},
- enableStepActions: true,
expectedError: apis.FieldError{
Message: "volumeMounts cannot be used with Ref",
Paths: []string{"steps[0].volumeMounts"},
},
+ }, {
+ name: "Cannot use results with Ref",
+ Steps: []v1.Step{{
+ Ref: &v1.Ref{
+ Name: "stepAction",
+ },
+ Results: []v1.StepResult{{
+ Name: "result",
+ }},
+ }},
+ expectedError: apis.FieldError{
+ Message: "results cannot be used with Ref",
+ Paths: []string{"steps[0].results"},
+ },
},
}
for _, tt := range tests {
@@ -1538,9 +1601,10 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
}
ctx := config.ToContext(context.Background(), &config.Config{
FeatureFlags: &config.FeatureFlags{
- EnableStepActions: tt.enableStepActions,
+ EnableStepActions: true,
},
})
+ ctx = apis.WithinCreate(ctx)
ts.SetDefaults(ctx)
err := ts.Validate(ctx)
if err == nil {
@@ -2412,3 +2476,188 @@ func TestParamEnum_Failure(t *testing.T) {
}
}
}
+
+func TestTaskSpecValidate_StepResults(t *testing.T) {
+ type fields struct {
+ Image string
+ Args []string
+ Script string
+ Results []v1.StepResult
+ }
+ tests := []struct {
+ name string
+ fields fields
+ }{{
+ name: "valid result",
+ fields: fields{
+ Image: "my-image",
+ Args: []string{"arg"},
+ Results: []v1.StepResult{{
+ Name: "MY-RESULT",
+ Description: "my great result",
+ }},
+ },
+ }, {
+ name: "valid result type string",
+ fields: fields{
+ Image: "my-image",
+ Args: []string{"arg"},
+ Script: "echo $(step.results.MY-RESULT.path)",
+ Results: []v1.StepResult{{
+ Name: "MY-RESULT",
+ Type: "string",
+ Description: "my great result",
+ }},
+ },
+ }, {
+ name: "valid result type array",
+ fields: fields{
+ Image: "my-image",
+ Args: []string{"arg"},
+ Results: []v1.StepResult{{
+ Name: "MY-RESULT",
+ Type: v1.ResultsTypeArray,
+ Description: "my great result",
+ }},
+ },
+ }, {
+ name: "valid result type object",
+ fields: fields{
+ Image: "my-image",
+ Args: []string{"arg"},
+ Results: []v1.StepResult{{
+ Name: "MY-RESULT",
+ Type: v1.ResultsTypeObject,
+ Description: "my great result",
+ Properties: map[string]v1.PropertySpec{
+ "url": {Type: "string"},
+ "commit": {Type: "string"},
+ },
+ }},
+ },
+ }}
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ts := &v1.TaskSpec{
+ Steps: []v1.Step{{
+ Image: tt.fields.Image,
+ Args: tt.fields.Args,
+ Script: tt.fields.Script,
+ Results: tt.fields.Results,
+ }},
+ }
+ ctx := config.ToContext(context.Background(), &config.Config{
+ FeatureFlags: &config.FeatureFlags{
+ EnableStepActions: true,
+ },
+ })
+ ts.SetDefaults(ctx)
+ if err := ts.Validate(ctx); err != nil {
+ t.Errorf("TaskSpec.Validate() = %v", err)
+ }
+ })
+ }
+}
+
+func TestTaskSpecValidate_StepResults_Error(t *testing.T) {
+ type fields struct {
+ Image string
+ Script string
+ Results []v1.StepResult
+ }
+ tests := []struct {
+ name string
+ fields fields
+ expectedError apis.FieldError
+ isCreate bool
+ isUpdate bool
+ enableStepActions bool
+ }{{
+ name: "step result not allowed withoue enable step actions - create event",
+ fields: fields{
+ Image: "my-image",
+ Results: []v1.StepResult{{Name: "a-result"}},
+ },
+ enableStepActions: false,
+ isCreate: true,
+ expectedError: apis.FieldError{
+ Message: "feature flag %s should be set to true in order to use Results in Steps.",
+ Paths: []string{"steps[0].enable-step-actions"},
+ },
+ }, {
+ name: "step result not allowed withoue enable step actions - update event",
+ fields: fields{
+ Image: "my-image",
+ Results: []v1.StepResult{{Name: "a-result"}},
+ },
+ enableStepActions: false,
+ isUpdate: true,
+ expectedError: apis.FieldError{
+ Message: "feature flag %s should be set to true in order to use Results in Steps.",
+ Paths: []string{"steps[0].enable-step-actions"},
+ },
+ }, {
+ name: "step result not allowed withoue enable step actions - neither create nor update",
+ fields: fields{
+ Image: "my-image",
+ Results: []v1.StepResult{{Name: "a-result"}},
+ },
+ enableStepActions: false,
+ expectedError: apis.FieldError{},
+ }, {
+ name: "step script refers to nonexistent result",
+ fields: fields{
+ Image: "my-image",
+ Script: `
+ #!/usr/bin/env bash
+ date | tee $(results.non-exist.path)`,
+ Results: []v1.StepResult{{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{"steps[0].script"},
+ },
+ enableStepActions: true,
+ }, {
+ name: "step script refers to nonexistent stepresult",
+ fields: fields{
+ Image: "my-image",
+ Script: `
+ #!/usr/bin/env bash
+ date | tee $(step.results.non-exist.path)`,
+ Results: []v1.StepResult{{Name: "a-result"}},
+ },
+ expectedError: apis.FieldError{
+ Message: `non-existent variable in "\n\t\t\t#!/usr/bin/env bash\n\t\t\tdate | tee $(step.results.non-exist.path)"`,
+ Paths: []string{"steps[0].script"},
+ },
+ enableStepActions: true,
+ }}
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ts := &v1.TaskSpec{
+ Steps: []v1.Step{{
+ Image: tt.fields.Image,
+ Script: tt.fields.Script,
+ Results: tt.fields.Results,
+ }},
+ }
+ ctx := config.ToContext(context.Background(), &config.Config{
+ FeatureFlags: &config.FeatureFlags{
+ EnableStepActions: tt.enableStepActions,
+ },
+ })
+ if tt.isCreate {
+ ctx = apis.WithinCreate(ctx)
+ }
+ if tt.isUpdate {
+ ctx = apis.WithinUpdate(ctx, apis.GetBaseline(ctx))
+ }
+ ts.SetDefaults(ctx)
+ err := ts.Validate(ctx)
+ if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" {
+ t.Errorf("StepActionSpec.Validate() errors diff %s", diff.PrintWantGot(d))
+ }
+ })
+ }
+}
diff --git a/pkg/apis/pipeline/v1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1/zz_generated.deepcopy.go
index 628630cf446..dd332d9188a 100644
--- a/pkg/apis/pipeline/v1/zz_generated.deepcopy.go
+++ b/pkg/apis/pipeline/v1/zz_generated.deepcopy.go
@@ -1297,6 +1297,13 @@ func (in *Step) DeepCopyInto(out *Step) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.Results != nil {
+ in, out := &in.Results, &out.Results
+ *out = make([]StepResult, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
return
}
@@ -1326,6 +1333,29 @@ func (in *StepOutputConfig) DeepCopy() *StepOutputConfig {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *StepResult) DeepCopyInto(out *StepResult) {
+ *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 StepResult.
+func (in *StepResult) DeepCopy() *StepResult {
+ if in == nil {
+ return nil
+ }
+ out := new(StepResult)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StepState) DeepCopyInto(out *StepState) {
*out = *in
diff --git a/pkg/apis/pipeline/v1alpha1/openapi_generated.go b/pkg/apis/pipeline/v1alpha1/openapi_generated.go
index cb18896855e..3426a815b7f 100644
--- a/pkg/apis/pipeline/v1alpha1/openapi_generated.go
+++ b/pkg/apis/pipeline/v1alpha1/openapi_generated.go
@@ -41,7 +41,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"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),
@@ -748,59 +747,6 @@ 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/v1.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/v1.PropertySpec"},
- }
-}
-
func schema_pkg_apis_pipeline_v1alpha1_StepActionSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -915,7 +861,7 @@ func schema_pkg_apis_pipeline_v1alpha1_StepActionSpec(ref common.ReferenceCallba
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
- Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.StepActionResult"),
+ Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepResult"),
},
},
},
@@ -952,7 +898,7 @@ func schema_pkg_apis_pipeline_v1alpha1_StepActionSpec(ref common.ReferenceCallba
},
},
Dependencies: []string{
- "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.ParamSpec", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1.StepActionResult", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.VolumeMount"},
+ "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.ParamSpec", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepResult", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.VolumeMount"},
}
}
diff --git a/pkg/apis/pipeline/v1alpha1/result_defaults_test.go b/pkg/apis/pipeline/v1alpha1/result_defaults_test.go
deleted file mode 100644
index 302be5b969a..00000000000
--- a/pkg/apis/pipeline/v1alpha1/result_defaults_test.go
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
-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"
- v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
- 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: v1.ResultsTypeString,
- },
- }, {
- name: "string type specified not changed",
- before: &v1alpha1.StepActionResult{
- Name: "resultname",
- Type: v1.ResultsTypeString,
- },
- after: &v1alpha1.StepActionResult{
- Name: "resultname",
- Type: v1.ResultsTypeString,
- },
- }, {
- name: "array type specified not changed",
- before: &v1alpha1.StepActionResult{
- Name: "resultname",
- Type: v1.ResultsTypeArray,
- },
- after: &v1alpha1.StepActionResult{
- Name: "resultname",
- Type: v1.ResultsTypeArray,
- },
- }, {
- name: "inferred object type from properties - PropertySpec type is provided",
- before: &v1alpha1.StepActionResult{
- Name: "resultname",
- Properties: map[string]v1.PropertySpec{"key1": {Type: v1.ParamTypeString}},
- },
- after: &v1alpha1.StepActionResult{
- Name: "resultname",
- Type: v1.ResultsTypeObject,
- Properties: map[string]v1.PropertySpec{"key1": {Type: v1.ParamTypeString}},
- },
- }, {
- name: "inferred type from properties - PropertySpec type is not provided",
- before: &v1alpha1.StepActionResult{
- Name: "resultname",
- Properties: map[string]v1.PropertySpec{"key1": {}},
- },
- after: &v1alpha1.StepActionResult{
- Name: "resultname",
- Type: v1.ResultsTypeObject,
- Properties: map[string]v1.PropertySpec{"key1": {Type: v1.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
deleted file mode 100644
index df2214875bd..00000000000
--- a/pkg/apis/pipeline/v1alpha1/result_types.go
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
-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"
-
- v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
-)
-
-// 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 v1.ResultsType `json:"type,omitempty"`
-
- // Properties is the JSON Schema properties to support key-value pairs results.
- // +optional
- Properties map[string]v1.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 = v1.ResultsTypeObject
- } else {
- // ResultsTypeString is the default value
- sar.Type = v1.ResultsTypeString
- }
- }
-
- // Set default type of object values to string
- for key, propertySpec := range sar.Properties {
- if propertySpec.Type == "" {
- sar.Properties[key] = v1.PropertySpec{Type: v1.ParamType(v1.ResultsTypeString)}
- }
- }
-}
diff --git a/pkg/apis/pipeline/v1alpha1/result_validation.go b/pkg/apis/pipeline/v1alpha1/result_validation.go
deleted file mode 100644
index 3a63ddb976a..00000000000
--- a/pkg/apis/pipeline/v1alpha1/result_validation.go
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
-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"
-
- v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
- "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 == v1.ResultsTypeObject:
- errs := validateObjectResult(sar)
- return errs
- case sar.Type == v1.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 != v1.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 v1.ParamType(sar.Type) == v1.ParamTypeObject && sar.Properties == nil {
- return apis.ErrMissingField(fmt.Sprintf("%s.properties", sar.Name))
- }
-
- invalidKeys := []string{}
- for key, propertySpec := range sar.Properties {
- if propertySpec.Type != v1.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
deleted file mode 100644
index 72f55096fd6..00000000000
--- a/pkg/apis/pipeline/v1alpha1/result_validation_test.go
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
-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"
- v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
- "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: v1.ResultsTypeString,
- Description: "my great result",
- },
- }, {
- name: "valid result type array",
- Result: StepActionResult{
- Name: "MY-RESULT",
- Type: v1.ResultsTypeArray,
- Description: "my great result",
- },
- }, {
- name: "valid result type object",
- Result: StepActionResult{
- Name: "MY-RESULT",
- Type: v1.ResultsTypeObject,
- Description: "my great result",
- Properties: map[string]v1.PropertySpec{"hello": {Type: v1.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: v1.ResultsTypeObject,
- Description: "my great result",
- Properties: map[string]v1.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: v1.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_types.go b/pkg/apis/pipeline/v1alpha1/stepaction_types.go
index 97752a2b421..4209f1de4ca 100644
--- a/pkg/apis/pipeline/v1alpha1/stepaction_types.go
+++ b/pkg/apis/pipeline/v1alpha1/stepaction_types.go
@@ -120,7 +120,7 @@ type StepActionSpec struct {
// Results are values that this StepAction can output
// +optional
// +listType=atomic
- Results []StepActionResult `json:"results,omitempty"`
+ Results []v1.StepResult `json:"results,omitempty"`
// SecurityContext defines the security options the Step should be run with.
// If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext.
// More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
diff --git a/pkg/apis/pipeline/v1alpha1/stepaction_validation.go b/pkg/apis/pipeline/v1alpha1/stepaction_validation.go
index f170410674a..f2d2ccfb7f2 100644
--- a/pkg/apis/pipeline/v1alpha1/stepaction_validation.go
+++ b/pkg/apis/pipeline/v1alpha1/stepaction_validation.go
@@ -66,8 +66,8 @@ func (ss *StepActionSpec) Validate(ctx context.Context) (errs *apis.FieldError)
errs = errs.Also(validateUsageOfDeclaredParameters(ctx, *ss))
errs = errs.Also(v1.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"))
+ errs = errs.Also(v1.ValidateStepResultsVariables(ctx, ss.Results, ss.Script))
+ errs = errs.Also(v1.ValidateStepResults(ctx, ss.Results).ViaField("results"))
errs = errs.Also(validateVolumeMounts(ss.VolumeMounts, ss.Params).ViaField("volumeMounts"))
return errs
}
@@ -84,13 +84,6 @@ func validateUsageOfDeclaredParameters(ctx context.Context, sas StepActionSpec)
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
-}
-
func validateVolumeMounts(volumeMounts []corev1.VolumeMount, params v1.ParamSpecs) (errs *apis.FieldError) {
if len(volumeMounts) == 0 {
return
@@ -200,14 +193,3 @@ func validateStepActionVariables(ctx context.Context, sas StepActionSpec, prefix
}
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 2288ea4fdae..cd85acdd132 100644
--- a/pkg/apis/pipeline/v1alpha1/stepaction_validation_test.go
+++ b/pkg/apis/pipeline/v1alpha1/stepaction_validation_test.go
@@ -67,7 +67,7 @@ func TestStepActionSpecValidate(t *testing.T) {
Script string
Env []corev1.EnvVar
Params []v1.ParamSpec
- Results []v1alpha1.StepActionResult
+ Results []v1.StepResult
VolumeMounts []corev1.VolumeMount
}
tests := []struct {
@@ -209,7 +209,7 @@ func TestStepActionSpecValidate(t *testing.T) {
fields: fields{
Image: "my-image",
Args: []string{"arg"},
- Results: []v1alpha1.StepActionResult{{
+ Results: []v1.StepResult{{
Name: "MY-RESULT",
Description: "my great result",
}},
@@ -219,7 +219,7 @@ func TestStepActionSpecValidate(t *testing.T) {
fields: fields{
Image: "my-image",
Args: []string{"arg"},
- Results: []v1alpha1.StepActionResult{{
+ Results: []v1.StepResult{{
Name: "MY-RESULT",
Type: "string",
Description: "my great result",
@@ -230,7 +230,7 @@ func TestStepActionSpecValidate(t *testing.T) {
fields: fields{
Image: "my-image",
Args: []string{"arg"},
- Results: []v1alpha1.StepActionResult{{
+ Results: []v1.StepResult{{
Name: "MY-RESULT",
Type: v1.ResultsTypeArray,
Description: "my great result",
@@ -241,7 +241,7 @@ func TestStepActionSpecValidate(t *testing.T) {
fields: fields{
Image: "my-image",
Args: []string{"arg"},
- Results: []v1alpha1.StepActionResult{{
+ Results: []v1.StepResult{{
Name: "MY-RESULT",
Type: v1.ResultsTypeObject,
Description: "my great result",
@@ -310,7 +310,7 @@ func TestStepActionValidateError(t *testing.T) {
Script string
Env []corev1.EnvVar
Params []v1.ParamSpec
- Results []v1alpha1.StepActionResult
+ Results []v1.StepResult
VolumeMounts []corev1.VolumeMount
}
tests := []struct {
@@ -590,7 +590,7 @@ func TestStepActionSpecValidateError(t *testing.T) {
Script string
Env []corev1.EnvVar
Params []v1.ParamSpec
- Results []v1alpha1.StepActionResult
+ Results []v1.StepResult
}
tests := []struct {
name string
@@ -633,12 +633,25 @@ func TestStepActionSpecValidateError(t *testing.T) {
Script: `
#!/usr/bin/env bash
date | tee $(results.non-exist.path)`,
- Results: []v1alpha1.StepActionResult{{Name: "a-result"}},
+ Results: []v1.StepResult{{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: "step script refers to nonexistent stepresult",
+ fields: fields{
+ Image: "my-image",
+ Script: `
+ #!/usr/bin/env bash
+ date | tee $(step.results.non-exist.path)`,
+ Results: []v1.StepResult{{Name: "a-result"}},
+ },
+ expectedError: apis.FieldError{
+ Message: `non-existent variable in "\n\t\t\t#!/usr/bin/env bash\n\t\t\tdate | tee $(step.results.non-exist.path)"`,
+ Paths: []string{"script"},
+ },
}, {
name: "invalid param name format",
fields: fields{
diff --git a/pkg/apis/pipeline/v1alpha1/swagger.json b/pkg/apis/pipeline/v1alpha1/swagger.json
index eea6d5d9a67..4423cb8c3cb 100644
--- a/pkg/apis/pipeline/v1alpha1/swagger.json
+++ b/pkg/apis/pipeline/v1alpha1/swagger.json
@@ -384,36 +384,6 @@
}
}
},
- "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/v1.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",
@@ -465,7 +435,7 @@
"type": "array",
"items": {
"default": {},
- "$ref": "#/definitions/v1alpha1.StepActionResult"
+ "$ref": "#/definitions/v1.StepResult"
},
"x-kubernetes-list-type": "atomic"
},
diff --git a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go
index 4e02d12fe6d..c25623d07dd 100644
--- a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go
@@ -278,29 +278,6 @@ 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]pipelinev1.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
@@ -330,7 +307,7 @@ func (in *StepActionSpec) DeepCopyInto(out *StepActionSpec) {
}
if in.Results != nil {
in, out := &in.Results, &out.Results
- *out = make([]StepActionResult, len(*in))
+ *out = make([]pipelinev1.StepResult, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
diff --git a/pkg/apis/pipeline/v1beta1/container_conversion.go b/pkg/apis/pipeline/v1beta1/container_conversion.go
index 620a655f124..4f025e7dd25 100644
--- a/pkg/apis/pipeline/v1beta1/container_conversion.go
+++ b/pkg/apis/pipeline/v1beta1/container_conversion.go
@@ -71,6 +71,12 @@ func (s Step) convertTo(ctx context.Context, sink *v1.Step) {
p.convertTo(ctx, &new)
sink.Params = append(sink.Params, new)
}
+ sink.Results = nil
+ for _, r := range s.Results {
+ new := v1.StepResult{}
+ r.convertTo(ctx, &new)
+ sink.Results = append(sink.Results, new)
+ }
}
func (s *Step) convertFrom(ctx context.Context, source v1.Step) {
@@ -109,6 +115,12 @@ func (s *Step) convertFrom(ctx context.Context, source v1.Step) {
new.ConvertFrom(ctx, p)
s.Params = append(s.Params, new)
}
+ s.Results = nil
+ for _, r := range source.Results {
+ new := StepResult{}
+ new.convertFrom(ctx, r)
+ s.Results = append(s.Results, new)
+ }
}
func (s StepTemplate) convertTo(ctx context.Context, sink *v1.StepTemplate) {
diff --git a/pkg/apis/pipeline/v1beta1/container_types.go b/pkg/apis/pipeline/v1beta1/container_types.go
index 97bf664d0b3..4f9be74b62f 100644
--- a/pkg/apis/pipeline/v1beta1/container_types.go
+++ b/pkg/apis/pipeline/v1beta1/container_types.go
@@ -236,6 +236,15 @@ type Step struct {
// +optional
// +listType=atomic
Params Params `json:"params,omitempty"`
+ // Results declares StepResults produced by the step.
+ // This is field is at an alpha stability level and gated by "enable-step-actions" feature flag.
+ // It can be used in an inlined step when used to store results to
+ // $(step.results.resultName.path). When referencing StepActions,
+ // it cannot be used. The results declared by the StepActions will be stored here instead.
+ // Using it along side referencing StepActions will result in a validation error.
+ // +optional
+ // +listType=atomic
+ Results []StepResult `json:"results,omitempty"`
}
// Ref can be used to refer to a specific instance of a StepAction.
diff --git a/pkg/apis/pipeline/v1beta1/openapi_generated.go b/pkg/apis/pipeline/v1beta1/openapi_generated.go
index 6408a47d891..cf29c4ad102 100644
--- a/pkg/apis/pipeline/v1beta1/openapi_generated.go
+++ b/pkg/apis/pipeline/v1beta1/openapi_generated.go
@@ -85,6 +85,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.SkippedTask": schema_pkg_apis_pipeline_v1beta1_SkippedTask(ref),
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Step": schema_pkg_apis_pipeline_v1beta1_Step(ref),
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.StepOutputConfig": schema_pkg_apis_pipeline_v1beta1_StepOutputConfig(ref),
+ "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.StepResult": schema_pkg_apis_pipeline_v1beta1_StepResult(ref),
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.StepState": schema_pkg_apis_pipeline_v1beta1_StepState(ref),
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.StepTemplate": schema_pkg_apis_pipeline_v1beta1_StepTemplate(ref),
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Task": schema_pkg_apis_pipeline_v1beta1_Task(ref),
@@ -3939,12 +3940,31 @@ func schema_pkg_apis_pipeline_v1beta1_Step(ref common.ReferenceCallback) common.
},
},
},
+ "results": {
+ VendorExtensible: spec.VendorExtensible{
+ Extensions: spec.Extensions{
+ "x-kubernetes-list-type": "atomic",
+ },
+ },
+ SchemaProps: spec.SchemaProps{
+ Description: "Results declares StepResults produced by the step. This is field is at an alpha stability level and gated by \"enable-step-actions\" feature flag. It can be used in an inlined step when used to store results to $(step.results.resultName.path). When referencing StepActions, it cannot be used. The results declared by the StepActions will be stored here instead. Using it along side referencing StepActions will result in a validation error.",
+ 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/v1beta1.StepResult"),
+ },
+ },
+ },
+ },
+ },
},
Required: []string{"name"},
},
},
Dependencies: []string{
- "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Param", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Ref", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.StepOutputConfig", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WorkspaceUsage", "k8s.io/api/core/v1.ContainerPort", "k8s.io/api/core/v1.EnvFromSource", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.Lifecycle", "k8s.io/api/core/v1.Probe", "k8s.io/api/core/v1.ResourceRequirements", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.VolumeDevice", "k8s.io/api/core/v1.VolumeMount", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration"},
+ "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Param", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Ref", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.StepOutputConfig", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.StepResult", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.WorkspaceUsage", "k8s.io/api/core/v1.ContainerPort", "k8s.io/api/core/v1.EnvFromSource", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.Lifecycle", "k8s.io/api/core/v1.Probe", "k8s.io/api/core/v1.ResourceRequirements", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.VolumeDevice", "k8s.io/api/core/v1.VolumeMount", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration"},
}
}
@@ -3968,6 +3988,59 @@ func schema_pkg_apis_pipeline_v1beta1_StepOutputConfig(ref common.ReferenceCallb
}
}
+func schema_pkg_apis_pipeline_v1beta1_StepResult(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "StepResult used to describe the results of a step action",
+ 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/v1beta1.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/v1beta1.PropertySpec"},
+ }
+}
+
func schema_pkg_apis_pipeline_v1beta1_StepState(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
diff --git a/pkg/apis/pipeline/v1beta1/result_conversion.go b/pkg/apis/pipeline/v1beta1/result_conversion.go
index 8854a283cb7..46d7bcab3ff 100644
--- a/pkg/apis/pipeline/v1beta1/result_conversion.go
+++ b/pkg/apis/pipeline/v1beta1/result_conversion.go
@@ -55,3 +55,29 @@ func (r *TaskResult) convertFrom(ctx context.Context, source v1.TaskResult) {
r.Value.convertFrom(ctx, *source.Value)
}
}
+
+func (r StepResult) convertTo(ctx context.Context, sink *v1.StepResult) {
+ sink.Name = r.Name
+ sink.Type = v1.ResultsType(r.Type)
+ sink.Description = r.Description
+ if r.Properties != nil {
+ properties := make(map[string]v1.PropertySpec)
+ for k, v := range r.Properties {
+ properties[k] = v1.PropertySpec{Type: v1.ParamType(v.Type)}
+ }
+ sink.Properties = properties
+ }
+}
+
+func (r *StepResult) convertFrom(ctx context.Context, source v1.StepResult) {
+ r.Name = source.Name
+ r.Type = ResultsType(source.Type)
+ r.Description = source.Description
+ if source.Properties != nil {
+ properties := make(map[string]PropertySpec)
+ for k, v := range source.Properties {
+ properties[k] = PropertySpec{Type: ParamType(v.Type)}
+ }
+ r.Properties = properties
+ }
+}
diff --git a/pkg/apis/pipeline/v1beta1/result_defaults.go b/pkg/apis/pipeline/v1beta1/result_defaults.go
index 12af7aa36ef..c1f80a33470 100644
--- a/pkg/apis/pipeline/v1beta1/result_defaults.go
+++ b/pkg/apis/pipeline/v1beta1/result_defaults.go
@@ -37,3 +37,25 @@ func (tr *TaskResult) SetDefaults(context.Context) {
}
}
}
+
+// SetDefaults set the default type for StepResult
+func (sr *StepResult) SetDefaults(context.Context) {
+ if sr == nil {
+ return
+ }
+ if sr.Type == "" {
+ if sr.Properties != nil {
+ // Set type to object if `properties` is given
+ sr.Type = ResultsTypeObject
+ } else {
+ // ResultsTypeString is the default value
+ sr.Type = ResultsTypeString
+ }
+ }
+ // Set default type of object values to string
+ for key, propertySpec := range sr.Properties {
+ if propertySpec.Type == "" {
+ sr.Properties[key] = PropertySpec{Type: ParamType(ResultsTypeString)}
+ }
+ }
+}
diff --git a/pkg/apis/pipeline/v1beta1/result_defaults_test.go b/pkg/apis/pipeline/v1beta1/result_defaults_test.go
index 13dd6799233..1853a05be22 100644
--- a/pkg/apis/pipeline/v1beta1/result_defaults_test.go
+++ b/pkg/apis/pipeline/v1beta1/result_defaults_test.go
@@ -93,3 +93,75 @@ func TestTaskResult_SetDefaults(t *testing.T) {
})
}
}
+
+func TestStepResult_SetDefaults(t *testing.T) {
+ tests := []struct {
+ name string
+ before *v1beta1.StepResult
+ after *v1beta1.StepResult
+ }{{
+ name: "empty taskresult",
+ before: nil,
+ after: nil,
+ }, {
+ name: "inferred string type",
+ before: &v1beta1.StepResult{
+ Name: "resultname",
+ },
+ after: &v1beta1.StepResult{
+ Name: "resultname",
+ Type: v1beta1.ResultsTypeString,
+ },
+ }, {
+ name: "string type specified not changed",
+ before: &v1beta1.StepResult{
+ Name: "resultname",
+ Type: v1beta1.ResultsTypeString,
+ },
+ after: &v1beta1.StepResult{
+ Name: "resultname",
+ Type: v1beta1.ResultsTypeString,
+ },
+ }, {
+ name: "array type specified not changed",
+ before: &v1beta1.StepResult{
+ Name: "resultname",
+ Type: v1beta1.ResultsTypeArray,
+ },
+ after: &v1beta1.StepResult{
+ Name: "resultname",
+ Type: v1beta1.ResultsTypeArray,
+ },
+ }, {
+ name: "inferred object type from properties - PropertySpec type is provided",
+ before: &v1beta1.StepResult{
+ Name: "resultname",
+ Properties: map[string]v1beta1.PropertySpec{"key1": {Type: v1beta1.ParamTypeString}},
+ },
+ after: &v1beta1.StepResult{
+ Name: "resultname",
+ Type: v1beta1.ResultsTypeObject,
+ Properties: map[string]v1beta1.PropertySpec{"key1": {Type: v1beta1.ParamTypeString}},
+ },
+ }, {
+ name: "inferred type from properties - PropertySpec type is not provided",
+ before: &v1beta1.StepResult{
+ Name: "resultname",
+ Properties: map[string]v1beta1.PropertySpec{"key1": {}},
+ },
+ after: &v1beta1.StepResult{
+ Name: "resultname",
+ Type: v1beta1.ResultsTypeObject,
+ Properties: map[string]v1beta1.PropertySpec{"key1": {Type: v1beta1.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/v1beta1/result_types.go b/pkg/apis/pipeline/v1beta1/result_types.go
index 2f484f47ddd..225c5791d23 100644
--- a/pkg/apis/pipeline/v1beta1/result_types.go
+++ b/pkg/apis/pipeline/v1beta1/result_types.go
@@ -38,6 +38,22 @@ type TaskResult struct {
Value *ResultValue `json:"value,omitempty"`
}
+// StepResult used to describe the results of a step action
+type StepResult 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"`
+}
+
// TaskRunResult used to describe the results of a task
type TaskRunResult struct {
// Name the given name
diff --git a/pkg/apis/pipeline/v1beta1/result_validation.go b/pkg/apis/pipeline/v1beta1/result_validation.go
index a9f776b5277..9e7b4987cf3 100644
--- a/pkg/apis/pipeline/v1beta1/result_validation.go
+++ b/pkg/apis/pipeline/v1beta1/result_validation.go
@@ -110,3 +110,44 @@ func (tr TaskResult) validateValue(ctx context.Context) (errs *apis.FieldError)
}
return errs
}
+
+// Validate implements apis.Validatable
+func (sr StepResult) Validate(ctx context.Context) (errs *apis.FieldError) {
+ if !resultNameFormatRegex.MatchString(sr.Name) {
+ return apis.ErrInvalidKeyName(sr.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 sr.Type == ResultsTypeObject:
+ return validateObjectStepResult(sr)
+ case sr.Type == ResultsTypeArray:
+ return nil
+ // The Type is string by default if it is empty.
+ case sr.Type == "":
+ return nil
+ case sr.Type == ResultsTypeString:
+ return nil
+ default:
+ return apis.ErrInvalidValue(sr.Type, "type", fmt.Sprintf("invalid type %s", sr.Type))
+ }
+}
+
+// validateObjectStepResult validates the object result and check if the Properties is missing
+// for Properties values it will check if the type is string.
+func validateObjectStepResult(sr StepResult) (errs *apis.FieldError) {
+ if ParamType(sr.Type) == ParamTypeObject && sr.Properties == nil {
+ return apis.ErrMissingField(fmt.Sprintf("%s.properties", sr.Name))
+ }
+ invalidKeys := []string{}
+ for key, propertySpec := range sr.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", sr.Name)},
+ }
+ }
+ return nil
+}
diff --git a/pkg/apis/pipeline/v1beta1/result_validation_test.go b/pkg/apis/pipeline/v1beta1/result_validation_test.go
index 18d01f640fb..434db1c929e 100644
--- a/pkg/apis/pipeline/v1beta1/result_validation_test.go
+++ b/pkg/apis/pipeline/v1beta1/result_validation_test.go
@@ -278,3 +278,112 @@ func TestResultsValidateValueError(t *testing.T) {
})
}
}
+
+func TestStepResultsValidate(t *testing.T) {
+ tests := []struct {
+ name string
+ Result v1beta1.StepResult
+ }{{
+ name: "valid result type empty",
+ Result: v1beta1.StepResult{
+ Name: "MY-RESULT",
+ Description: "my great result",
+ },
+ }, {
+ name: "valid result type string",
+ Result: v1beta1.StepResult{
+ Name: "MY-RESULT",
+ Type: v1beta1.ResultsTypeString,
+ Description: "my great result",
+ },
+ }, {
+ name: "valid result type array",
+ Result: v1beta1.StepResult{
+ Name: "MY-RESULT",
+ Type: v1beta1.ResultsTypeArray,
+ Description: "my great result",
+ },
+ }, {
+ name: "valid result type object",
+ Result: v1beta1.StepResult{
+ Name: "MY-RESULT",
+ Type: v1beta1.ResultsTypeObject,
+ Description: "my great result",
+ Properties: map[string]v1beta1.PropertySpec{"hello": {Type: v1beta1.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 TestStepResultsValidateError(t *testing.T) {
+ tests := []struct {
+ name string
+ Result v1beta1.StepResult
+ expectedError apis.FieldError
+ }{{
+ name: "invalid result name",
+ Result: v1beta1.StepResult{
+ Name: "_MY-RESULT",
+ Type: v1beta1.ResultsTypeString,
+ Description: "wrong name",
+ },
+ expectedError: apis.FieldError{
+ Message: `invalid key name "_MY-RESULT"`,
+ Paths: []string{"name"},
+ Details: "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 '^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$')",
+ },
+ }, {
+ name: "invalid result type",
+ Result: v1beta1.StepResult{
+ Name: "MY-RESULT",
+ Type: "wrong",
+ Description: "my great result",
+ },
+ expectedError: apis.FieldError{
+ Message: `invalid value: wrong`,
+ Paths: []string{"type"},
+ Details: "invalid type wrong",
+ },
+ }, {
+ name: "invalid object properties type",
+ Result: v1beta1.StepResult{
+ Name: "MY-RESULT",
+ Type: v1beta1.ResultsTypeObject,
+ Description: "my great result",
+ Properties: map[string]v1beta1.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: v1beta1.StepResult{
+ Name: "MY-RESULT",
+ Type: v1beta1.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/v1beta1/swagger.json b/pkg/apis/pipeline/v1beta1/swagger.json
index 6df62472fbe..8ad1cb6754d 100644
--- a/pkg/apis/pipeline/v1beta1/swagger.json
+++ b/pkg/apis/pipeline/v1beta1/swagger.json
@@ -2107,6 +2107,15 @@
"default": {},
"$ref": "#/definitions/v1.ResourceRequirements"
},
+ "results": {
+ "description": "Results declares StepResults produced by the step. This is field is at an alpha stability level and gated by \"enable-step-actions\" feature flag. It can be used in an inlined step when used to store results to $(step.results.resultName.path). When referencing StepActions, it cannot be used. The results declared by the StepActions will be stored here instead. Using it along side referencing StepActions will result in a validation error.",
+ "type": "array",
+ "items": {
+ "default": {},
+ "$ref": "#/definitions/v1beta1.StepResult"
+ },
+ "x-kubernetes-list-type": "atomic"
+ },
"script": {
"description": "Script is the contents of an executable file to execute.\n\nIf Script is not empty, the Step cannot have an Command and the Args will be passed to the Script.",
"type": "string"
@@ -2198,6 +2207,36 @@
}
}
},
+ "v1beta1.StepResult": {
+ "description": "StepResult used to describe the results of a step action",
+ "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/v1beta1.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"
+ }
+ }
+ },
"v1beta1.StepState": {
"description": "StepState reports the results of running a step in a Task.",
"type": "object",
diff --git a/pkg/apis/pipeline/v1beta1/task_conversion_test.go b/pkg/apis/pipeline/v1beta1/task_conversion_test.go
index 1f59a33c18f..ddf97617f8c 100644
--- a/pkg/apis/pipeline/v1beta1/task_conversion_test.go
+++ b/pkg/apis/pipeline/v1beta1/task_conversion_test.go
@@ -74,6 +74,27 @@ spec:
steps:
- image: foo
- image: bar
+`
+ stepResultTaskYAML := `
+metadata:
+ name: foo
+ namespace: bar
+ generation: 1
+spec:
+ displayName: "task-display-name"
+ description: test
+ steps:
+ - image: foo
+ results:
+ - name: res
+ type: string
+ - name: arr
+ type: array
+ - name: obj
+ type: object
+ properties:
+ key:
+ type: string
`
stepActionTaskYAML := `
metadata:
@@ -306,6 +327,9 @@ spec:
multiStepTaskV1beta1 := parse.MustParseV1beta1Task(t, multiStepTaskYAML)
multiStepTaskV1 := parse.MustParseV1Task(t, multiStepTaskYAML)
+ stepResultTaskV1beta1 := parse.MustParseV1beta1Task(t, stepResultTaskYAML)
+ stepResultTaskV1 := parse.MustParseV1Task(t, stepResultTaskYAML)
+
stepActionTaskV1beta1 := parse.MustParseV1beta1Task(t, stepActionTaskYAML)
stepActionTaskV1 := parse.MustParseV1Task(t, stepActionTaskYAML)
@@ -347,6 +371,10 @@ spec:
name: "task conversion all non deprecated fields",
v1beta1Task: taskWithAllNoDeprecatedFieldsV1beta1,
v1Task: taskWithAllNoDeprecatedFieldsV1,
+ }, {
+ name: "step results in task",
+ v1beta1Task: stepResultTaskV1beta1,
+ v1Task: stepResultTaskV1,
}, {
name: "step action in task",
v1beta1Task: stepActionTaskV1beta1,
diff --git a/pkg/apis/pipeline/v1beta1/task_validation.go b/pkg/apis/pipeline/v1beta1/task_validation.go
index 127d20cd788..1f354c9aa11 100644
--- a/pkg/apis/pipeline/v1beta1/task_validation.go
+++ b/pkg/apis/pipeline/v1beta1/task_validation.go
@@ -265,13 +265,21 @@ func validateSteps(ctx context.Context, steps []Step) (errs *apis.FieldError) {
names := sets.NewString()
for idx, s := range steps {
errs = errs.Also(validateStep(ctx, s, names).ViaIndex(idx))
+ if s.Results != nil {
+ errs = errs.Also(validateStepResultsVariables(ctx, s.Results, s.Script).ViaIndex(idx))
+ errs = errs.Also(validateStepResults(ctx, s.Results).ViaIndex(idx).ViaField("results"))
+ }
}
return errs
}
+func isCreateOrUpdate(ctx context.Context) bool {
+ return apis.IsInCreate(ctx) || apis.IsInUpdate(ctx)
+}
+
func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.FieldError) {
if s.Ref != nil {
- if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableStepActions {
+ if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableStepActions && isCreateOrUpdate(ctx) {
return apis.ErrGeneric("feature flag %s should be set to true to reference StepActions in Steps.", config.EnableStepActions)
}
errs = errs.Also(s.Ref.Validate(ctx))
@@ -311,6 +319,12 @@ func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.Fi
Paths: []string{"volumeMounts"},
})
}
+ if len(s.Results) > 0 {
+ errs = errs.Also(&apis.FieldError{
+ Message: "results cannot be used with Ref",
+ Paths: []string{"results"},
+ })
+ }
} else {
if len(s.Params) > 0 {
errs = errs.Also(&apis.FieldError{
@@ -318,6 +332,11 @@ func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.Fi
Paths: []string{"params"},
})
}
+ if len(s.Results) > 0 {
+ if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableStepActions && isCreateOrUpdate(ctx) {
+ return apis.ErrGeneric("feature flag %s should be set to true in order to use Results in Steps.", config.EnableStepActions)
+ }
+ }
if s.Image == "" {
errs = errs.Also(apis.ErrMissingField("Image"))
}
@@ -680,3 +699,22 @@ func (ts *TaskSpec) GetIndexingReferencesToArrayParams() sets.String {
}
return sets.NewString(arrayIndexParamRefs...)
}
+
+// ValidateStepResults validates that all of the declared StepResults are valid.
+func validateStepResults(ctx context.Context, results []StepResult) (errs *apis.FieldError) {
+ for index, result := range results {
+ errs = errs.Also(result.Validate(ctx).ViaIndex(index))
+ }
+ return errs
+}
+
+// validateStepResultsVariables validates if the StepResults referenced in step script are defined in step's results.
+func validateStepResultsVariables(ctx context.Context, results []StepResult, script string) (errs *apis.FieldError) {
+ resultsNames := sets.NewString()
+ for _, r := range results {
+ resultsNames.Insert(r.Name)
+ }
+ errs = errs.Also(substitution.ValidateNoReferencesToUnknownVariables(script, "step.results", resultsNames).ViaField("script"))
+ errs = errs.Also(substitution.ValidateNoReferencesToUnknownVariables(script, "results", resultsNames).ViaField("script"))
+ return errs
+}
diff --git a/pkg/apis/pipeline/v1beta1/task_validation_test.go b/pkg/apis/pipeline/v1beta1/task_validation_test.go
index 936b32ca2bb..b0db4521ea7 100644
--- a/pkg/apis/pipeline/v1beta1/task_validation_test.go
+++ b/pkg/apis/pipeline/v1beta1/task_validation_test.go
@@ -1426,26 +1426,82 @@ func TestTaskSpecValidateError(t *testing.T) {
}
}
-func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
+func TestTaskSpecValidateErrorWithStepActionRef_CreateUpdateEvent(t *testing.T) {
tests := []struct {
- name string
- Steps []v1beta1.Step
- enableStepActions bool
- expectedError apis.FieldError
+ name string
+ Steps []v1beta1.Step
+ isCreate bool
+ isUpdate bool
+ expectedError apis.FieldError
}{{
- name: "invalid Task Spec - enableStepActions not on",
+ name: "is create ctx",
Steps: []v1beta1.Step{{
- Image: "image",
Ref: &v1beta1.Ref{
Name: "stepAction",
},
}},
- enableStepActions: false,
+ isCreate: true,
+ isUpdate: false,
expectedError: apis.FieldError{
Message: "feature flag %s should be set to true to reference StepActions in Steps.",
Paths: []string{"steps[0].enable-step-actions"},
},
}, {
+ name: "is update ctx",
+ Steps: []v1beta1.Step{{
+ Ref: &v1beta1.Ref{
+ Name: "stepAction",
+ },
+ }},
+ isCreate: false,
+ isUpdate: true,
+ expectedError: apis.FieldError{
+ Message: "feature flag %s should be set to true to reference StepActions in Steps.",
+ Paths: []string{"steps[0].enable-step-actions"},
+ },
+ }, {
+ name: "ctx is not create or update",
+ Steps: []v1beta1.Step{{
+ Ref: &v1beta1.Ref{
+ Name: "stepAction",
+ },
+ }},
+ isCreate: false,
+ isUpdate: false,
+ expectedError: apis.FieldError{},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ts := v1beta1.TaskSpec{
+ Steps: tt.Steps,
+ }
+ ctx := config.ToContext(context.Background(), &config.Config{
+ FeatureFlags: &config.FeatureFlags{
+ EnableStepActions: false,
+ },
+ })
+ if tt.isCreate {
+ ctx = apis.WithinCreate(ctx)
+ }
+ if tt.isUpdate {
+ ctx = apis.WithinUpdate(ctx, apis.GetBaseline(ctx))
+ }
+ ts.SetDefaults(ctx)
+ err := ts.Validate(ctx)
+ if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" {
+ t.Errorf("TaskSpec.Validate() errors diff %s", diff.PrintWantGot(d))
+ }
+ })
+ }
+}
+
+func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
+ tests := []struct {
+ name string
+ Steps []v1beta1.Step
+ expectedError apis.FieldError
+ }{{
name: "Cannot use image with Ref",
Steps: []v1beta1.Step{{
Ref: &v1beta1.Ref{
@@ -1453,7 +1509,6 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
},
Image: "foo",
}},
- enableStepActions: true,
expectedError: apis.FieldError{
Message: "image cannot be used with Ref",
Paths: []string{"steps[0].image"},
@@ -1466,7 +1521,6 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
},
Command: []string{"foo"},
}},
- enableStepActions: true,
expectedError: apis.FieldError{
Message: "command cannot be used with Ref",
Paths: []string{"steps[0].command"},
@@ -1479,7 +1533,6 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
},
Args: []string{"foo"},
}},
- enableStepActions: true,
expectedError: apis.FieldError{
Message: "args cannot be used with Ref",
Paths: []string{"steps[0].args"},
@@ -1492,7 +1545,6 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
},
Script: "echo hi",
}},
- enableStepActions: true,
expectedError: apis.FieldError{
Message: "script cannot be used with Ref",
Paths: []string{"steps[0].script"},
@@ -1508,7 +1560,6 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
Value: "value1",
}},
}},
- enableStepActions: true,
expectedError: apis.FieldError{
Message: "env cannot be used with Ref",
Paths: []string{"steps[0].env"},
@@ -1521,7 +1572,6 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
Name: "param",
}},
}},
- enableStepActions: true,
expectedError: apis.FieldError{
Message: "params cannot be used without Ref",
Paths: []string{"steps[0].params"},
@@ -1537,12 +1587,25 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
MountPath: "/registry-config",
}},
}},
- enableStepActions: true,
expectedError: apis.FieldError{
Message: "volumeMounts cannot be used with Ref",
Paths: []string{"steps[0].volumeMounts"},
+ },
+ }, {
+ name: "Cannot use results with Ref",
+ Steps: []v1beta1.Step{{
+ Ref: &v1beta1.Ref{
+ Name: "stepAction",
+ },
+ Results: []v1beta1.StepResult{{
+ Name: "result",
+ }},
}},
- }
+ expectedError: apis.FieldError{
+ Message: "results cannot be used with Ref",
+ Paths: []string{"steps[0].results"},
+ },
+ }}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := v1beta1.TaskSpec{
@@ -1550,7 +1613,7 @@ func TestTaskSpecValidateErrorWithStepActionRef(t *testing.T) {
}
ctx := config.ToContext(context.Background(), &config.Config{
FeatureFlags: &config.FeatureFlags{
- EnableStepActions: tt.enableStepActions,
+ EnableStepActions: true,
},
})
ts.SetDefaults(ctx)
@@ -2248,3 +2311,188 @@ func TestParamEnum_Failure(t *testing.T) {
}
}
}
+
+func TestTaskSpecValidate_StepResults(t *testing.T) {
+ type fields struct {
+ Image string
+ Args []string
+ Script string
+ Results []v1beta1.StepResult
+ }
+ tests := []struct {
+ name string
+ fields fields
+ }{{
+ name: "valid result",
+ fields: fields{
+ Image: "my-image",
+ Args: []string{"arg"},
+ Results: []v1beta1.StepResult{{
+ Name: "MY-RESULT",
+ Description: "my great result",
+ }},
+ },
+ }, {
+ name: "valid result type string",
+ fields: fields{
+ Image: "my-image",
+ Args: []string{"arg"},
+ Script: "echo $(step.results.MY-RESULT.path)",
+ Results: []v1beta1.StepResult{{
+ Name: "MY-RESULT",
+ Type: "string",
+ Description: "my great result",
+ }},
+ },
+ }, {
+ name: "valid result type array",
+ fields: fields{
+ Image: "my-image",
+ Args: []string{"arg"},
+ Results: []v1beta1.StepResult{{
+ Name: "MY-RESULT",
+ Type: v1beta1.ResultsTypeArray,
+ Description: "my great result",
+ }},
+ },
+ }, {
+ name: "valid result type object",
+ fields: fields{
+ Image: "my-image",
+ Args: []string{"arg"},
+ Results: []v1beta1.StepResult{{
+ Name: "MY-RESULT",
+ Type: v1beta1.ResultsTypeObject,
+ Description: "my great result",
+ Properties: map[string]v1beta1.PropertySpec{
+ "url": {Type: "string"},
+ "commit": {Type: "string"},
+ },
+ }},
+ },
+ }}
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ts := &v1beta1.TaskSpec{
+ Steps: []v1beta1.Step{{
+ Image: tt.fields.Image,
+ Args: tt.fields.Args,
+ Script: tt.fields.Script,
+ Results: tt.fields.Results,
+ }},
+ }
+ ctx := config.ToContext(context.Background(), &config.Config{
+ FeatureFlags: &config.FeatureFlags{
+ EnableStepActions: true,
+ },
+ })
+ ts.SetDefaults(ctx)
+ if err := ts.Validate(ctx); err != nil {
+ t.Errorf("TaskSpec.Validate() = %v", err)
+ }
+ })
+ }
+}
+
+func TestTaskSpecValidate_StepResults_Error(t *testing.T) {
+ type fields struct {
+ Image string
+ Script string
+ Results []v1beta1.StepResult
+ }
+ tests := []struct {
+ name string
+ fields fields
+ enableStepActions bool
+ isCreate bool
+ isUpdate bool
+ expectedError apis.FieldError
+ }{{
+ name: "step result not allowed withoue enable step actions - create event",
+ fields: fields{
+ Image: "my-image",
+ Results: []v1beta1.StepResult{{Name: "a-result"}},
+ },
+ enableStepActions: false,
+ isCreate: true,
+ expectedError: apis.FieldError{
+ Message: "feature flag %s should be set to true in order to use Results in Steps.",
+ Paths: []string{"steps[0].enable-step-actions"},
+ },
+ }, {
+ name: "step result not allowed withoue enable step actions - update event",
+ fields: fields{
+ Image: "my-image",
+ Results: []v1beta1.StepResult{{Name: "a-result"}},
+ },
+ enableStepActions: false,
+ isUpdate: true,
+ expectedError: apis.FieldError{
+ Message: "feature flag %s should be set to true in order to use Results in Steps.",
+ Paths: []string{"steps[0].enable-step-actions"},
+ },
+ }, {
+ name: "step result not allowed withoue enable step actions - neither create nor update",
+ fields: fields{
+ Image: "my-image",
+ Results: []v1beta1.StepResult{{Name: "a-result"}},
+ },
+ enableStepActions: false,
+ expectedError: apis.FieldError{},
+ }, {
+ name: "step script refers to nonexistent result",
+ fields: fields{
+ Image: "my-image",
+ Script: `
+ #!/usr/bin/env bash
+ date | tee $(results.non-exist.path)`,
+ Results: []v1beta1.StepResult{{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{"steps[0].script"},
+ },
+ enableStepActions: true,
+ }, {
+ name: "step script refers to nonexistent stepresult",
+ fields: fields{
+ Image: "my-image",
+ Script: `
+ #!/usr/bin/env bash
+ date | tee $(step.results.non-exist.path)`,
+ Results: []v1beta1.StepResult{{Name: "a-result"}},
+ },
+ expectedError: apis.FieldError{
+ Message: `non-existent variable in "\n\t\t\t#!/usr/bin/env bash\n\t\t\tdate | tee $(step.results.non-exist.path)"`,
+ Paths: []string{"steps[0].script"},
+ },
+ enableStepActions: true,
+ }}
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ts := &v1beta1.TaskSpec{
+ Steps: []v1beta1.Step{{
+ Image: tt.fields.Image,
+ Script: tt.fields.Script,
+ Results: tt.fields.Results,
+ }},
+ }
+ ctx := config.ToContext(context.Background(), &config.Config{
+ FeatureFlags: &config.FeatureFlags{
+ EnableStepActions: tt.enableStepActions,
+ },
+ })
+ if tt.isCreate {
+ ctx = apis.WithinCreate(ctx)
+ }
+ if tt.isUpdate {
+ ctx = apis.WithinUpdate(ctx, apis.GetBaseline(ctx))
+ }
+ ts.SetDefaults(ctx)
+ err := ts.Validate(ctx)
+ if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" {
+ t.Errorf("StepActionSpec.Validate() errors diff %s", diff.PrintWantGot(d))
+ }
+ })
+ }
+}
diff --git a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go
index ca255ddee6c..02a0f9dc77f 100644
--- a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go
+++ b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go
@@ -1769,6 +1769,13 @@ func (in *Step) DeepCopyInto(out *Step) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.Results != nil {
+ in, out := &in.Results, &out.Results
+ *out = make([]StepResult, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
return
}
@@ -1798,6 +1805,29 @@ func (in *StepOutputConfig) DeepCopy() *StepOutputConfig {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *StepResult) DeepCopyInto(out *StepResult) {
+ *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 StepResult.
+func (in *StepResult) DeepCopy() *StepResult {
+ if in == nil {
+ return nil
+ }
+ out := new(StepResult)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StepState) DeepCopyInto(out *StepState) {
*out = *in