Skip to content

Commit c13edc2

Browse files
chuangw6tekton-robot
authored andcommitted
Add validation against the usage of the entire object
According to TEP-0075: When providing values for strings, Task and Pipeline authors can access individual attributes of an object param; but they cannot access the object as whole. Also modified `extractVariablesFromString` function a bit to recognize `params.foo.bar` reference that was treated as invalid previously.
1 parent d4c3786 commit c13edc2

File tree

4 files changed

+301
-28
lines changed

4 files changed

+301
-28
lines changed

pkg/apis/pipeline/v1beta1/task_validation.go

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -375,11 +375,7 @@ func ValidateResourcesVariables(ctx context.Context, steps []Step, resources *Ta
375375
return validateVariables(ctx, steps, "resources.(?:inputs|outputs)", resourceNames)
376376
}
377377

378-
// TODO (@chuangw6): Make sure an object param is not used as a whole when providing values for strings.
379-
// https://github.com/tektoncd/community/blob/main/teps/0075-object-param-and-result-types.md#variable-replacement-with-object-params
380-
// "When providing values for strings, Task and Pipeline authors can access
381-
// individual attributes of an object param; they cannot access the object
382-
// as whole (we could add support for this later)."
378+
// validateObjectUsage validates the usage of individual attributes of an object param and the usage of the entire object
383379
func validateObjectUsage(ctx context.Context, steps []Step, params []ParamSpec) (errs *apis.FieldError) {
384380
objectParameterNames := sets.NewString()
385381
for _, p := range params {
@@ -396,7 +392,7 @@ func validateObjectUsage(ctx context.Context, steps []Step, params []ParamSpec)
396392
errs = errs.Also(validateVariables(ctx, steps, fmt.Sprintf("params\\.%s", p.Name), objectKeys))
397393
}
398394

399-
return errs
395+
return errs.Also(validateObjectUsageAsWhole(steps, "params", objectParameterNames))
400396
}
401397

402398
// validateObjectDefault validates the keys of all the object params within a
@@ -438,6 +434,38 @@ func validateObjectKeys(properties map[string]PropertySpec, propertiesProvider *
438434
return nil
439435
}
440436

