pipeline/pkg/apis/pipeline/v1alpha1/task_validation.go
Scott 5824fd514c Helpful error message when multiple volumes share name
A Task is not allowed to have multiple volumes that share a name. Prior
to this commit the error message printed when a name collision occurred
was "expected exactly one, got both: name" which doesn't describe the
problem clearly.

After this commit the error message is updated to 'multiple volumes with
same name "foo"'. A test case has been added to exercise the new message.
2019-10-09 13:24:18 -05:00

321 lines
9.7 KiB
Go

/*
Copyright 2019 The Tekton Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
"context"
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/util/validation"
"knative.dev/pkg/apis"
)
func (t *Task) Validate(ctx context.Context) *apis.FieldError {
if err := validateObjectMetadata(t.GetObjectMeta()); err != nil {
return err.ViaField("metadata")
}
return t.Spec.Validate(ctx)
}
func (ts *TaskSpec) Validate(ctx context.Context) *apis.FieldError {
if equality.Semantic.DeepEqual(ts, &TaskSpec{}) {
return apis.ErrMissingField(apis.CurrentField)
}
if len(ts.Steps) == 0 {
return apis.ErrMissingField("steps")
}
if err := ValidateVolumes(ts.Volumes).ViaField("volumes"); err != nil {
return err
}
mergedSteps, err := MergeStepsWithStepTemplate(ts.StepTemplate, ts.Steps)
if err != nil {
return &apis.FieldError{
Message: fmt.Sprintf("error merging step template and steps: %s", err),
Paths: []string{"stepTemplate"},
}
}
if err := validateSteps(mergedSteps).ViaField("steps"); err != nil {
return err
}
// A task doesn't have to have inputs or outputs, but if it does they must be valid.
// A task can't duplicate input or output names.
if ts.Inputs != nil {
for _, resource := range ts.Inputs.Resources {
if err := validateResourceType(resource, fmt.Sprintf("taskspec.Inputs.Resources.%s.Type", resource.Name)); err != nil {
return err
}
}
if err := checkForDuplicates(ts.Inputs.Resources, "taskspec.Inputs.Resources.Name"); err != nil {
return err
}
if err := validateInputParameterTypes(ts.Inputs); err != nil {
return err
}
}
if ts.Outputs != nil {
for _, resource := range ts.Outputs.Resources {
if err := validateResourceType(resource, fmt.Sprintf("taskspec.Outputs.Resources.%s.Type", resource.Name)); err != nil {
return err
}
}
if err := checkForDuplicates(ts.Outputs.Resources, "taskspec.Outputs.Resources.Name"); err != nil {
return err
}
}
// Validate task step names
for _, step := range ts.Steps {
if errs := validation.IsDNS1123Label(step.Name); step.Name != "" && len(errs) > 0 {
return &apis.FieldError{
Message: fmt.Sprintf("invalid value %q", step.Name),
Paths: []string{"taskspec.steps.name"},
Details: "Task step name must be a valid DNS Label, For more info refer to https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names",
}
}
}
if err := validateInputParameterVariables(ts.Steps, ts.Inputs); err != nil {
return err
}
if err := validateResourceVariables(ts.Steps, ts.Inputs, ts.Outputs); err != nil {
return err
}
return nil
}
func ValidateVolumes(volumes []corev1.Volume) *apis.FieldError {
// Task must not have duplicate volume names.
vols := map[string]struct{}{}
for _, v := range volumes {
if _, ok := vols[v.Name]; ok {
return &apis.FieldError{
Message: fmt.Sprintf("multiple volumes with same name %q", v.Name),
Paths: []string{"name"},
}
}
vols[v.Name] = struct{}{}
}
return nil
}
func validateSteps(steps []Step) *apis.FieldError {
// Task must not have duplicate step names.
names := map[string]struct{}{}
for _, s := range steps {
if s.Image == "" {
return apis.ErrMissingField("Image")
}
if s.Name == "" {
continue
}
if _, ok := names[s.Name]; ok {
return apis.ErrInvalidValue(s.Name, "name")
}
names[s.Name] = struct{}{}
}
return nil
}
func validateInputParameterTypes(inputs *Inputs) *apis.FieldError {
for _, p := range inputs.Params {
// Ensure param has a valid type.
validType := false
for _, allowedType := range AllParamTypes {
if p.Type == allowedType {
validType = true
}
}
if !validType {
return apis.ErrInvalidValue(p.Type, fmt.Sprintf("taskspec.inputs.params.%s.type", p.Name))
}
// If a default value is provided, ensure its type matches param's declared type.
if (p.Default != nil) && (p.Default.Type != p.Type) {
return &apis.FieldError{
Message: fmt.Sprintf(
"\"%v\" type does not match default value's type: \"%v\"", p.Type, p.Default.Type),
Paths: []string{
fmt.Sprintf("taskspec.inputs.params.%s.type", p.Name),
fmt.Sprintf("taskspec.inputs.params.%s.default.type", p.Name),
},
}
}
}
return nil
}
func validateInputParameterVariables(steps []Step, inputs *Inputs) *apis.FieldError {
parameterNames := map[string]struct{}{}
arrayParameterNames := map[string]struct{}{}
if inputs != nil {
for _, p := range inputs.Params {
parameterNames[p.Name] = struct{}{}
if p.Type == ParamTypeArray {
arrayParameterNames[p.Name] = struct{}{}
}
}
}
if err := validateVariables(steps, "params", parameterNames); err != nil {
return err
}
return validateArrayUsage(steps, "params", arrayParameterNames)
}
func validateResourceVariables(steps []Step, inputs *Inputs, outputs *Outputs) *apis.FieldError {
resourceNames := map[string]struct{}{}
if inputs != nil {
for _, r := range inputs.Resources {
resourceNames[r.Name] = struct{}{}
}
}
if outputs != nil {
for _, r := range outputs.Resources {
resourceNames[r.Name] = struct{}{}
if r.Type == PipelineResourceTypeImage {
if r.OutputImageDir == "" {
return apis.ErrMissingField("OutputImageDir")
}
}
}
}
return validateVariables(steps, "resources", resourceNames)
}
func validateArrayUsage(steps []Step, prefix string, vars map[string]struct{}) *apis.FieldError {
for _, step := range steps {
if err := validateTaskNoArrayReferenced("name", step.Name, prefix, vars); err != nil {
return err
}
if err := validateTaskNoArrayReferenced("image", step.Image, prefix, vars); err != nil {
return err
}
if err := validateTaskNoArrayReferenced("workingDir", step.WorkingDir, prefix, vars); err != nil {
return err
}
for i, cmd := range step.Command {
if err := validateTaskArraysIsolated(fmt.Sprintf("command[%d]", i), cmd, prefix, vars); err != nil {
return err
}
}
for i, arg := range step.Args {
if err := validateTaskArraysIsolated(fmt.Sprintf("arg[%d]", i), arg, prefix, vars); err != nil {
return err
}
}
for _, env := range step.Env {
if err := validateTaskNoArrayReferenced(fmt.Sprintf("env[%s]", env.Name), env.Value, prefix, vars); err != nil {
return err
}
}
for i, v := range step.VolumeMounts {
if err := validateTaskNoArrayReferenced(fmt.Sprintf("volumeMount[%d].Name", i), v.Name, prefix, vars); err != nil {
return err
}
if err := validateTaskNoArrayReferenced(fmt.Sprintf("volumeMount[%d].MountPath", i), v.MountPath, prefix, vars); err != nil {
return err
}
if err := validateTaskNoArrayReferenced(fmt.Sprintf("volumeMount[%d].SubPath", i), v.SubPath, prefix, vars); err != nil {
return err
}
}
}
return nil
}
func validateVariables(steps []Step, prefix string, vars map[string]struct{}) *apis.FieldError {
for _, step := range steps {
if err := validateTaskVariable("name", step.Name, prefix, vars); err != nil {
return err
}
if err := validateTaskVariable("image", step.Image, prefix, vars); err != nil {
return err
}
if err := validateTaskVariable("workingDir", step.WorkingDir, prefix, vars); err != nil {
return err
}
for i, cmd := range step.Command {
if err := validateTaskVariable(fmt.Sprintf("command[%d]", i), cmd, prefix, vars); err != nil {
return err
}
}
for i, arg := range step.Args {
if err := validateTaskVariable(fmt.Sprintf("arg[%d]", i), arg, prefix, vars); err != nil {
return err
}
}
for _, env := range step.Env {
if err := validateTaskVariable(fmt.Sprintf("env[%s]", env.Name), env.Value, prefix, vars); err != nil {
return err
}
}
for i, v := range step.VolumeMounts {
if err := validateTaskVariable(fmt.Sprintf("volumeMount[%d].Name", i), v.Name, prefix, vars); err != nil {
return err
}
if err := validateTaskVariable(fmt.Sprintf("volumeMount[%d].MountPath", i), v.MountPath, prefix, vars); err != nil {
return err
}
if err := validateTaskVariable(fmt.Sprintf("volumeMount[%d].SubPath", i), v.SubPath, prefix, vars); err != nil {
return err
}
}
}
return nil
}
func validateTaskVariable(name, value, prefix string, vars map[string]struct{}) *apis.FieldError {
return ValidateVariable(name, value, prefix, "(?:inputs|outputs).", "step", "taskspec.steps", vars)
}
func validateTaskNoArrayReferenced(name, value, prefix string, arrayNames map[string]struct{}) *apis.FieldError {
return ValidateVariableProhibited(name, value, prefix, "(?:inputs|outputs).", "step", "taskspec.steps", arrayNames)
}
func validateTaskArraysIsolated(name, value, prefix string, arrayNames map[string]struct{}) *apis.FieldError {
return ValidateVariableIsolated(name, value, prefix, "(?:inputs|outputs).", "step", "taskspec.steps", arrayNames)
}
func checkForDuplicates(resources []TaskResource, path string) *apis.FieldError {
encountered := map[string]struct{}{}
for _, r := range resources {
if _, ok := encountered[strings.ToLower(r.Name)]; ok {
return apis.ErrMultipleOneOf(path)
}
encountered[strings.ToLower(r.Name)] = struct{}{}
}
return nil
}
func validateResourceType(r TaskResource, path string) *apis.FieldError {
for _, allowed := range AllResourceTypes {
if r.Type == allowed {
return nil
}
}
return apis.ErrInvalidValue(string(r.Type), path)
}