api/tests/validator.go

244 lines
8.3 KiB
Go

// Copyright Istio 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 crd
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apiextval "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
structurallisttype "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/listtype"
structuralpruning "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning"
"k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
kubeyaml "k8s.io/apimachinery/pkg/util/yaml"
celconfig "k8s.io/apiserver/pkg/apis/cel"
)
// Validator returns a new validator for custom resources
// Warning: this is meant for usage in tests only
type Validator struct {
byGvk map[schema.GroupVersionKind]validation.SchemaCreateValidator
structural map[schema.GroupVersionKind]*structuralschema.Structural
cel map[schema.GroupVersionKind]*cel.Validator
// If enabled, resources without a validator will be ignored. Otherwise, they will fail.
SkipMissing bool
}
// Split where the '---' appears at the very beginning of a line. This will avoid
// accidentally splitting in cases where yaml resources contain nested yaml (which
// is indented).
var splitRegex = regexp.MustCompile(`(^|\n)---`)
// SplitString splits the given yaml doc if it's multipart document.
func SplitString(yamlText string) []string {
out := make([]string, 0)
parts := splitRegex.Split(yamlText, -1)
for _, part := range parts {
part := strings.TrimSpace(part)
if len(part) > 0 {
out = append(out, part)
}
}
return out
}
func (v *Validator) ValidateCustomResource(o runtime.Object) error {
content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o)
if err != nil {
return err
}
un := &unstructured.Unstructured{Object: content}
vd, f := v.byGvk[un.GroupVersionKind()]
if !f {
if v.SkipMissing {
return nil
}
return fmt.Errorf("failed to validate type %v: no validator found", un.GroupVersionKind())
}
// Fill in defaults
structural := v.structural[un.GroupVersionKind()]
structuraldefaulting.Default(un.Object, structural)
if err := validation.ValidateCustomResource(nil, un.Object, vd).ToAggregate(); err != nil {
return fmt.Errorf("%v/%v/%v: %v", un.GroupVersionKind().Kind, un.GetName(), un.GetNamespace(), err)
}
if err := structurallisttype.ValidateListSetsAndMaps(nil, structural, un.Object).ToAggregate(); err != nil {
return fmt.Errorf("%v/%v/%v: %v", un.GroupVersionKind().Kind, un.GetName(), un.GetNamespace(), err)
}
pruneOpts := structuralschema.UnknownFieldPathOptions{TrackUnknownFieldPaths: true}
if unknownFieldPaths := structuralpruning.PruneWithOptions(un.DeepCopy().Object, structural, false, pruneOpts); len(unknownFieldPaths) > 0 {
return fmt.Errorf("%v/%v/%v: unknown fields %v", un.GroupVersionKind().Kind, un.GetName(), un.GetNamespace(), unknownFieldPaths)
}
errs, _ := v.cel[un.GroupVersionKind()].Validate(context.Background(), nil, structural, un.Object, nil, celconfig.RuntimeCELCostBudget)
if errs.ToAggregate() != nil {
return fmt.Errorf("%v/%v/%v: %v", un.GroupVersionKind().Kind, un.GetName(), un.GetNamespace(), errs.ToAggregate().Error())
}
return nil
}
func NewValidatorFromFiles(files ...string) (*Validator, error) {
crds := []apiextensions.CustomResourceDefinition{}
closers := make([]io.Closer, 0, len(files))
defer func() {
for _, closer := range closers {
closer.Close()
}
}()
for _, file := range files {
data, err := os.Open(file)
if err != nil {
return nil, fmt.Errorf("failed to read input yaml file: %v", err)
}
closers = append(closers, data)
yamlDecoder := kubeyaml.NewYAMLOrJSONDecoder(data, 512*1024)
for {
un := &unstructured.Unstructured{}
err = yamlDecoder.Decode(&un)
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
crd := apiextensions.CustomResourceDefinition{}
switch un.GroupVersionKind() {
case schema.GroupVersionKind{
Group: "apiextensions.k8s.io",
Version: "v1",
Kind: "CustomResourceDefinition",
}:
crdv1 := apiextensionsv1.CustomResourceDefinition{}
if err := runtime.DefaultUnstructuredConverter.
FromUnstructured(un.UnstructuredContent(), &crdv1); err != nil {
return nil, err
}
if err := apiextensionsv1.Convert_v1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(&crdv1, &crd, nil); err != nil {
return nil, err
}
case schema.GroupVersionKind{
Group: "apiextensions.k8s.io",
Version: "v1beta1",
Kind: "CustomResourceDefinition",
}:
crdv1beta1 := apiextensionsv1beta1.CustomResourceDefinition{}
if err := runtime.DefaultUnstructuredConverter.
FromUnstructured(un.UnstructuredContent(), &crdv1beta1); err != nil {
return nil, err
}
if err := apiextensionsv1beta1.Convert_v1beta1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(&crdv1beta1, &crd, nil); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown CRD type: %v", un.GroupVersionKind())
}
crds = append(crds, crd)
}
}
return NewValidatorFromCRDs(crds...)
}
func NewValidatorFromCRDs(crds ...apiextensions.CustomResourceDefinition) (*Validator, error) {
v := &Validator{
byGvk: map[schema.GroupVersionKind]validation.SchemaCreateValidator{},
structural: map[schema.GroupVersionKind]*structuralschema.Structural{},
cel: map[schema.GroupVersionKind]*cel.Validator{},
}
for _, crd := range crds {
versions := crd.Spec.Versions
if len(versions) == 0 {
versions = []apiextensions.CustomResourceDefinitionVersion{{Name: crd.Spec.Version}} // nolint: staticcheck
}
for _, v := range versions {
crd.Status.StoredVersions = append(crd.Status.StoredVersions, v.Name)
}
errs := apiextval.ValidateCustomResourceDefinition(context.Background(), &crd)
if len(errs) > 0 {
return nil, fmt.Errorf("CRD %v is not valid: %v", crd.Name, formatError(errs))
}
for _, ver := range versions {
gvk := schema.GroupVersionKind{
Group: crd.Spec.Group,
Version: ver.Name,
Kind: crd.Spec.Names.Kind,
}
crdSchema := ver.Schema
if crdSchema == nil {
crdSchema = crd.Spec.Validation
}
if crdSchema == nil {
return nil, fmt.Errorf("crd did not have validation defined")
}
schemaValidator, _, err := validation.NewSchemaValidator(crdSchema.OpenAPIV3Schema)
if err != nil {
return nil, err
}
structural, err := structuralschema.NewStructural(crdSchema.OpenAPIV3Schema)
if err != nil {
return nil, err
}
v.byGvk[gvk] = schemaValidator
v.structural[gvk] = structural
// CEL programs are compiled and cached here
if celv := cel.NewValidator(structural, true, celconfig.PerCallLimit); celv != nil {
v.cel[gvk] = celv
}
}
}
return v, nil
}
func formatError(errs field.ErrorList) error {
if len(errs) == 0 {
return nil
}
s := strings.Builder{}
s.WriteString("\n")
for _, e := range errs.ToAggregate().Errors() {
s.WriteString(fmt.Sprintf("- %v\n", strings.TrimPrefix(e.Error(), "spec.validation.openAPIV3Schema")))
}
return errors.New(s.String())
}
func NewIstioValidator(t *testing.T) *Validator {
v, err := NewValidatorFromFiles(filepath.Join("../kubernetes/customresourcedefinitions.gen.yaml"))
if err != nil {
t.Fatal(err)
}
return v
}