scripts/add-validation-to-crds/parse-crds.go (271 lines of code) (raw):

// Copyright 2024 Google LLC // // 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. // This program parses CRDs found in a given YAML file and outputs them onto // individual CRD files. package main import ( "bytes" "context" "flag" "fmt" "io/fs" "os" "path/filepath" "strings" kccyaml "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/yaml" apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/klog/v2" "sigs.k8s.io/yaml" ) func main() { err := run(context.Background()) if err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } } func run(ctx context.Context) error { var dir string flag.StringVar(&dir, "dir", "", "Directory to process") flag.Parse() if dir == "" { return fmt.Errorf("--dir is required") } if err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } if filepath.Ext(p) != ".yaml" { return nil } if err := processFile(ctx, p); err != nil { return fmt.Errorf("processing file %q: %w", p, err) } return nil }); err != nil { return err } return nil } func processFile(ctx context.Context, p string) error { b, err := os.ReadFile(p) if err != nil { return fmt.Errorf("error reading file %q: %w", p, err) } var out bytes.Buffer // We preserve the yaml header (copyright, typically) for _, line := range bytes.Split(b, []byte("\n")) { if len(line) == 0 || line[0] == '#' { out.Write(line) out.WriteString("\n") } else { break } } yamls, err := kccyaml.SplitYAML(b) if err != nil { return fmt.Errorf("error splitting bytes into YAMLs: %w", err) } crds := make([]*apiextensions.CustomResourceDefinition, 0) for _, y := range yamls { var crd apiextensions.CustomResourceDefinition if err := yaml.Unmarshal(y, &crd); err != nil { return fmt.Errorf("error unmarshalling bytes into CRD: %w", err) } crds = append(crds, &crd) } for _, crd := range crds { if err := addRefsToCRD(crd); err != nil { return err } } for i, crd := range crds { b, err := yaml.Marshal(crd) if err != nil { return fmt.Errorf("error marshalling CRD into bytes: %w", err) } out.Write(b) if i != 0 { out.WriteString("\n---\n") } } if err := os.WriteFile(p, out.Bytes(), 0644); err != nil { return fmt.Errorf("error writing file %q: %w", p, err) } return nil } func addRefsToCRD(crd *apiextensions.CustomResourceDefinition) error { if crd.Spec.Names.Kind == "IAMAuditConfig" { // no refs here return nil } for _, v := range crd.Spec.Versions { if err := addRefsToProps("", v.Schema.OpenAPIV3Schema); err != nil { return err } } return nil } func addRefsToProps(fieldPath string, props *apiextensions.JSONSchemaProps) error { // Descend into arrays if props.Items != nil { if props.Items.Schema != nil { if err := addRefsToProps(fieldPath+"[]", props.Items.Schema); err != nil { return err } } for i := range props.Items.JSONSchemas { if err := addRefsToProps(fieldPath+"[]", &props.Items.JSONSchemas[i]); err != nil { return err } } } // Descend into objects for k := range props.Properties { v := props.Properties[k] if err := addRefsToProps(fieldPath+"."+k, &v); err != nil { return err } props.Properties[k] = v } if err := addValidationToRefs(fieldPath, props); err != nil { return err } return nil } const refRuleWithoutKind = ` oneOf: - not: required: - external required: - name - not: anyOf: - required: - name - required: - namespace required: - external ` const refRuleWithKind = ` oneOf: - not: required: - external required: - name - kind - not: anyOf: - required: - name - required: - namespace - required: - kind required: - external ` const refRuleWithOptionalKind = ` oneOf: - not: required: - external required: - name - not: anyOf: - required: - name - required: - namespace required: - external ` const resourceRefRuleWithOnlyKind = ` oneOf: - not: required: - external required: - name - not: anyOf: - required: - name - required: - namespace required: - external - not: anyOf: - required: - name - required: - namespace - required: - apiVersion - required: - external ` func addValidationToRefs(fieldPath string, props *apiextensions.JSONSchemaProps) error { // Is this a ref? if props.Type != "object" { return nil } var ruleYAML string if fieldPath == ".spec.bindings[].members[]" { ruleYAML = ` oneOf: - required: - member - required: - memberFrom ` } else if fieldPath == ".spec.bindings[].members[].memberFrom" { ruleYAML = ` oneOf: - required: - bigQueryConnectionConnectionRef - required: - logSinkRef - required: - serviceAccountRef - required: - serviceIdentityRef - required: - sqlInstanceRef ` } else { fields := sets.New[string]() for k := range props.Properties { fields.Insert(k) } signature := strings.Join(sets.List(fields), ",") if signature == "apiVersion,external,kind,name,namespace" { // hack for IAMPolicy.spec.resourceRef for backwards compat if fieldPath == ".spec.resourceRef" { ruleYAML = resourceRefRuleWithOnlyKind } else { ruleYAML = refRuleWithKind } } else if signature == "external,kind,name,namespace" { ruleYAML = refRuleWithKind // kind is optional for projectRef (and maybe in future other well-known ref types) // fieldPath is the best mechanism we have today (?) if fieldPath == ".spec.projectRef" { ruleYAML = refRuleWithOptionalKind } } else if signature == "external,name,namespace" { ruleYAML = refRuleWithoutKind } else { if strings.HasPrefix(signature, "external,") { klog.Warningf("unknown signature %q", signature) } if strings.HasPrefix(signature, "apiVersion,external,") { klog.Warningf("unknown signature %q", signature) } return nil } } rule := &apiextensions.JSONSchemaProps{} if err := yaml.Unmarshal([]byte(ruleYAML), &rule); err != nil { return err } props.OneOf = rule.OneOf return nil }