github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/pkg/api/kptfile/v1/validation.go (about)

     1  // Copyright 2021 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package v1
    16  
    17  import (
    18  	"fmt"
    19  	"path/filepath"
    20  	"regexp"
    21  	"strings"
    22  
    23  	"github.com/GoogleContainerTools/kpt/internal/types"
    24  	"sigs.k8s.io/kustomize/api/konfig"
    25  	kustomizetypes "sigs.k8s.io/kustomize/api/types"
    26  	"sigs.k8s.io/kustomize/kyaml/filesys"
    27  	"sigs.k8s.io/kustomize/kyaml/kio"
    28  	"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
    29  	"sigs.k8s.io/kustomize/kyaml/yaml"
    30  )
    31  
    32  const (
    33  	// constants related to kustomize
    34  	kustomizationAPIGroup = "kustomize.config.k8s.io"
    35  )
    36  
    37  func (kf *KptFile) Validate(fsys filesys.FileSystem, pkgPath types.UniquePath) error {
    38  	if err := kf.Pipeline.validate(fsys, pkgPath); err != nil {
    39  		return fmt.Errorf("invalid pipeline: %w", err)
    40  	}
    41  	// TODO: validate other fields
    42  	return nil
    43  }
    44  
    45  // validate will validate all fields in the Pipeline
    46  // 'mutators' and 'validators' share same schema and
    47  // they are valid if all functions in them are ALL valid.
    48  func (p *Pipeline) validate(fsys filesys.FileSystem, pkgPath types.UniquePath) error {
    49  	if p == nil {
    50  		return nil
    51  	}
    52  	for i := range p.Mutators {
    53  		f := p.Mutators[i]
    54  		err := f.validate(fsys, "mutators", i, pkgPath)
    55  		if err != nil {
    56  			return fmt.Errorf("function %q: %w", f.Image, err)
    57  		}
    58  	}
    59  	for i := range p.Validators {
    60  		f := p.Validators[i]
    61  		err := f.validate(fsys, "validators", i, pkgPath)
    62  		if err != nil {
    63  			return fmt.Errorf("function %q: %w", f.Image, err)
    64  		}
    65  	}
    66  	return nil
    67  }
    68  
    69  func (f *Function) validate(fsys filesys.FileSystem, fnType string, idx int, pkgPath types.UniquePath) error {
    70  	if f.Image == "" && f.Exec == "" {
    71  		return &ValidateError{
    72  			Field:  fmt.Sprintf("pipeline.%s[%d]", fnType, idx),
    73  			Reason: "must specify a functon (`image` or `exec`) to execute",
    74  		}
    75  	}
    76  	if f.Image != "" && f.Exec != "" {
    77  		return &ValidateError{
    78  			Field:  fmt.Sprintf("pipeline.%s[%d]", fnType, idx),
    79  			Reason: "must not specify both `image` and `exec` at the same time",
    80  		}
    81  	}
    82  	if f.Image != "" {
    83  		err := ValidateFunctionImageURL(f.Image)
    84  		if err != nil {
    85  			return &ValidateError{
    86  				Field:  fmt.Sprintf("pipeline.%s[%d].image", fnType, idx),
    87  				Value:  f.Image,
    88  				Reason: err.Error(),
    89  			}
    90  		}
    91  	}
    92  	// TODO(droot): validate the exec
    93  
    94  	if len(f.ConfigMap) != 0 && f.ConfigPath != "" {
    95  		return &ValidateError{
    96  			Field:  fmt.Sprintf("pipeline.%s[%d]", fnType, idx),
    97  			Reason: "functionConfig must not specify both `configMap` and `configPath` at the same time",
    98  		}
    99  	}
   100  
   101  	if f.ConfigPath != "" {
   102  		if err := validateFnConfigPathSyntax(f.ConfigPath); err != nil {
   103  			return &ValidateError{
   104  				Field:  fmt.Sprintf("pipeline.%s[%d].configPath", fnType, idx),
   105  				Value:  f.ConfigPath,
   106  				Reason: err.Error(),
   107  			}
   108  		}
   109  		if _, err := GetValidatedFnConfigFromPath(fsys, pkgPath, f.ConfigPath); err != nil {
   110  			return &ValidateError{
   111  				Field:  fmt.Sprintf("pipeline.%s[%d].configPath", fnType, idx),
   112  				Value:  f.ConfigPath,
   113  				Reason: err.Error(),
   114  			}
   115  		}
   116  	}
   117  	return nil
   118  }
   119  
   120  // ValidateFunctionImageURL validates the function name.
   121  // According to Docker implementation
   122  // https://github.com/docker/distribution/blob/master/reference/reference.go. A valid
   123  // name definition is:
   124  //
   125  //	name                            := [domain '/'] path-component ['/' path-component]*
   126  //	domain                          := domain-component ['.' domain-component]* [':' port-number]
   127  //	domain-component                := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
   128  //	port-number                     := /[0-9]+/
   129  //	path-component                  := alpha-numeric [separator alpha-numeric]*
   130  //	alpha-numeric                   := /[a-z0-9]+/
   131  //	separator                       := /[_.]|__|[-]*/
   132  func ValidateFunctionImageURL(name string) error {
   133  	pathComponentRegexp := `(?:[a-z0-9](?:(?:[_.]|__|[-]*)[a-z0-9]+)*)`
   134  	domainComponentRegexp := `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`
   135  	domainRegexp := fmt.Sprintf(`%s(?:\.%s)*(?:\:[0-9]+)?`, domainComponentRegexp, domainComponentRegexp)
   136  	nameRegexp := fmt.Sprintf(`(?:%s\/)?%s(?:\/%s)*`, domainRegexp,
   137  		pathComponentRegexp, pathComponentRegexp)
   138  	tagRegexp := `(?:[\w][\w.-]{0,127})`
   139  	shaRegexp := `(sha256:[a-zA-Z0-9]{64})`
   140  	versionRegexp := fmt.Sprintf(`(%s|%s)`, tagRegexp, shaRegexp)
   141  	r := fmt.Sprintf(`^(?:%s(?:(\:|@)%s)?)$`, nameRegexp, versionRegexp)
   142  
   143  	matched, err := regexp.MatchString(r, name)
   144  	if err != nil {
   145  		return err
   146  	}
   147  	if !matched {
   148  		return fmt.Errorf("function name %q is invalid", name)
   149  	}
   150  	return nil
   151  }
   152  
   153  // validateFnConfigPathSyntax validates syntactic correctness of given functionConfig path
   154  // and return an error if it's invalid.
   155  func validateFnConfigPathSyntax(p string) error {
   156  	if strings.TrimSpace(p) == "" {
   157  		return fmt.Errorf("path must not be empty")
   158  	}
   159  	p = filepath.Clean(p)
   160  	if filepath.IsAbs(p) {
   161  		return fmt.Errorf("path must be relative")
   162  	}
   163  	if strings.Contains(p, "..") {
   164  		// fn config must not live outside the package directory
   165  		// Allowing outside path opens up an attack vector that allows
   166  		// reading any YAML file on package consumer's machine.
   167  		return fmt.Errorf("path must not be outside the package")
   168  	}
   169  	return nil
   170  }
   171  
   172  // GetValidatedFnConfigFromPath validates the functionConfig at the path specified by
   173  // the package path (pkgPath) and configPath, returning the functionConfig as an
   174  // RNode if the validation is successful.
   175  func GetValidatedFnConfigFromPath(fsys filesys.FileSystem, pkgPath types.UniquePath, configPath string) (*yaml.RNode, error) {
   176  	path := filepath.Join(string(pkgPath), configPath)
   177  	file, err := fsys.Open(path)
   178  	if err != nil {
   179  		return nil, fmt.Errorf("functionConfig must exist in the current package")
   180  	}
   181  	defer file.Close()
   182  	reader := kio.ByteReader{Reader: file, PreserveSeqIndent: true, WrapBareSeqNode: true, DisableUnwrapping: true}
   183  	nodes, err := reader.Read()
   184  	if err != nil {
   185  		return nil, fmt.Errorf("failed to read functionConfig %q: %w", configPath, err)
   186  	}
   187  	if len(nodes) > 1 {
   188  		return nil, fmt.Errorf("functionConfig %q must not contain more than one config, got %d", configPath, len(nodes))
   189  	}
   190  	if err := IsKRM(nodes[0]); err != nil {
   191  		return nil, fmt.Errorf("functionConfig %q: %s", configPath, err.Error())
   192  	}
   193  	return nodes[0], nil
   194  }
   195  
   196  // AreKRM validates if given resources are valid KRM resources.
   197  func AreKRM(nodes []*yaml.RNode) error {
   198  	for i := range nodes {
   199  		if err := IsKRM(nodes[i]); err != nil {
   200  			path, _, _ := kioutil.GetFileAnnotations(nodes[i])
   201  			return fmt.Errorf("%s: %s", path, err.Error())
   202  		}
   203  	}
   204  	return nil
   205  }
   206  
   207  // IsKRM validates if given resource is a valid KRM resource by ensuring
   208  // that resource has a valid apiVersion, kind and metadata.name field.
   209  // It excludes kustomization resource from KRM check.
   210  func IsKRM(n *yaml.RNode) error {
   211  	if isKustomization(n) {
   212  		// exclude kustomization files from KRM check
   213  		// https://github.com/GoogleContainerTools/kpt/issues/2388
   214  		return nil
   215  	}
   216  	meta, err := n.GetMeta()
   217  	if err != nil {
   218  		return fmt.Errorf("resource must have `apiVersion`, `kind`, and `name`")
   219  	}
   220  	if meta.APIVersion == "" {
   221  		return fmt.Errorf("resource must have `apiVersion`")
   222  	}
   223  	if meta.Kind == "" {
   224  		return fmt.Errorf("resource must have `kind`")
   225  	}
   226  	if meta.Name == "" {
   227  		return fmt.Errorf("resource must have `metadata.name`")
   228  	}
   229  	return nil
   230  }
   231  
   232  // isKustomization determines if given YAML is a kustomization file or resource.
   233  func isKustomization(n *yaml.RNode) bool {
   234  	resourcePath, _, err := kioutil.GetFileAnnotations(n)
   235  	if err == nil {
   236  		// perform the check only if we are able to reliably
   237  		// read the file path of the resource
   238  		resourceFile := filepath.Base(resourcePath)
   239  
   240  		for _, kustomizationFileName := range konfig.RecognizedKustomizationFileNames() {
   241  			if resourceFile == kustomizationFileName {
   242  				return true
   243  			}
   244  		}
   245  	}
   246  	meta, err := n.GetMeta()
   247  	if err != nil {
   248  		return false
   249  	}
   250  
   251  	if strings.HasPrefix(meta.APIVersion, kustomizationAPIGroup) {
   252  		return true
   253  	}
   254  
   255  	if meta.APIVersion == "" && meta.Kind == kustomizetypes.KustomizationKind {
   256  		return true
   257  	}
   258  
   259  	return false
   260  }
   261  
   262  // ValidateError is the error returned when validation fails.
   263  type ValidateError struct {
   264  	// Field is the field that causes error
   265  	Field string
   266  	// Value is the value of invalid field
   267  	Value string
   268  	// Reason is the reason for the error
   269  	Reason string
   270  }
   271  
   272  func (e *ValidateError) Error() string {
   273  	var sb strings.Builder
   274  	sb.WriteString(fmt.Sprintf("Kptfile is invalid:\nField: `%s`\n", e.Field))
   275  	if e.Value != "" {
   276  		sb.WriteString(fmt.Sprintf("Value: %q\n", e.Value))
   277  	}
   278  	sb.WriteString(fmt.Sprintf("Reason: %s\n", e.Reason))
   279  	return sb.String()
   280  }