diff --git a/docs/pipeline-api.md b/docs/pipeline-api.md index de0deb66868..f8922737054 100644 --- a/docs/pipeline-api.md +++ b/docs/pipeline-api.md @@ -1542,6 +1542,19 @@ IncludeParamsList
Include is a list of IncludeParams which allows passing in specific combinations of Parameters into the Matrix.
+strategy
Strategy is a JSON payload with a list of combinations +Strategy is an extension of Include to support dynamic combinations
+Field | +Description | +
---|---|
+include + +[]string + + |
++ | +
string
alias)@@ -9447,6 +9484,19 @@ IncludeParamsList
Include is a list of IncludeParams which allows passing in specific combinations of Parameters into the Matrix.
+strategy
Strategy is a JSON payload with a list of combinations +Strategy is an extension of Include to support dynamic combinations
+Field | +Description | +
---|---|
+include + +[]string + + |
++ | +
string
alias)diff --git a/pkg/apis/pipeline/v1/matrix_types.go b/pkg/apis/pipeline/v1/matrix_types.go index f51a0ac4af4..767d8f7227a 100644 --- a/pkg/apis/pipeline/v1/matrix_types.go +++ b/pkg/apis/pipeline/v1/matrix_types.go @@ -15,6 +15,7 @@ package v1 import ( "context" + "encoding/json" "fmt" "sort" @@ -38,6 +39,12 @@ type Matrix struct { // +optional // +listType=atomic Include IncludeParamsList `json:"include,omitempty"` + + // Strategy is a JSON payload with a list of combinations + // Strategy is an extension of Include to support dynamic combinations + // +optional + // +listType=atomic + Strategy string `json:"strategy,omitempty"` } // IncludeParamsList is a list of IncludeParams which allows passing in specific combinations of Parameters into the Matrix. @@ -54,14 +61,21 @@ type IncludeParams struct { Params Params `json:"params,omitempty"` } +type Strategy struct { + Include []map[string]string `json:"include,omitempty"` +} + // Combination is a map, mainly defined to hold a single combination from a Matrix with key as param.Name and value as param.Value type Combination map[string]string // Combinations is a Combination list type Combinations []Combination -// FanOut returns an list of params that represent combinations +// FanOut returns a list of params that represent combinations func (m *Matrix) FanOut() []Params { + if m.HasStrategy() { + return m.getStrategy().toParams() + } var combinations, includeCombinations Combinations includeCombinations = m.getIncludeCombinations() if m.HasInclude() && !m.HasParams() { @@ -177,6 +191,23 @@ func (m *Matrix) getIncludeCombinations() Combinations { return combinations } +// getStrategy generates combinations based on Matrix Strategy section +// Matrix Strategy allows "include" as a JSON payload +func (m *Matrix) getStrategy() Combinations { + var combinations Combinations + var s Strategy + if err := json.Unmarshal([]byte(m.Strategy), &s); err == nil { + for _, i := range s.Include { + newCombination := make(Combination) + for k, v := range i { + newCombination[k] = v + } + combinations = append(combinations, newCombination) + } + } + return combinations +} + // distribute generates a new Combination of Parameters by adding a new Parameter to an existing list of Combinations. func (cs Combinations) distribute(param Param) Combinations { var expandedCombinations Combinations @@ -225,6 +256,9 @@ func (m *Matrix) CountCombinations() int { // Add any additional Combinations generated from Matrix Include Parameters count += m.countNewCombinationsFromInclude() + // Iterate over Matrix Strategy to count all combinations specified + count += m.countCombinationsFromStrategy() + return count } @@ -267,6 +301,11 @@ func (m *Matrix) countNewCombinationsFromInclude() int { return count } +// countCombinationsFromStrategy returns the count of Combinations specified in the strategy +func (m *Matrix) countCombinationsFromStrategy() int { + return len(m.getStrategy()) +} + // HasInclude returns true if the Matrix has Include Parameters func (m *Matrix) HasInclude() bool { return m != nil && m.Include != nil && len(m.Include) > 0 @@ -277,6 +316,10 @@ func (m *Matrix) HasParams() bool { return m != nil && m.Params != nil && len(m.Params) > 0 } +func (m *Matrix) HasStrategy() bool { + return m != nil && len(m.Strategy) > 0 +} + // GetAllParams returns a list of all Matrix Parameters func (m *Matrix) GetAllParams() Params { var params Params @@ -288,6 +331,12 @@ func (m *Matrix) GetAllParams() Params { params = append(params, include.Params...) } } + if m.HasStrategy() { + ps := m.getStrategy().toParams() + for _, p := range ps { + params = append(params, p...) + } + } return params } @@ -336,6 +385,18 @@ func (m *Matrix) validatePipelineParametersVariablesInMatrixParameters(prefix st } } } + if m.HasStrategy() { + var s Strategy + if err := json.Unmarshal([]byte(m.Strategy), &s); err != nil { + for _, i := range s.Include { + for k, v := range i { + // Matrix Strategy Params must be of type string + errs = errs.Also(validateStringVariable(v, prefix, paramNames, arrayParamNames, objectParamNameKeys).ViaField(k).ViaField("matrix.strategy", k)) + + } + } + } + } return errs } @@ -348,3 +409,21 @@ func (m *Matrix) validateParameterInOneOfMatrixOrParams(params []Param) (errs *a } return errs } + +// validateStrategy validates syntax of Matrix Strategy section +// Matrix Strategy allows "include" as a JSON payload with a list of combinations +func (m *Matrix) validateStrategy() (errs *apis.FieldError) { + e := "matrix.strategy section does not have a valid JSON payload, " + + "matrix.strategy section only allows valid JSON payload in the form of " + + "{include: []map[string]string} e.g. {include: [{k1: v1, k2: v2}, {k1: v3, k2: v4}, {k3: v1}]" + var s Strategy + if m.HasStrategy() { + if !json.Valid([]byte(m.Strategy)) { + return apis.ErrGeneric(e) + } + if err := json.Unmarshal([]byte(m.Strategy), &s); err != nil { + return apis.ErrGeneric(e + ": " + err.Error()) + } + } + return errs +} diff --git a/pkg/apis/pipeline/v1/matrix_types_test.go b/pkg/apis/pipeline/v1/matrix_types_test.go index 26b83f83c25..9da219e758c 100644 --- a/pkg/apis/pipeline/v1/matrix_types_test.go +++ b/pkg/apis/pipeline/v1/matrix_types_test.go @@ -582,6 +582,40 @@ func TestMatrix_FanOut(t *testing.T) { Value: v1.ParamValue{Type: v1.ParamTypeString, StringVal: "I-do-not-exist"}, }, }}, + }, { + name: "Fan out explicit combinations using strategy", + matrix: v1.Matrix{ + Strategy: "{\"include\": [" + + "{\"DOCKERFILE\": \"path/to/Dockerfile1\", \"IMAGE\": \"image-1\"}," + + "{\"DOCKERFILE\": \"path/to/Dockerfile2\", \"IMAGE\": \"image-2\"}," + + "{\"DOCKERFILE\": \"path/to/Dockerfile3\", \"IMAGE\": \"image-3\"}" + + "]}", + }, + want: []v1.Params{{ + { + Name: "DOCKERFILE", + Value: v1.ParamValue{Type: v1.ParamTypeString, StringVal: "path/to/Dockerfile1"}, + }, { + Name: "IMAGE", + Value: v1.ParamValue{Type: v1.ParamTypeString, StringVal: "image-1"}, + }, + }, { + { + Name: "DOCKERFILE", + Value: v1.ParamValue{Type: v1.ParamTypeString, StringVal: "path/to/Dockerfile2"}, + }, { + Name: "IMAGE", + Value: v1.ParamValue{Type: v1.ParamTypeString, StringVal: "image-2"}, + }, + }, { + { + Name: "DOCKERFILE", + Value: v1.ParamValue{Type: v1.ParamTypeString, StringVal: "path/to/Dockerfile3"}, + }, { + Name: "IMAGE", + Value: v1.ParamValue{Type: v1.ParamTypeString, StringVal: "image-3"}, + }, + }}, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/apis/pipeline/v1/openapi_generated.go b/pkg/apis/pipeline/v1/openapi_generated.go index c3011075abc..4e8fb5ed28b 100644 --- a/pkg/apis/pipeline/v1/openapi_generated.go +++ b/pkg/apis/pipeline/v1/openapi_generated.go @@ -71,6 +71,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.StepOutputConfig": schema_pkg_apis_pipeline_v1_StepOutputConfig(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.Strategy": schema_pkg_apis_pipeline_v1_Strategy(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Task": schema_pkg_apis_pipeline_v1_Task(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskList": schema_pkg_apis_pipeline_v1_TaskList(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRef": schema_pkg_apis_pipeline_v1_TaskRef(ref), @@ -700,6 +701,18 @@ func schema_pkg_apis_pipeline_v1_Matrix(ref common.ReferenceCallback) common.Ope }, }, }, + "strategy": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Strategy is a JSON payload with a list of combinations Strategy is an extension of Include to support dynamic combinations", + Type: []string{"string"}, + Format: "", + }, + }, }, }, }, @@ -3167,6 +3180,40 @@ func schema_pkg_apis_pipeline_v1_StepTemplate(ref common.ReferenceCallback) comm } } +func schema_pkg_apis_pipeline_v1_Strategy(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "include": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + func schema_pkg_apis_pipeline_v1_Task(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/apis/pipeline/v1/pipeline_validation.go b/pkg/apis/pipeline/v1/pipeline_validation.go index 71b002d9bcd..38fd903049c 100644 --- a/pkg/apis/pipeline/v1/pipeline_validation.go +++ b/pkg/apis/pipeline/v1/pipeline_validation.go @@ -235,6 +235,7 @@ func (pt *PipelineTask) validateMatrix(ctx context.Context) (errs *apis.FieldErr errs = errs.Also(version.ValidateEnabledAPIFields(ctx, "matrix", config.AlphaAPIFields)) errs = errs.Also(pt.Matrix.validateCombinationsCount(ctx)) errs = errs.Also(pt.Matrix.validateUniqueParams()) + errs = errs.Also(pt.Matrix.validateStrategy()) } errs = errs.Also(pt.Matrix.validateParameterInOneOfMatrixOrParams(pt.Params)) return errs diff --git a/pkg/apis/pipeline/v1/swagger.json b/pkg/apis/pipeline/v1/swagger.json index e2f82a64734..0c73514656e 100644 --- a/pkg/apis/pipeline/v1/swagger.json +++ b/pkg/apis/pipeline/v1/swagger.json @@ -307,6 +307,11 @@ "$ref": "#/definitions/v1.Param" }, "x-kubernetes-list-type": "atomic" + }, + "strategy": { + "description": "Strategy is a JSON payload with a list of combinations Strategy is an extension of Include to support dynamic combinations", + "type": "string", + "x-kubernetes-list-type": "atomic" } } }, @@ -1622,6 +1627,21 @@ } } }, + "v1.Strategy": { + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + } + } + } + }, "v1.Task": { "description": "Task represents a collection of sequential steps that are run as part of a Pipeline using a set of inputs and producing a set of outputs. Tasks execute when TaskRuns are created that provide the input parameters and resources and output resources the Task requires.", "type": "object", diff --git a/pkg/apis/pipeline/v1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1/zz_generated.deepcopy.go index 484d08a4341..312b129bab1 100644 --- a/pkg/apis/pipeline/v1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1/zz_generated.deepcopy.go @@ -1367,6 +1367,35 @@ func (in *StepTemplate) DeepCopy() *StepTemplate { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Strategy) DeepCopyInto(out *Strategy) { + *out = *in + if in.Include != nil { + in, out := &in.Include, &out.Include + *out = make([]map[string]string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Strategy. +func (in *Strategy) DeepCopy() *Strategy { + if in == nil { + return nil + } + out := new(Strategy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Task) DeepCopyInto(out *Task) { *out = *in diff --git a/pkg/apis/pipeline/v1beta1/matrix_types.go b/pkg/apis/pipeline/v1beta1/matrix_types.go index 19042e11e4b..43e2af956a9 100644 --- a/pkg/apis/pipeline/v1beta1/matrix_types.go +++ b/pkg/apis/pipeline/v1beta1/matrix_types.go @@ -15,6 +15,7 @@ package v1beta1 import ( "context" + "encoding/json" "fmt" "sort" @@ -38,6 +39,12 @@ type Matrix struct { // +optional // +listType=atomic Include IncludeParamsList `json:"include,omitempty"` + + // Strategy is a JSON payload with a list of combinations + // Strategy is an extension of Include to support dynamic combinations + // +optional + // +listType=atomic + Strategy string `json:"strategy,omitempty"` } // IncludeParamsList is a list of IncludeParams which allows passing in specific combinations of Parameters into the Matrix. @@ -54,15 +61,22 @@ type IncludeParams struct { Params Params `json:"params,omitempty"` } +type Strategy struct { + Include []map[string]string `json:"include,omitempty"` +} + // Combination is a map, mainly defined to hold a single combination from a Matrix with key as param.Name and value as param.Value type Combination map[string]string // Combinations is a Combination list type Combinations []Combination -// FanOut returns an list of params that represent combinations +// FanOut returns a list of params that represent combinations func (m *Matrix) FanOut() []Params { var combinations, includeCombinations Combinations + if m.HasStrategy() { + return m.getStrategy().toParams() + } includeCombinations = m.getIncludeCombinations() if m.HasInclude() && !m.HasParams() { // If there are only Matrix Include Parameters return explicit combinations @@ -177,6 +191,23 @@ func (m *Matrix) getIncludeCombinations() Combinations { return combinations } +// getStrategy generates combinations based on Matrix Strategy section +// Matrix Strategy allows "include" as a JSON payload +func (m *Matrix) getStrategy() Combinations { + var combinations Combinations + var s Strategy + if err := json.Unmarshal([]byte(m.Strategy), &s); err == nil { + for _, i := range s.Include { + newCombination := make(Combination) + for k, v := range i { + newCombination[k] = v + } + combinations = append(combinations, newCombination) + } + } + return combinations +} + // distribute generates a new Combination of Parameters by adding a new Parameter to an existing list of Combinations. func (cs Combinations) distribute(param Param) Combinations { var expandedCombinations Combinations @@ -225,6 +256,9 @@ func (m *Matrix) CountCombinations() int { // Add any additional Combinations generated from Matrix Include Parameters count += m.countNewCombinationsFromInclude() + // Iterate over Matrix Strategy to count all combinations specified + count += m.countCombinationsFromStrategy() + return count } @@ -267,6 +301,11 @@ func (m *Matrix) countNewCombinationsFromInclude() int { return count } +// countCombinationsFromStrategy returns the count of Combinations specified in the strategy +func (m *Matrix) countCombinationsFromStrategy() int { + return len(m.getStrategy()) +} + // HasInclude returns true if the Matrix has Include Parameters func (m *Matrix) HasInclude() bool { return m != nil && m.Include != nil && len(m.Include) > 0 @@ -277,6 +316,10 @@ func (m *Matrix) HasParams() bool { return m != nil && m.Params != nil && len(m.Params) > 0 } +func (m *Matrix) HasStrategy() bool { + return m != nil && len(m.Strategy) > 0 +} + // GetAllParams returns a list of all Matrix Parameters func (m *Matrix) GetAllParams() Params { var params Params @@ -336,6 +379,18 @@ func (m *Matrix) validatePipelineParametersVariablesInMatrixParameters(prefix st } } } + if m.HasStrategy() { + var s Strategy + if err := json.Unmarshal([]byte(m.Strategy), &s); err != nil { + for _, i := range s.Include { + for k, v := range i { + // Matrix Strategy Params must be of type string + errs = errs.Also(validateStringVariable(v, prefix, paramNames, arrayParamNames, objectParamNameKeys).ViaField(k).ViaField("matrix.strategy", k)) + + } + } + } + } return errs } @@ -348,3 +403,21 @@ func (m *Matrix) validateParameterInOneOfMatrixOrParams(params Params) (errs *ap } return errs } + +// validateStrategy validates syntax of Matrix Strategy section +// Matrix Strategy allows "include" as a JSON payload with a list of combinations +func (m *Matrix) validateStrategy() (errs *apis.FieldError) { + e := "matrix.strategy section does not have a valid JSON payload, " + + "matrix.strategy section only allows valid JSON payload in the form of " + + "{include: []map[string]string} e.g. {include: [{k1: v1, k2: v2}, {k1: v3, k2: v4}, {k3: v1}]" + var s Strategy + if m.HasStrategy() { + if !json.Valid([]byte(m.Strategy)) { + return apis.ErrGeneric(e) + } + if err := json.Unmarshal([]byte(m.Strategy), &s); err != nil { + return apis.ErrGeneric(e + ": " + err.Error()) + } + } + return errs +} diff --git a/pkg/apis/pipeline/v1beta1/matrix_types_test.go b/pkg/apis/pipeline/v1beta1/matrix_types_test.go index 601eed19f35..214ed2b8464 100644 --- a/pkg/apis/pipeline/v1beta1/matrix_types_test.go +++ b/pkg/apis/pipeline/v1beta1/matrix_types_test.go @@ -582,6 +582,40 @@ func TestMatrix_FanOut(t *testing.T) { Value: v1beta1.ParamValue{Type: v1beta1.ParamTypeString, StringVal: "I-do-not-exist"}, }, }}, + }, { + name: "Fan out explicit combinations using strategy", + matrix: v1beta1.Matrix{ + Strategy: "{\"include\": [" + + "{\"DOCKERFILE\": \"path/to/Dockerfile1\", \"IMAGE\": \"image-1\"}," + + "{\"DOCKERFILE\": \"path/to/Dockerfile2\", \"IMAGE\": \"image-2\"}," + + "{\"DOCKERFILE\": \"path/to/Dockerfile3\", \"IMAGE\": \"image-3\"}" + + "]}", + }, + want: []v1beta1.Params{{ + { + Name: "DOCKERFILE", + Value: v1beta1.ParamValue{Type: v1beta1.ParamTypeString, StringVal: "path/to/Dockerfile1"}, + }, { + Name: "IMAGE", + Value: v1beta1.ParamValue{Type: v1beta1.ParamTypeString, StringVal: "image-1"}, + }, + }, { + { + Name: "DOCKERFILE", + Value: v1beta1.ParamValue{Type: v1beta1.ParamTypeString, StringVal: "path/to/Dockerfile2"}, + }, { + Name: "IMAGE", + Value: v1beta1.ParamValue{Type: v1beta1.ParamTypeString, StringVal: "image-2"}, + }, + }, { + { + Name: "DOCKERFILE", + Value: v1beta1.ParamValue{Type: v1beta1.ParamTypeString, StringVal: "path/to/Dockerfile3"}, + }, { + Name: "IMAGE", + Value: v1beta1.ParamValue{Type: v1beta1.ParamTypeString, StringVal: "image-3"}, + }, + }}, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/apis/pipeline/v1beta1/openapi_generated.go b/pkg/apis/pipeline/v1beta1/openapi_generated.go index 71844dd01d9..2b84b708708 100644 --- a/pkg/apis/pipeline/v1beta1/openapi_generated.go +++ b/pkg/apis/pipeline/v1beta1/openapi_generated.go @@ -86,6 +86,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.StepOutputConfig": schema_pkg_apis_pipeline_v1beta1_StepOutputConfig(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.Strategy": schema_pkg_apis_pipeline_v1beta1_Strategy(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Task": schema_pkg_apis_pipeline_v1beta1_Task(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.TaskList": schema_pkg_apis_pipeline_v1beta1_TaskList(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.TaskRef": schema_pkg_apis_pipeline_v1beta1_TaskRef(ref), @@ -1241,6 +1242,18 @@ func schema_pkg_apis_pipeline_v1beta1_Matrix(ref common.ReferenceCallback) commo }, }, }, + "strategy": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Strategy is a JSON payload with a list of combinations Strategy is an extension of Include to support dynamic combinations", + Type: []string{"string"}, + Format: "", + }, + }, }, }, }, @@ -4214,6 +4227,40 @@ func schema_pkg_apis_pipeline_v1beta1_StepTemplate(ref common.ReferenceCallback) } } +func schema_pkg_apis_pipeline_v1beta1_Strategy(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "include": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + func schema_pkg_apis_pipeline_v1beta1_Task(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/apis/pipeline/v1beta1/pipeline_validation.go b/pkg/apis/pipeline/v1beta1/pipeline_validation.go index 7529c273b4f..221696724c2 100644 --- a/pkg/apis/pipeline/v1beta1/pipeline_validation.go +++ b/pkg/apis/pipeline/v1beta1/pipeline_validation.go @@ -187,6 +187,7 @@ func (pt *PipelineTask) validateMatrix(ctx context.Context) (errs *apis.FieldErr errs = errs.Also(version.ValidateEnabledAPIFields(ctx, "matrix", config.AlphaAPIFields)) errs = errs.Also(pt.Matrix.validateCombinationsCount(ctx)) errs = errs.Also(pt.Matrix.validateUniqueParams()) + errs = errs.Also(pt.Matrix.validateStrategy()) } errs = errs.Also(pt.Matrix.validateParameterInOneOfMatrixOrParams(pt.Params)) return errs diff --git a/pkg/apis/pipeline/v1beta1/swagger.json b/pkg/apis/pipeline/v1beta1/swagger.json index 47aafa9a098..b54fae236f3 100644 --- a/pkg/apis/pipeline/v1beta1/swagger.json +++ b/pkg/apis/pipeline/v1beta1/swagger.json @@ -588,6 +588,11 @@ "$ref": "#/definitions/v1beta1.Param" }, "x-kubernetes-list-type": "atomic" + }, + "strategy": { + "description": "Strategy is a JSON payload with a list of combinations Strategy is an extension of Include to support dynamic combinations", + "type": "string", + "x-kubernetes-list-type": "atomic" } } }, @@ -2336,6 +2341,21 @@ } } }, + "v1beta1.Strategy": { + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + } + } + } + }, "v1beta1.Task": { "description": "Task represents a collection of sequential steps that are run as part of a Pipeline using a set of inputs and producing a set of outputs. Tasks execute when TaskRuns are created that provide the input parameters and resources and output resources the Task requires.\n\nDeprecated: Please use v1.Task instead.", "type": "object", diff --git a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go index c158be1779f..b2151220cfe 100644 --- a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go @@ -1864,6 +1864,35 @@ func (in *StepTemplate) DeepCopy() *StepTemplate { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Strategy) DeepCopyInto(out *Strategy) { + *out = *in + if in.Include != nil { + in, out := &in.Include, &out.Include + *out = make([]map[string]string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Strategy. +func (in *Strategy) DeepCopy() *Strategy { + if in == nil { + return nil + } + out := new(Strategy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Task) DeepCopyInto(out *Task) { *out = *in diff --git a/pkg/reconciler/pipelinerun/pipelinerun_test.go b/pkg/reconciler/pipelinerun/pipelinerun_test.go index f61d0cd0e5d..04b93620b2f 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun_test.go +++ b/pkg/reconciler/pipelinerun/pipelinerun_test.go @@ -9595,6 +9595,196 @@ spec: } } +func TestReconciler_PipelineTaskMatrixStrategy(t *testing.T) { + names.TestingSeed() + + task := parse.MustParseV1Task(t, ` +metadata: + name: mytask + namespace: foo +spec: + params: + - name: IMAGE + - name: DOCKERFILE + steps: + - name: echo + image: alpine + script: | + echo "$(params.IMAGE) and $(params.DOCKERFILE)" +`) + + expectedTaskRuns := []*v1.TaskRun{ + mustParseTaskRunWithObjectMeta(t, + taskRunObjectMeta("pr-matrix-strategy-0", "foo", + "pr", "p", "matrix-strategy", false), + ` +spec: + params: + - name: DOCKERFILE + value: path/to/Dockerfile1 + - name: IMAGE + value: image-1 + serviceAccountName: test-sa + taskRef: + name: mytask + kind: Task +`), + mustParseTaskRunWithObjectMeta(t, + taskRunObjectMeta("pr-matrix-strategy-1", "foo", + "pr", "p", "matrix-strategy", false), + ` +spec: + params: + - name: DOCKERFILE + value: path/to/Dockerfile2 + - name: IMAGE + value: image-2 + serviceAccountName: test-sa + taskRef: + name: mytask + kind: Task +`), + mustParseTaskRunWithObjectMeta(t, + taskRunObjectMeta("pr-matrix-strategy-2", "foo", + "pr", "p", "matrix-strategy", false), + ` +spec: + params: + - name: DOCKERFILE + value: path/to/Dockerfile3 + - name: IMAGE + value: image-3 + serviceAccountName: test-sa + taskRef: + name: mytask + kind: Task +`), + } + cms := []*corev1.ConfigMap{withEnabledAlphaAPIFields(newFeatureFlagsConfigMap())} + cms = append(cms, withMaxMatrixCombinationsCount(newDefaultsConfigMap(), 10)) + + tests := []struct { + name string + memberOf string + p *v1.Pipeline + tr *v1.TaskRun + expectedPipelineRun *v1.PipelineRun + }{{ + name: "p-dag", + memberOf: "tasks", + p: parse.MustParseV1Pipeline(t, fmt.Sprintf(` +metadata: + name: %s + namespace: foo +spec: + tasks: + - name: matrix-strategy + taskRef: + name: mytask + matrix: + strategy: "{\"include\":[{\"IMAGE\":\"image-1\",\"DOCKERFILE\":\"path/to/Dockerfile1\"},{\"IMAGE\":\"image-2\",\"DOCKERFILE\":\"path/to/Dockerfile2\"},{\"IMAGE\":\"image-3\",\"DOCKERFILE\":\"path/to/Dockerfile3\"}]}" +`, "p-dag")), + expectedPipelineRun: parse.MustParseV1PipelineRun(t, ` +metadata: + name: pr + namespace: foo + annotations: {} + labels: + tekton.dev/pipeline: p-dag +spec: + taskRunTemplate: + serviceAccountName: test-sa + pipelineRef: + name: p-dag +status: + pipelineSpec: + tasks: + - name: matrix-strategy + taskRef: + name: mytask + kind: Task + matrix: + strategy: "{\"include\":[{\"IMAGE\":\"image-1\",\"DOCKERFILE\":\"path/to/Dockerfile1\"},{\"IMAGE\":\"image-2\",\"DOCKERFILE\":\"path/to/Dockerfile2\"},{\"IMAGE\":\"image-3\",\"DOCKERFILE\":\"path/to/Dockerfile3\"}]}" + conditions: + - type: Succeeded + status: "Unknown" + reason: "Running" + message: "Tasks Completed: 0 (Failed: 0, Cancelled 0), Incomplete: 1, Skipped: 0" + childReferences: + - apiVersion: tekton.dev/v1 + kind: TaskRun + name: pr-matrix-strategy-0 + pipelineTaskName: matrix-strategy + - apiVersion: tekton.dev/v1 + kind: TaskRun + name: pr-matrix-strategy-1 + pipelineTaskName: matrix-strategy + - apiVersion: tekton.dev/v1 + kind: TaskRun + name: pr-matrix-strategy-2 + pipelineTaskName: matrix-strategy +`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pr := parse.MustParseV1PipelineRun(t, fmt.Sprintf(` +metadata: + name: pr + namespace: foo +spec: + taskRunTemplate: + serviceAccountName: test-sa + pipelineRef: + name: %s +`, tt.name)) + d := test.Data{ + PipelineRuns: []*v1.PipelineRun{pr}, + Pipelines: []*v1.Pipeline{tt.p}, + Tasks: []*v1.Task{task}, + ConfigMaps: cms, + } + if tt.tr != nil { + d.TaskRuns = []*v1.TaskRun{tt.tr} + } + prt := newPipelineRunTest(t, d) + defer prt.Cancel() + + _, clients := prt.reconcileRun("foo", "pr", []string{}, false) + taskRuns, err := clients.Pipeline.TektonV1().TaskRuns("foo").List(prt.TestAssets.Ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("tekton.dev/pipelineRun=pr,tekton.dev/pipeline=%s,tekton.dev/pipelineTask=matrix-strategy", tt.name), + Limit: 1, + }) + if err != nil { + t.Fatalf("Failure to list TaskRun's %s", err) + } + + if len(taskRuns.Items) != 3 { + t.Fatalf("Expected 3 TaskRuns got %d", len(taskRuns.Items)) + } + + for i := range taskRuns.Items { + expectedTaskRun := expectedTaskRuns[i] + expectedTaskRun.Labels["tekton.dev/pipeline"] = tt.name + expectedTaskRun.Labels["tekton.dev/memberOf"] = tt.memberOf + if d := cmp.Diff(expectedTaskRun, &taskRuns.Items[i], ignoreResourceVersion, ignoreTypeMeta); d != "" { + t.Errorf("expected to see TaskRun %v created. Diff %s", expectedTaskRuns[i].Name, diff.PrintWantGot(d)) + } + } + + pipelineRun, err := clients.Pipeline.TektonV1().PipelineRuns("foo").Get(prt.TestAssets.Ctx, "pr", metav1.GetOptions{}) + if err != nil { + t.Fatalf("Got an error getting reconciled run out of fake client: %s", err) + } + + if d := cmp.Diff(tt.expectedPipelineRun, pipelineRun, ignoreResourceVersion, ignoreTypeMeta, ignoreLastTransitionTime, + ignoreStartTime, ignoreFinallyStartTime, ignoreProvenance, cmpopts.EquateEmpty()); d != "" { + t.Errorf("expected PipelineRun was not created. Diff %s", diff.PrintWantGot(d)) + } + }) + } +} + func TestReconciler_PipelineTaskMatrixWithResults(t *testing.T) { names.TestingSeed()