437+
// validateObjectUsageAsWhole makes sure the object params are not used as whole when providing values for strings
438+
// i.e. param.objectParam, param.objectParam[*]
439+
func validateObjectUsageAsWhole(steps []Step, prefix string, vars sets.String) (errs *apis.FieldError) {
440+
for idx, step := range steps {
441+
errs = errs.Also(validateStepObjectUsageAsWhole(step, prefix, vars)).ViaFieldIndex("steps", idx)
442+
}
443+
return errs
444+
}
445+
446+
func validateStepObjectUsageAsWhole(step Step, prefix string, vars sets.String) *apis.FieldError {
447+
errs := validateTaskNoObjectReferenced(step.Name, prefix, vars).ViaField("name")
448+
errs = errs.Also(validateTaskNoObjectReferenced(step.Image, prefix, vars).ViaField("image"))
449+
errs = errs.Also(validateTaskNoObjectReferenced(step.WorkingDir, prefix, vars).ViaField("workingDir"))
450+
errs = errs.Also(validateTaskNoObjectReferenced(step.Script, prefix, vars).ViaField("script"))
451+
for i, cmd := range step.Command {
452+
errs = errs.Also(validateTaskNoObjectReferenced(cmd, prefix, vars).ViaFieldIndex("command", i))
453+
}
454+
for i, arg := range step.Args {
455+
errs = errs.Also(validateTaskNoObjectReferenced(arg, prefix, vars).ViaFieldIndex("args", i))
456+
457+
}
458+
for _, env := range step.Env {
459+
errs = errs.Also(validateTaskNoObjectReferenced(env.Value, prefix, vars).ViaFieldKey("env", env.Name))
460+
}
461+
for i, v := range step.VolumeMounts {
462+
errs = errs.Also(validateTaskNoObjectReferenced(v.Name, prefix, vars).ViaField("name").ViaFieldIndex("volumeMount", i))
463+
errs = errs.Also(validateTaskNoObjectReferenced(v.MountPath, prefix, vars).ViaField("mountPath").ViaFieldIndex("volumeMount", i))
464+
errs = errs.Also(validateTaskNoObjectReferenced(v.SubPath, prefix, vars).ViaField("subPath").ViaFieldIndex("volumeMount", i))
465+
}
466+
return errs
467+
}
468+
441469
func validateArrayUsage(steps []Step, prefix string, vars sets.String) (errs *apis.FieldError) {
442470
for idx, step := range steps {
443471
errs = errs.Also(validateStepArrayUsage(step, prefix, vars)).ViaFieldIndex("steps", idx)
@@ -525,6 +553,10 @@ func validateTaskVariable(value, prefix string, vars sets.String) *apis.FieldErr
525553
return substitution.ValidateVariableP(value, prefix, vars)
526554
}
527555

556+
func validateTaskNoObjectReferenced(value, prefix string, objectNames sets.String) *apis.FieldError {
557+
return substitution.ValidateEntireVariableProhibitedP(value, prefix, objectNames)
558+
}
559+
528560
func validateTaskNoArrayReferenced(value, prefix string, arrayNames sets.String) *apis.FieldError {
529561
return substitution.ValidateVariableProhibitedP(value, prefix, arrayNames)
530562
}

pkg/apis/pipeline/v1beta1/task_validation_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,24 @@ func TestTaskSpecValidate(t *testing.T) {
193193
WorkingDir: "/foo/bar/src/",
194194
}},
195195
},
196+
}, {
197+
name: "valid object template variable",
198+
fields: fields{
199+
Params: []v1beta1.ParamSpec{{
200+
Name: "gitrepo",
201+
Type: v1beta1.ParamTypeObject,
202+
Properties: map[string]v1beta1.PropertySpec{
203+
"url": {},
204+
"commit": {},
205+
},
206+
}},
207+
Steps: []v1beta1.Step{{
208+
Name: "do-the-clone",
209+
Image: "some-git-image",
210+
Args: []string{"-url=$(params.gitrepo.url)", "-commit=$(params.gitrepo.commit)"},
211+
WorkingDir: "/foo/bar/src/",
212+
}},
213+
},
196214
}, {
197215
name: "valid star array template variable",
198216
fields: fields{
@@ -901,6 +919,94 @@ func TestTaskSpecValidateError(t *testing.T) {
901919
Message: `variable is not properly isolated in "not isolated: $(params.baz[*])"`,
902920
Paths: []string{"steps[0].args[0]"},
903921
},
922+
}, {
923+
name: "object used in a string field",
924+
fields: fields{
925+
Params: []v1beta1.ParamSpec{{
926+
Name: "gitrepo",
927+
Type: v1beta1.ParamTypeObject,
928+
Properties: map[string]v1beta1.PropertySpec{
929+
"url": {},
930+
"commit": {},
931+
},
932+
}},
933+
Steps: []v1beta1.Step{{
934+
Name: "do-the-clone",
935+
Image: "$(params.gitrepo)",
936+
Args: []string{"echo"},
937+
WorkingDir: "/foo/bar/src/",
938+
}},
939+
},
940+
expectedError: apis.FieldError{
941+
Message: `variable type invalid in "$(params.gitrepo)"`,
942+
Paths: []string{"steps[0].image"},
943+
},
944+
}, {
945+
name: "object star used in a string field",
946+
fields: fields{
947+
Params: []v1beta1.ParamSpec{{
948+
Name: "gitrepo",
949+
Type: v1beta1.ParamTypeObject,
950+
Properties: map[string]v1beta1.PropertySpec{
951+
"url": {},
952+
"commit": {},
953+
},
954+
}},
955+
Steps: []v1beta1.Step{{
956+
Name: "do-the-clone",
957+
Image: "$(params.gitrepo[*])",
958+
Args: []string{"echo"},
959+
WorkingDir: "/foo/bar/src/",
960+
}},
961+
},
962+
expectedError: apis.FieldError{
963+
Message: `variable type invalid in "$(params.gitrepo[*])"`,
964+
Paths: []string{"steps[0].image"},
965+
},
966+
}, {
967+
name: "object used in a field that can accept array type",
968+
fields: fields{
969+
Params: []v1beta1.ParamSpec{{
970+
Name: "gitrepo",
971+
Type: v1beta1.ParamTypeObject,
972+
Properties: map[string]v1beta1.PropertySpec{
973+
"url": {},
974+
"commit": {},
975+
},
976+
}},
977+
Steps: []v1beta1.Step{{
978+
Name: "do-the-clone",
979+
Image: "myimage",
980+
Args: []string{"$(params.gitrepo)"},
981+
WorkingDir: "/foo/bar/src/",
982+
}},
983+
},
984+
expectedError: apis.FieldError{
985+
Message: `variable type invalid in "$(params.gitrepo)"`,
986+
Paths: []string{"steps[0].args[0]"},
987+
},
988+
}, {
989+
name: "object star used in a field that can accept array type",
990+
fields: fields{
991+
Params: []v1beta1.ParamSpec{{
992+
Name: "gitrepo",
993+
Type: v1beta1.ParamTypeObject,
994+
Properties: map[string]v1beta1.PropertySpec{
995+
"url": {},
996+
"commit": {},
997+
},
998+
}},
999+
Steps: []v1beta1.Step{{
1000+
Name: "do-the-clone",
1001+
Image: "some-git-image",
1002+
Args: []string{"$(params.gitrepo[*])"},
1003+
WorkingDir: "/foo/bar/src/",
1004+
}},
1005+
},
1006+
expectedError: apis.FieldError{
1007+
Message: `variable type invalid in "$(params.gitrepo[*])"`,
1008+
Paths: []string{"steps[0].args[0]"},
1009+
},
9041010
}, {
9051011
name: "Inexistent param variable in volumeMount with existing",
9061012
fields: fields{

pkg/substitution/substitution.go

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,31 @@ func ValidateVariableProhibitedP(value, prefix string, vars sets.String) *apis.F
109109
return nil
110110
}
111111

112+
// ValidateEntireVariableProhibitedP verifies that values of object type are not used as whole.
113+
func ValidateEntireVariableProhibitedP(value, prefix string, vars sets.String) *apis.FieldError {
114+
vs, err := extractEntireVariablesFromString(value, prefix)
115+
if err != nil {
116+
return &apis.FieldError{
117+
Message: fmt.Sprintf("extractEntireVariablesFromString failed : %v", err),
118+
// Empty path is required to make the `ViaField`, … work
119+
Paths: []string{""},
120+
}
121+
}
122+
123+
for _, v := range vs {
124+
v = strings.TrimSuffix(v, "[*]")
125+
if vars.Has(v) {
126+
return &apis.FieldError{
127+
Message: fmt.Sprintf("variable type invalid in %q", value),
128+
// Empty path is required to make the `ViaField`, … work
129+
Paths: []string{""},
130+
}
131+
}
132+
}
133+
134+
return nil
135+
}
136+
112137
// ValidateVariableIsolated verifies that variables matching the relevant string expressions are completely isolated if present.
113138
func ValidateVariableIsolated(name, value, prefix, locationName, path string, vars sets.String) *apis.FieldError {
114139
if vs, present, _ := extractVariablesFromString(value, prefix); present {
@@ -180,38 +205,57 @@ func extractVariablesFromString(s, prefix string) ([]string, bool, string) {
180205
groups := matchGroups(match, re)
181206
for j, v := range []string{"var1", "var2", "var3"} {
182207
val := groups[v]
183-
// if using the dot notation
208+
// If using the dot notation, the number of dot-separated components is restricted up to 2.
209+
// Valid Examples:
210+
// - extract "aString" from <prefix>.aString
211+
// - extract "anObject" from <prefix>.anObject.key
212+
// Invalid Examples:
213+
// - <prefix>.foo.bar.baz....
184214
if j == 0 && strings.Contains(val, ".") {
185-
switch prefix {
186-
case "params":
187-
// params can only have a maximum of two components in the dot notation otherwise it needs to use the bracket notation.
188-
if len(strings.Split(val, ".")) > 0 {
189-
errString = fmt.Sprintf(`Invalid referencing of parameters in %s !!! You can only use the dots inside single or double quotes. eg. $(params["org.foo.blah"]) or $(params['org.foo.blah']) are valid references but NOT $params.org.foo.blah.`, s)
190-
return vars, true, errString
191-
}
192-
case "resources.(?:inputs|outputs)":
193-
// resources can only have a maximum of 4 components.
194-
if len(strings.Split(val, ".")) > 2 {
195-
errString = fmt.Sprintf(`Invalid referencing of parameters in %s !!! resources.* can only have 4 components (eg. resources.inputs.foo.bar). Found more than 4 components.`, s)
196-
return vars, true, errString
197-
}
198-
vars[i] = strings.SplitN(val, ".", 2)[0]
199-
default:
200-
// for backwards compatibality
201-
vars[i] = strings.SplitN(val, ".", 2)[0]
215+
if len(strings.Split(val, ".")) > 2 {
216+
errString = fmt.Sprintf(`Invalid referencing of parameters in "%s"! Only two dot-separated components after the prefix "%s" are allowed.`, s, prefix)
217+
return vars, true, errString
202218
}
219+
vars[i] = strings.SplitN(val, ".", 2)[0]
203220
break
204221
}
205-
if groups[v] != "" {
222+
if val != "" {
206223
vars[i] = val
207224
break
208225
}
209-
210226
}
211227
}
212228
return vars, true, errString
213229
}
214230

231+
func extractEntireVariablesFromString(s, prefix string) ([]string, error) {
232+
pattern := fmt.Sprintf(braceMatchingRegex, prefix, parameterSubstitution, parameterSubstitution, parameterSubstitution)
233+
re, err := regexp.Compile(pattern)
234+
if err != nil {
235+
return nil, fmt.Errorf("Fail to parse regex pattern: %v", err)
236+
}
237+
238+
matches := re.FindAllStringSubmatch(s, -1)
239+
if len(matches) == 0 {
240+
return []string{}, nil
241+
}
242+
vars := make([]string, len(matches))
243+
for i, match := range matches {
244+
groups := matchGroups(match, re)
245+
// foo -> foo
246+
// foo.bar -> foo.bar
247+
// foo.bar.baz -> foo.bar.baz
248+
for _, v := range []string{"var1", "var2", "var3"} {
249+
val := groups[v]
250+
if val != "" {
251+
vars[i] = val
252+
break
253+
}
254+
}
255+
}
256+
return vars, nil
257+
}
258+
215259
func matchGroups(matches []string, pattern *regexp.Regexp) map[string]string {
216260
groups := make(map[string]string)
217261
for i, name := range pattern.SubexpNames()[1:] {

0 commit comments

Comments
 (0)