diff --git a/cmd/uptest/main.go b/cmd/uptest/main.go index c6b40a6..bc6c578 100644 --- a/cmd/uptest/main.go +++ b/cmd/uptest/main.go @@ -39,17 +39,6 @@ var ( cmdSelf = cmdCRDDiff.Command("self", "Use OpenAPI v3 schemas from a single CRD") ) -func main() { - switch kingpin.MustParse(app.Parse(os.Args[1:])) { - case e2e.FullCommand(): - e2eTests() - case cmdRevision.FullCommand(): - crdDiffRevision() - case cmdSelf.FullCommand(): - crdDiffSelf() - } -} - var ( manifestList = e2e.Arg("manifest-list", "List of manifests. Value of this option will be used to trigger/configure the tests."+ "The possible usage:\n"+ @@ -68,6 +57,29 @@ var ( testDir = e2e.Flag("test-directory", "Directory where kuttl test case will be generated and executed.").Envar("UPTEST_TEST_DIR").Default(filepath.Join(os.TempDir(), "uptest-e2e")).String() ) +var ( + revisionDiffOptions = getCRDdiffCommonOptions(cmdRevision) + selfDiffOptions = getCRDdiffCommonOptions(cmdSelf) +) + +func getCRDdiffCommonOptions(cmd *kingpin.CmdClause) *crdschema.CommonOptions { + opts := &crdschema.CommonOptions{} + cmd.Flag("enable-upjet-extensions", "Enables diff extensions for the CRDs generated by upjet. "+ + "An example extension is the processing of the x-kubernetes-validations CEL rules generated by upjet.").Default("false").BoolVar(&opts.EnableUpjetExtensions) + return opts +} + +func main() { + switch kingpin.MustParse(app.Parse(os.Args[1:])) { + case e2e.FullCommand(): + e2eTests() + case cmdRevision.FullCommand(): + crdDiffRevision() + case cmdSelf.FullCommand(): + crdDiffSelf() + } +} + func e2eTests() { cd, err := os.Getwd() if err != nil { @@ -120,7 +132,7 @@ var ( ) func crdDiffRevision() { - crdDiff, err := crdschema.NewRevisionDiff(*baseCRDPath, *revisionCRDPath) + crdDiff, err := crdschema.NewRevisionDiff(*baseCRDPath, *revisionCRDPath, crdschema.WithRevisionDiffCommonOptions(revisionDiffOptions)) kingpin.FatalIfError(err, "Failed to load CRDs") reportDiff(crdDiff) } @@ -130,7 +142,7 @@ var ( ) func crdDiffSelf() { - crdDiff, err := crdschema.NewSelfDiff(*crdPath) + crdDiff, err := crdschema.NewSelfDiff(*crdPath, crdschema.WithSelfDiffCommonOptions(selfDiffOptions)) kingpin.FatalIfError(err, "Failed to load CRDs") reportDiff(crdDiff) } diff --git a/internal/crdschema/crd.go b/internal/crdschema/crd.go index 56db742..1d0ef26 100644 --- a/internal/crdschema/crd.go +++ b/internal/crdschema/crd.go @@ -20,6 +20,7 @@ package crdschema import ( "os" "path/filepath" + "regexp" "strings" "github.com/getkin/kin-openapi/openapi3" @@ -40,6 +41,17 @@ const ( errBreakingSelfVersionsCompute = "failed to compute breaking changes in the versions of a CRD" ) +var regexXValidationMessage = regexp.MustCompile(`spec\.forProvider\.(.+) is a required parameter`) + +// CommonOptions declares the common configuration options that +// customize how the diff between two OpenAPIv3 schemas are +// calculated. +type CommonOptions struct { + // EnableUpjetExtensions enables special handling for the CRDs + // generated by upjet. + EnableUpjetExtensions bool +} + // SchemaCheck represents a schema checker that can return the set of breaking // API changes between schemas. type SchemaCheck interface { @@ -49,21 +61,38 @@ type SchemaCheck interface { // RevisionDiff can compute schema changes between the base CRD found at `basePath` // and the revision CRD found at `revisionPath`. type RevisionDiff struct { - baseCRD *v1.CustomResourceDefinition - revisionCRD *v1.CustomResourceDefinition + baseCRD *v1.CustomResourceDefinition + revisionCRD *v1.CustomResourceDefinition + commonOptions CommonOptions +} + +// RevisionDiffOption is a functional option to configure the behavior of +// a RevisionDiff. +type RevisionDiffOption func(*RevisionDiff) + +// WithRevisionDiffCommonOptions configures the common diff options for a +// RevisionDiff. +func WithRevisionDiffCommonOptions(opts *CommonOptions) RevisionDiffOption { + return func(rd *RevisionDiff) { + rd.commonOptions = *opts + } } // NewRevisionDiff returns a new RevisionDiff initialized with // the base and revision CRDs loaded from the specified // base and revision CRD paths. -func NewRevisionDiff(basePath, revisionPath string) (*RevisionDiff, error) { +func NewRevisionDiff(basePath, revisionPath string, opts ...RevisionDiffOption) (*RevisionDiff, error) { d := &RevisionDiff{} + for _, o := range opts { + o(d) + } + var err error - d.baseCRD, err = loadCRD(basePath) + d.baseCRD, err = loadCRD(basePath, d.commonOptions.EnableUpjetExtensions) if err != nil { return nil, errors.Wrap(err, errCRDLoad) } - d.revisionCRD, err = loadCRD(revisionPath) + d.revisionCRD, err = loadCRD(revisionPath, d.commonOptions.EnableUpjetExtensions) if err != nil { return nil, errors.Wrap(err, errCRDLoad) } @@ -73,22 +102,39 @@ func NewRevisionDiff(basePath, revisionPath string) (*RevisionDiff, error) { // SelfDiff can compute schema changes between the consecutive versions // declared for a CRD. type SelfDiff struct { - crd *v1.CustomResourceDefinition + crd *v1.CustomResourceDefinition + commonOptions CommonOptions +} + +// SelfDiffOption is a functional option to configure the behavior of +// a SelfDiff. +type SelfDiffOption func(*SelfDiff) + +// WithSelfDiffCommonOptions configures the common diff options for a +// SelfDiff. +func WithSelfDiffCommonOptions(opts *CommonOptions) SelfDiffOption { + return func(sd *SelfDiff) { + sd.commonOptions = *opts + } } // NewSelfDiff returns a new SelfDiff initialized with a CRD loaded // from the specified path. -func NewSelfDiff(crdPath string) (*SelfDiff, error) { +func NewSelfDiff(crdPath string, opts ...SelfDiffOption) (*SelfDiff, error) { d := &SelfDiff{} + for _, o := range opts { + o(d) + } + var err error - d.crd, err = loadCRD(crdPath) + d.crd, err = loadCRD(crdPath, d.commonOptions.EnableUpjetExtensions) if err != nil { return nil, errors.Wrap(err, errCRDLoad) } return d, nil } -func loadCRD(m string) (*v1.CustomResourceDefinition, error) { +func loadCRD(m string, enableUpjetExtensions bool) (*v1.CustomResourceDefinition, error) { crd := &v1.CustomResourceDefinition{} buff, err := os.ReadFile(filepath.Clean(m)) if err != nil { @@ -97,9 +143,46 @@ func loadCRD(m string) (*v1.CustomResourceDefinition, error) { if err := apiyaml.Unmarshal(buff, crd); err != nil { return nil, errors.Wrapf(err, "failed to unmarshal CRD manifest from file: %s", m) } + + if enableUpjetExtensions { + if err := injectUpjetXKubernetesValidationRules(crd); err != nil { + return nil, errors.Wrapf(err, "failed to inject upjet's x-kubernetes-validations imposed required rules") + } + } return crd, nil } +func injectUpjetXKubernetesValidationRules(crd *v1.CustomResourceDefinition) error { + for vIndex, v := range crd.Spec.Versions { + spec, ok := v.Schema.OpenAPIV3Schema.Properties["spec"] + if !ok { + return errors.New("no 'spec' field in upjet generated CRD") + } + forProvider, ok := spec.Properties["forProvider"] + if !ok { + return errors.New("no spec.forProvider field in upjet generated CRD") + } + + for _, r := range spec.XValidations { + matches := regexXValidationMessage.FindStringSubmatch(r.Message) + if len(matches) <= 1 { + return errors.Errorf("unexpected rule message %q in upjet generated CRD", r.Message) + } + fName := matches[1] + _, ok := forProvider.Properties[fName] + if !ok { + return errors.Errorf("x-kubernetes-validations rule imposed field %q not found under spec.forProvider", fName) + } + forProvider.Required = append(forProvider.Required, fName) + } + + spec.Properties["forProvider"] = forProvider + v.Schema.OpenAPIV3Schema.Properties["spec"] = spec + crd.Spec.Versions[vIndex] = v + } + return nil +} + func getOpenAPIv3Document(crd *v1.CustomResourceDefinition) ([]*openapi3.T, error) { schemas := make([]*openapi3.T, 0, len(crd.Spec.Versions)) for _, v := range crd.Spec.Versions {