k8s.io/apiserver@v0.31.1/pkg/cel/library/format.go (about)

     1  /*
     2  Copyright 2024 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package library
    18  
    19  import (
    20  	"fmt"
    21  	"net/url"
    22  
    23  	"github.com/asaskevich/govalidator"
    24  	"github.com/google/cel-go/cel"
    25  	"github.com/google/cel-go/common/decls"
    26  	"github.com/google/cel-go/common/types"
    27  	"github.com/google/cel-go/common/types/ref"
    28  	apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
    29  	"k8s.io/apimachinery/pkg/util/validation"
    30  	apiservercel "k8s.io/apiserver/pkg/cel"
    31  	"k8s.io/kube-openapi/pkg/validation/strfmt"
    32  )
    33  
    34  // Format provides a CEL library exposing common named Kubernetes string
    35  // validations. Can be used in CRD ValidationRules messageExpression.
    36  //
    37  //  Example:
    38  //
    39  //    rule:              format.dns1123label.validate(object.metadata.name).hasValue()
    40  //    messageExpression: format.dns1123label.validate(object.metadata.name).value().join("\n")
    41  //
    42  // format.named(name: string) -> ?Format
    43  //
    44  //  Returns the Format with the given name, if it exists. Otherwise, optional.none
    45  //  Allowed names are:
    46  // 	 - `dns1123Label`
    47  // 	 - `dns1123Subdomain`
    48  // 	 - `dns1035Label`
    49  // 	 - `qualifiedName`
    50  // 	 - `dns1123LabelPrefix`
    51  // 	 - `dns1123SubdomainPrefix`
    52  // 	 - `dns1035LabelPrefix`
    53  // 	 - `labelValue`
    54  // 	 - `uri`
    55  // 	 - `uuid`
    56  // 	 - `byte`
    57  // 	 - `date`
    58  // 	 - `datetime`
    59  //
    60  // format.<formatName>() -> Format
    61  //
    62  //  Convenience functions for all the named formats are also available
    63  //
    64  //  Examples:
    65  //      format.dns1123Label().validate("my-label-name")
    66  //      format.dns1123Subdomain().validate("apiextensions.k8s.io")
    67  //      format.dns1035Label().validate("my-label-name")
    68  //      format.qualifiedName().validate("apiextensions.k8s.io/v1beta1")
    69  //      format.dns1123LabelPrefix().validate("my-label-prefix-")
    70  //      format.dns1123SubdomainPrefix().validate("mysubdomain.prefix.-")
    71  //      format.dns1035LabelPrefix().validate("my-label-prefix-")
    72  //      format.uri().validate("http://example.com")
    73  //          Uses same pattern as isURL, but returns an error
    74  //      format.uuid().validate("123e4567-e89b-12d3-a456-426614174000")
    75  //      format.byte().validate("aGVsbG8=")
    76  //      format.date().validate("2021-01-01")
    77  //      format.datetime().validate("2021-01-01T00:00:00Z")
    78  //
    79  
    80  // <Format>.validate(str: string) -> ?list<string>
    81  //
    82  //	Validates the given string against the given format. Returns optional.none
    83  //	if the string is valid, otherwise a list of validation error strings.
    84  func Format() cel.EnvOption {
    85  	return cel.Lib(formatLib)
    86  }
    87  
    88  var formatLib = &format{}
    89  
    90  type format struct{}
    91  
    92  func (*format) LibraryName() string {
    93  	return "format"
    94  }
    95  
    96  func ZeroArgumentFunctionBinding(binding func() ref.Val) decls.OverloadOpt {
    97  	return func(o *decls.OverloadDecl) (*decls.OverloadDecl, error) {
    98  		wrapped, err := decls.FunctionBinding(func(values ...ref.Val) ref.Val { return binding() })(o)
    99  		if err != nil {
   100  			return nil, err
   101  		}
   102  		if len(wrapped.ArgTypes()) != 0 {
   103  			return nil, fmt.Errorf("function binding must have 0 arguments")
   104  		}
   105  		return o, nil
   106  	}
   107  }
   108  
   109  func (*format) CompileOptions() []cel.EnvOption {
   110  	options := make([]cel.EnvOption, 0, len(formatLibraryDecls))
   111  	for name, overloads := range formatLibraryDecls {
   112  		options = append(options, cel.Function(name, overloads...))
   113  	}
   114  	for name, constantValue := range ConstantFormats {
   115  		prefixedName := "format." + name
   116  		options = append(options, cel.Function(prefixedName, cel.Overload(prefixedName, []*cel.Type{}, apiservercel.FormatType, ZeroArgumentFunctionBinding(func() ref.Val {
   117  			return constantValue
   118  		}))))
   119  	}
   120  	return options
   121  }
   122  
   123  func (*format) ProgramOptions() []cel.ProgramOption {
   124  	return []cel.ProgramOption{}
   125  }
   126  
   127  var ConstantFormats map[string]*apiservercel.Format = map[string]*apiservercel.Format{
   128  	"dns1123Label": {
   129  		Name:         "DNS1123Label",
   130  		ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSLabel(s, false) },
   131  		MaxRegexSize: 30,
   132  	},
   133  	"dns1123Subdomain": {
   134  		Name:         "DNS1123Subdomain",
   135  		ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSSubdomain(s, false) },
   136  		MaxRegexSize: 60,
   137  	},
   138  	"dns1035Label": {
   139  		Name:         "DNS1035Label",
   140  		ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNS1035Label(s, false) },
   141  		MaxRegexSize: 30,
   142  	},
   143  	"qualifiedName": {
   144  		Name:         "QualifiedName",
   145  		ValidateFunc: validation.IsQualifiedName,
   146  		MaxRegexSize: 60, // uses subdomain regex
   147  	},
   148  
   149  	"dns1123LabelPrefix": {
   150  		Name:         "DNS1123LabelPrefix",
   151  		ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSLabel(s, true) },
   152  		MaxRegexSize: 30,
   153  	},
   154  	"dns1123SubdomainPrefix": {
   155  		Name:         "DNS1123SubdomainPrefix",
   156  		ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSSubdomain(s, true) },
   157  		MaxRegexSize: 60,
   158  	},
   159  	"dns1035LabelPrefix": {
   160  		Name:         "DNS1035LabelPrefix",
   161  		ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNS1035Label(s, true) },
   162  		MaxRegexSize: 30,
   163  	},
   164  	"labelValue": {
   165  		Name:         "LabelValue",
   166  		ValidateFunc: validation.IsValidLabelValue,
   167  		MaxRegexSize: 40,
   168  	},
   169  
   170  	// CRD formats
   171  	// Implementations sourced from strfmt, which kube-openapi uses as its
   172  	// format library. There are other CRD formats supported, but they are
   173  	// covered by other portions of the CEL library (like IP/CIDR), or their
   174  	// use is discouraged (like bsonobjectid, email, etc)
   175  	"uri": {
   176  		Name: "URI",
   177  		ValidateFunc: func(s string) []string {
   178  			// Directly call ParseRequestURI since we can get a better error message
   179  			_, err := url.ParseRequestURI(s)
   180  			if err != nil {
   181  				return []string{err.Error()}
   182  			}
   183  			return nil
   184  		},
   185  		// Use govalidator url regex to estimate, since ParseRequestURI
   186  		// doesnt use regex
   187  		MaxRegexSize: len(govalidator.URL),
   188  	},
   189  	"uuid": {
   190  		Name: "uuid",
   191  		ValidateFunc: func(s string) []string {
   192  			if !strfmt.Default.Validates("uuid", s) {
   193  				return []string{"does not match the UUID format"}
   194  			}
   195  			return nil
   196  		},
   197  		MaxRegexSize: len(strfmt.UUIDPattern),
   198  	},
   199  	"byte": {
   200  		Name: "byte",
   201  		ValidateFunc: func(s string) []string {
   202  			if !strfmt.Default.Validates("byte", s) {
   203  				return []string{"invalid base64"}
   204  			}
   205  			return nil
   206  		},
   207  		MaxRegexSize: len(govalidator.Base64),
   208  	},
   209  	"date": {
   210  		Name: "date",
   211  		ValidateFunc: func(s string) []string {
   212  			if !strfmt.Default.Validates("date", s) {
   213  				return []string{"invalid date"}
   214  			}
   215  			return nil
   216  		},
   217  		// Estimated regex size for RFC3339FullDate which is
   218  		// a date format. Assume a date-time pattern is longer
   219  		// so use that to conservatively estimate this
   220  		MaxRegexSize: len(strfmt.DateTimePattern),
   221  	},
   222  	"datetime": {
   223  		Name: "datetime",
   224  		ValidateFunc: func(s string) []string {
   225  			if !strfmt.Default.Validates("datetime", s) {
   226  				return []string{"invalid datetime"}
   227  			}
   228  			return nil
   229  		},
   230  		MaxRegexSize: len(strfmt.DateTimePattern),
   231  	},
   232  }
   233  
   234  var formatLibraryDecls = map[string][]cel.FunctionOpt{
   235  	"validate": {
   236  		cel.MemberOverload("format-validate", []*cel.Type{apiservercel.FormatType, cel.StringType}, cel.OptionalType(cel.ListType(cel.StringType)), cel.BinaryBinding(formatValidate)),
   237  	},
   238  	"format.named": {
   239  		cel.Overload("format-named", []*cel.Type{cel.StringType}, cel.OptionalType(apiservercel.FormatType), cel.UnaryBinding(func(name ref.Val) ref.Val {
   240  			nameString, ok := name.Value().(string)
   241  			if !ok {
   242  				return types.MaybeNoSuchOverloadErr(name)
   243  			}
   244  
   245  			f, ok := ConstantFormats[nameString]
   246  			if !ok {
   247  				return types.OptionalNone
   248  			}
   249  			return types.OptionalOf(f)
   250  		})),
   251  	},
   252  }
   253  
   254  func formatValidate(arg1, arg2 ref.Val) ref.Val {
   255  	f, ok := arg1.Value().(*apiservercel.Format)
   256  	if !ok {
   257  		return types.MaybeNoSuchOverloadErr(arg1)
   258  	}
   259  
   260  	str, ok := arg2.Value().(string)
   261  	if !ok {
   262  		return types.MaybeNoSuchOverloadErr(arg2)
   263  	}
   264  
   265  	res := f.ValidateFunc(str)
   266  	if len(res) == 0 {
   267  		return types.OptionalNone
   268  	}
   269  	return types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, res))
   270  }