mirror of https://github.com/istio/api.git
244 lines
8.3 KiB
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
|
|
}
|