Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the --enable-upjet-extensions command-line option to process CRDs generated by upjet #160

Merged
merged 1 commit into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 25 additions & 13 deletions cmd/uptest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"+
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down
101 changes: 92 additions & 9 deletions internal/crdschema/crd.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package crdschema
import (
"os"
"path/filepath"
"regexp"
"strings"

"github.com/getkin/kin-openapi/openapi3"
Expand All @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down