diff --git a/api/multicluster/v1alpha1/group.go b/api/multicluster/v1alpha1/group.go index 9291ebf47..a63cf9fa7 100644 --- a/api/multicluster/v1alpha1/group.go +++ b/api/multicluster/v1alpha1/group.go @@ -26,14 +26,16 @@ var Group = model.Group{ Name: "KubernetesClusterStatus", }, }, + Stored: true, }, }, - RenderManifests: true, - RenderValidationSchemas: true, - RenderController: true, - RenderClients: true, - RenderTypes: true, - MockgenDirective: true, - ApiRoot: "pkg/api", - CustomTemplates: contrib.AllGroupCustomTemplates, + RenderManifests: true, + RenderValidationSchemas: true, + RenderController: true, + RenderClients: true, + RenderTypes: true, + MockgenDirective: true, + ApiRoot: "pkg/api", + CustomTemplates: contrib.AllGroupCustomTemplates, + SkipConditionalCRDLoading: true, } diff --git a/changelog/v0.33.0/conditional_crd_rendering.yaml b/changelog/v0.33.0/conditional_crd_rendering.yaml new file mode 100644 index 000000000..0ad14deed --- /dev/null +++ b/changelog/v0.33.0/conditional_crd_rendering.yaml @@ -0,0 +1,5 @@ +changelog: + - type: BREAKING_CHANGE + issueLink: https://github.com/solo-io/gloo-mesh-enterprise/issues/9019 + description: > + Add support for conditional rendering of CRDs. diff --git a/codegen/cmd.go b/codegen/cmd.go index e9251af4a..41c90cd65 100644 --- a/codegen/cmd.go +++ b/codegen/cmd.go @@ -106,6 +106,10 @@ type Command struct { // context of the command ctx context.Context + + // the name of the flag to pass the list of enabled alpha-level crds + // used in codegen/templates/manifests/crd.yamltmpl + EnabledAlphaApiFlagName string } // function to execute skv2 code gen from another repository @@ -144,17 +148,16 @@ func (c Command) Execute() error { var groups []*model.Group for _, group := range c.Groups { group := group // pike + c.initGroup(&group, descriptors) groups = append(groups, &group) } - for i, group := range groups { - group := group // pike - c.initGroup(group, descriptors) - if err := c.generateGroup(*group, protoOpts, c.GroupOptions); err != nil { - return err - } + if err := c.generateGroups(groups, protoOpts, c.GroupOptions); err != nil { + return err + } - // replace group in Groups array with the group including generated fields + // replace group in Groups array with the group including generated fields + for i, group := range groups { c.Groups[i] = *group } @@ -239,8 +242,8 @@ func (c Command) renderProtos() ([]*collector.DescriptorWithPath, error) { return descriptors, nil } -func (c Command) generateGroup( - grp model.Group, +func (c Command) generateGroups( + grps []*model.Group, protoOpts proto.Options, groupOptions model.GroupOptions, ) error { @@ -250,25 +253,27 @@ func (c Command) generateGroup( Header: c.GeneratedHeader, } - protoTypes, err := render.RenderProtoTypes(grp) - if err != nil { - return err - } + for _, grp := range grps { + protoTypes, err := render.RenderProtoTypes(*grp) + if err != nil { + return err + } - if err := fileWriter.WriteFiles(protoTypes); err != nil { - return err - } + if err := fileWriter.WriteFiles(protoTypes); err != nil { + return err + } - apiTypes, err := render.RenderApiTypes(grp) - if err != nil { - return err - } + apiTypes, err := render.RenderApiTypes(*grp) + if err != nil { + return err + } - if err := fileWriter.WriteFiles(apiTypes); err != nil { - return err + if err := fileWriter.WriteFiles(apiTypes); err != nil { + return err + } } - manifests, err := render.RenderManifests(c.AppName, c.ManifestRoot, c.ProtoDir, protoOpts, groupOptions, grp) + manifests, err := render.RenderManifests(c.AppName, c.ManifestRoot, c.ProtoDir, c.EnabledAlphaApiFlagName, protoOpts, groupOptions, grps) if err != nil { return err } @@ -277,8 +282,10 @@ func (c Command) generateGroup( return err } - if err := render.KubeCodegen(grp); err != nil { - return err + for _, grp := range grps { + if err := render.KubeCodegen(*grp); err != nil { + return err + } } return nil @@ -335,6 +342,13 @@ func (c Command) initGroup( // default to the resource API Group name matchingProtoPackage = resource.Group.Group } + // TODO (dmitri-d): This assumes we only ever have a single message with a given name, and breaks when we + // have multiple versions of the same proto message. + // `go_package`` is not a reliable way to determine the package version, as it is not always set. + // protobuf path is not reliable either, as it is not always contains the version (see test/test_api.proto). + // A common approach is to include the version in the proto package name, perhaps we could adopt that? + // For example, workspace.proto package now is "admin.gloo.solo.io", it would change to + // "admin.gloo.solo.io.v2" with such an approach. if fileDescriptor.GetPackage() == matchingProtoPackage { if message := fileDescriptor.GetMessage(fieldType.Name); message != nil { fieldType.Message = message diff --git a/codegen/cmd_test.go b/codegen/cmd_test.go index 6eed47bc3..52d19caee 100644 --- a/codegen/cmd_test.go +++ b/codegen/cmd_test.go @@ -8,6 +8,7 @@ import ( "os/exec" "path/filepath" "reflect" + "strings" goyaml "gopkg.in/yaml.v3" rbacv1 "k8s.io/api/rbac/v1" @@ -54,11 +55,13 @@ var _ = Describe("Cmd", func() { Kind: "Paint", Spec: Field{Type: Type{Name: "PaintSpec"}}, Status: &Field{Type: Type{Name: "PaintStatus"}}, + Stored: true, }, { Kind: "ClusterResource", Spec: Field{Type: Type{Name: "ClusterResourceSpec"}}, ClusterScoped: true, + Stored: true, }, }, RenderManifests: true, @@ -133,6 +136,7 @@ var _ = Describe("Cmd", func() { Name: "KubernetesCluster", GoPackage: "github.com/solo-io/skv2/pkg/api/multicluster.solo.io/v1alpha1", }}, + Stored: true, }, }, RenderManifests: true, @@ -171,11 +175,13 @@ var _ = Describe("Cmd", func() { Kind: "Paint", Spec: Field{Type: Type{Name: "PaintSpec"}}, Status: &Field{Type: Type{Name: "PaintStatus"}}, + Stored: true, }, { Kind: "ClusterResource", Spec: Field{Type: Type{Name: "ClusterResourceSpec"}}, ClusterScoped: true, + Stored: true, }, }, RenderManifests: true, @@ -256,11 +262,13 @@ var _ = Describe("Cmd", func() { Kind: "Paint", Spec: Field{Type: Type{Name: "PaintSpec"}}, Status: &Field{Type: Type{Name: "PaintStatus"}}, + Stored: true, }, { Kind: "ClusterResource", Spec: Field{Type: Type{Name: "ClusterResourceSpec"}}, ClusterScoped: true, + Stored: true, }, }, RenderManifests: true, @@ -375,11 +383,13 @@ var _ = Describe("Cmd", func() { Kind: "Paint", Spec: Field{Type: Type{Name: "PaintSpec"}}, Status: &Field{Type: Type{Name: "PaintStatus"}}, + Stored: true, }, { Kind: "ClusterResource", Spec: Field{Type: Type{Name: "ClusterResourceSpec"}}, ClusterScoped: true, + Stored: true, }, }, RenderManifests: true, @@ -1195,11 +1205,13 @@ var _ = Describe("Cmd", func() { Kind: "Paint", Spec: Field{Type: Type{Name: "PaintSpec"}}, Status: &Field{Type: Type{Name: "PaintStatus"}}, + Stored: true, }, { Kind: "ClusterResource", Spec: Field{Type: Type{Name: "ClusterResourceSpec"}}, ClusterScoped: true, + Stored: true, }, }, RenderManifests: true, @@ -1315,7 +1327,7 @@ var _ = Describe("Cmd", func() { }) It("can include field descriptions", func() { - crdFilePath := filepath.Join(util.GetModuleRoot(), cmd.ManifestRoot, "/crds/things.test.io_v1_crds.yaml") + crdFilePath := filepath.Join(util.GetModuleRoot(), cmd.ManifestRoot, "/crds/things.test.io_crds.yaml") err := cmd.Execute() Expect(err).NotTo(HaveOccurred()) @@ -1325,16 +1337,25 @@ var _ = Describe("Cmd", func() { Expect(string(bytes)).To(ContainSubstring("description: OpenAPI gen test for recursive fields")) }) + // TODO (dmitri-d): kube_crud_test and kube_multicluster_test depend on crds in this suite. It("generates google.protobuf.Value with no type", func() { - crdFilePath := filepath.Join(util.GetModuleRoot(), cmd.ManifestRoot, "/crds/things.test.io_v1_crds.yaml") + crdFilePath := filepath.Join(util.GetModuleRoot(), cmd.ManifestRoot, "/crds/things.test.io_crds.yaml") err := cmd.Execute() Expect(err).NotTo(HaveOccurred()) bytes, err := ioutil.ReadFile(crdFilePath) Expect(err).NotTo(HaveOccurred()) + paintCrdYaml := "" + for _, crd := range strings.Split(string(bytes), "---") { + if strings.Contains(crd, "kind: Paint") { + paintCrdYaml = crd + } + } + Expect(paintCrdYaml).ToNot(BeEmpty()) + generatedCrd := &v12.CustomResourceDefinition{} - Expect(yaml.Unmarshal(bytes, generatedCrd)).NotTo(HaveOccurred()) + Expect(yaml.Unmarshal([]byte(paintCrdYaml), &generatedCrd)).NotTo(HaveOccurred()) protobufValueField := generatedCrd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["spec"].Properties["recursiveType"].Properties["protobufValue"] // access the field to make sure it's not nil Expect(protobufValueField.XPreserveUnknownFields).ToNot(BeNil()) @@ -1346,7 +1367,7 @@ var _ = Describe("Cmd", func() { // write this manifest to a different dir to avoid modifying the crd file from the // above test, which other tests seem to depend on cmd.ManifestRoot = "codegen/test/chart-no-desc" - crdFilePath := filepath.Join(util.GetModuleRoot(), cmd.ManifestRoot, "/crds/things.test.io_v1_crds.yaml") + crdFilePath := filepath.Join(util.GetModuleRoot(), cmd.ManifestRoot, "/crds/things.test.io_crds.yaml") cmd.Groups[0].SkipSchemaDescriptions = true @@ -1514,6 +1535,7 @@ func helmTemplate(path string, values interface{}) []byte { path, "--values", helmValuesFile.Name(), ).CombinedOutput() + ExpectWithOffset(1, err).NotTo(HaveOccurred(), string(out)) return out } diff --git a/codegen/kuberesource/crd.go b/codegen/kuberesource/crd.go index 02148c62d..ef655204e 100644 --- a/codegen/kuberesource/crd.go +++ b/codegen/kuberesource/crd.go @@ -2,11 +2,14 @@ package kuberesource import ( "fmt" + "sort" "strings" + "github.com/google/go-cmp/cmp" "github.com/mitchellh/hashstructure" "github.com/solo-io/skv2/codegen/util/stringutils" "github.com/solo-io/skv2/pkg/crdutils" + "golang.org/x/exp/maps" "github.com/rotisserie/eris" "github.com/solo-io/skv2/codegen/model" @@ -18,23 +21,54 @@ import ( // Create CRDs for a group func CustomResourceDefinitions( - group model.Group, -) (objects []metav1.Object, err error) { - for _, resource := range group.Resources { + groups []*model.Group, +) (objects []apiextv1.CustomResourceDefinition, err error) { + resourcesByKind := make(map[string][]model.Resource) + skipHashByKind := make(map[string]bool) + for _, group := range groups { + for i, resource := range group.Resources { + resourcesByKind[resource.Kind] = append(resourcesByKind[resource.Kind], group.Resources[i]) + skipHashByKind[resource.Kind] = skipHashByKind[resource.Kind] || resource.Group.SkipSpecHash + } + } + + // Make ordering of crds in a group deterministic + kinds := maps.Keys(resourcesByKind) + sort.Strings(kinds) + for _, kind := range kinds { + resources := resourcesByKind[kind] + // make version ordering deterministic + sort.Slice(resources, func(i, j int) bool { return resources[i].Version < resources[j].Version }) + validationSchemas, err := constructValidationSchemas(resources) + if err != nil { + return nil, err + } + + crd, err := CustomResourceDefinition(resources, validationSchemas, skipHashByKind[kind]) + if err != nil { + return nil, err + } + objects = append(objects, *crd) + } + return objects, nil +} +func constructValidationSchemas(resources []model.Resource) (map[string]*apiextv1.CustomResourceValidation, error) { + validationSchemas := make(map[string]*apiextv1.CustomResourceValidation) + for _, resource := range resources { var validationSchema *apiextv1.CustomResourceValidation - validationSchema, err = constructValidationSchema( - group.RenderValidationSchemas, + validationSchema, err := constructValidationSchema( + resource.Group.RenderValidationSchemas, resource, - group.OpenApiSchemas, + resource.Group.OpenApiSchemas, ) if err != nil { return nil, err } - objects = append(objects, CustomResourceDefinition(resource, validationSchema, group.SkipSpecHash)) + validationSchemas[resource.Group.String()] = validationSchema } - return objects, nil + return validationSchemas, nil } func constructValidationSchema( @@ -116,28 +150,71 @@ func validateStructural(s *apiextv1.JSONSchemaProps) error { return nil } +func validateCRDResources(resources []model.Resource) error { + if len(resources) < 2 { + return nil + } + + scope := resources[0].ClusterScoped + shortNames := resources[0].ShortNames + categories := resources[0].Categories + + for i := 1; i < len(resources); i++ { + if resources[i].ClusterScoped != scope { + return fmt.Errorf("mismatched 'currentScope' in versions of CRD for resource kind %s", resources[i].Kind) + } + if !cmp.Equal(resources[i].ShortNames, shortNames) { + return fmt.Errorf("mismatched 'ShortNames' in versions of CRD for resource kind %s", resources[i].Kind) + } + if !cmp.Equal(resources[i].Categories, categories) { + return fmt.Errorf("mismatched 'Categories' in versions of CRD for resource kind %s", resources[i].Kind) + } + } + + return nil +} + func CustomResourceDefinition( - resource model.Resource, - validationSchema *apiextv1.CustomResourceValidation, + resources []model.Resource, + validationSchemas map[string]*apiextv1.CustomResourceValidation, withoutSpecHash bool, -) *apiextv1.CustomResourceDefinition { +) (*apiextv1.CustomResourceDefinition, error) { + + err := validateCRDResources(resources) + if err != nil { + return nil, err + } - group := resource.Group.Group - version := resource.Group.Version - kind := resource.Kind + group := resources[0].Group.Group + kind := resources[0].Kind kindLowerPlural := strings.ToLower(stringutils.Pluralize(kind)) kindLower := strings.ToLower(kind) - var status *apiextv1.CustomResourceSubresourceStatus - if resource.Status != nil { - status = &apiextv1.CustomResourceSubresourceStatus{} - } - scope := apiextv1.NamespaceScoped - if resource.ClusterScoped { + if resources[0].ClusterScoped { scope = apiextv1.ClusterScoped } + versions := make([]apiextv1.CustomResourceDefinitionVersion, 0, len(resources)) + for _, resource := range resources { + var status *apiextv1.CustomResourceSubresourceStatus + if resource.Status != nil { + status = &apiextv1.CustomResourceSubresourceStatus{} + } + + v := apiextv1.CustomResourceDefinitionVersion{ + Name: resource.Group.Version, + Served: true, + Storage: resource.Stored, + Deprecated: resource.Deprecated, + AdditionalPrinterColumns: resource.AdditionalPrinterColumns, + Subresources: &apiextv1.CustomResourceSubresources{ + Status: status, + }, + Schema: validationSchemas[resource.Group.String()], + } + versions = append(versions, v) + } crd := &apiextv1.CustomResourceDefinition{ TypeMeta: metav1.TypeMeta{ APIVersion: apiextv1.SchemeGroupVersion.String(), @@ -147,27 +224,16 @@ func CustomResourceDefinition( Name: fmt.Sprintf("%s.%s", kindLowerPlural, group), }, Spec: apiextv1.CustomResourceDefinitionSpec{ - Group: group, - Scope: scope, - Versions: []apiextv1.CustomResourceDefinitionVersion{ - { - Name: version, - Served: true, - Storage: true, - AdditionalPrinterColumns: resource.AdditionalPrinterColumns, - Subresources: &apiextv1.CustomResourceSubresources{ - Status: status, - }, - Schema: validationSchema, - }, - }, + Group: group, + Scope: scope, + Versions: versions, Names: apiextv1.CustomResourceDefinitionNames{ Plural: kindLowerPlural, Singular: kindLower, Kind: kind, - ShortNames: resource.ShortNames, + ShortNames: resources[0].ShortNames, ListKind: kind + "List", - Categories: resource.Categories, + Categories: resources[0].Categories, }, }, } @@ -182,9 +248,9 @@ func CustomResourceDefinition( } } - if validationSchema != nil { + if len(validationSchemas) > 0 { // Setting PreserveUnknownFields to false ensures that objects with unknown fields are rejected. crd.Spec.PreserveUnknownFields = false } - return crd + return crd, nil } diff --git a/codegen/kuberesource/crd_test.go b/codegen/kuberesource/crd_test.go index efe9e4989..316d1ed52 100644 --- a/codegen/kuberesource/crd_test.go +++ b/codegen/kuberesource/crd_test.go @@ -1,8 +1,11 @@ package kuberesource_test import ( + "fmt" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" v1 "github.com/solo-io/skv2/codegen/test/api/things.test.io/v1" @@ -15,11 +18,11 @@ var _ = Describe("Crd", func() { Describe("CRD gen", func() { var ( - grp model.Group + grps []*model.Group ) BeforeEach(func() { - grp = model.Group{ + grps = []*model.Group{{ Resources: []model.Resource{ { Kind: "kind", @@ -29,31 +32,141 @@ var _ = Describe("Crd", func() { Message: &v1.AcrylicType{}, }, }, + Stored: true, + Deprecated: false, }, - }, + { + Kind: "kind-1", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.PaintColor{}, + }, + }, + Stored: false, + Deprecated: true, + }, + }}, + } + for i := range grps { + grps[i].Init() } - grp.Init() }) It("should generate spec hash", func() { - grp.SkipSpecHash = false - o, err := CustomResourceDefinitions(grp) + grps[0].SkipSpecHash = false + o, err := CustomResourceDefinitions(grps) Expect(err).NotTo(HaveOccurred()) - Expect(o).To(HaveLen(1)) + Expect(o).To(HaveLen(2)) // note: we intentionally provide the "b6ec737002f7d02e" hash in the test, as it shouldn't change // between runs. Expect(o[0].GetAnnotations()).To(HaveKeyWithValue(crdutils.CRDSpecHashKey, "b6ec737002f7d02e")) - }) It("should not generate spec hash", func() { - grp.SkipSpecHash = true - o, err := CustomResourceDefinitions(grp) + grps[0].SkipSpecHash = true + o, err := CustomResourceDefinitions(grps) Expect(err).NotTo(HaveOccurred()) - Expect(o).To(HaveLen(1)) + Expect(o).To(HaveLen(2)) // note: we intentionally provide the "d18828e563010e32" hash in the test, as it shouldn't change // between runs. Expect(o[0].GetAnnotations()).NotTo(HaveKey(crdutils.CRDSpecHashKey)) }) + It("should set 'Stored' and 'Deprecated' fields", func() { + grps[0].SkipSpecHash = false + o, err := CustomResourceDefinitions(grps) + Expect(err).NotTo(HaveOccurred()) + Expect(o).To(HaveLen(2)) + Expect(o[0].Spec.Versions).To(HaveLen(1)) + Expect(o[0].Spec.Versions[0].Storage).To(BeTrue()) + Expect(o[0].Spec.Versions[0].Deprecated).To(BeFalse()) + Expect(o[1].Spec.Versions).To(HaveLen(1)) + Expect(o[1].Spec.Versions[0].Storage).To(BeFalse()) + Expect(o[1].Spec.Versions[0].Deprecated).To(BeTrue()) + }) + }) + Describe("CRD gen with errors", func() { + It("should return an error when scopes are mismatched", func() { + resources := []model.Resource{ + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.AcrylicType{}, + }, + }, + ClusterScoped: true, + }, + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.PaintColor{}, + }, + }, + ClusterScoped: false, + }, + } + + _, err := CustomResourceDefinition(resources, map[string]*apiextv1.CustomResourceValidation{}, false) + Expect(err).To(Equal(fmt.Errorf("mismatched 'currentScope' in versions of CRD for resource kind kind"))) + }) + It("should return an error when ShortNames are mismatched", func() { + resources := []model.Resource{ + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.AcrylicType{}, + }, + }, + ShortNames: []string{"name1", "name2"}, + }, + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.PaintColor{}, + }, + }, + ShortNames: []string{"name2", "name3"}, + }, + } + + _, err := CustomResourceDefinition(resources, map[string]*apiextv1.CustomResourceValidation{}, false) + Expect(err).To(Equal(fmt.Errorf("mismatched 'ShortNames' in versions of CRD for resource kind kind"))) + }) + It("should return an error when Categories are mismatched", func() { + resources := []model.Resource{ + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.AcrylicType{}, + }, + }, + Categories: []string{"cat1", "cat2"}, + }, + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.PaintColor{}, + }, + }, + Categories: []string{"cat2", "cat3"}, + }, + } + + _, err := CustomResourceDefinition(resources, map[string]*apiextv1.CustomResourceValidation{}, false) + Expect(err).To(Equal(fmt.Errorf("mismatched 'Categories' in versions of CRD for resource kind kind"))) + }) + }) }) diff --git a/codegen/model/resource.go b/codegen/model/resource.go index f0c03e604..e508a6008 100644 --- a/codegen/model/resource.go +++ b/codegen/model/resource.go @@ -114,6 +114,17 @@ type Group struct { // Some resources use pointer slices for the Items field. PointerSlices bool `default:"false"` + + // Set to true to skip rendering of conditional loading logic + // for CRDs containing alpha-versioned resources. + // Used by codegen/templates/manifests/crd.yamltmpl + SkipConditionalCRDLoading bool + + // Skip generation of crd manifests that live in crd/ directory of a chart + SkipCRDManifest bool + + // Skip generation of templated crd manifests that live in templates/ dir of a chart + SkipTemplatedCRDManifest bool } type GroupOptions struct { @@ -172,6 +183,17 @@ type Resource struct { // If enabled, the unmarshal will NOT allow unknown fields. StrictUnmarshal bool + + // Corresponds to CRD's versions.storage field + // Only one version of a resource can be marked as "stored" + // Set to false by default + // See https://kubernetes.io/docs/reference/kubernetes-api/extend-resources/custom-resource-definition-v1/#CustomResourceDefinitionSpec + Stored bool + + // Corresponds to CRD's versions.deprecated field + // Set to false by default + // See https://kubernetes.io/docs/reference/kubernetes-api/extend-resources/custom-resource-definition-v1/#CustomResourceDefinitionSpec + Deprecated bool } type Field struct { diff --git a/codegen/render/funcs.go b/codegen/render/funcs.go index e875cc9e9..3a9135116 100644 --- a/codegen/render/funcs.go +++ b/codegen/render/funcs.go @@ -15,6 +15,7 @@ import ( "github.com/solo-io/skv2/codegen/util/stringutils" "google.golang.org/protobuf/types/known/structpb" v1 "k8s.io/api/core/v1" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -131,6 +132,14 @@ func makeTemplateFuncs(customFuncs template.FuncMap) template.FuncMap { "containerConfigs": containerConfigs, "opVar": opVar, + + "render_outer_conditional_crd_template": func(crd apiextv1.CustomResourceDefinition, currentVersion string, skips map[string]bool) bool { + return len(crd.Spec.Versions) < 2 && strings.Contains(currentVersion, "alpha") && !skips[crd.Spec.Group+"/"+currentVersion] + }, + + "render_inner_conditional_crd_template": func(crd apiextv1.CustomResourceDefinition, currentVersion string, skips map[string]bool) bool { + return len(crd.Spec.Versions) > 1 && strings.Contains(currentVersion, "alpha") && !skips[crd.Spec.Group+"/"+currentVersion] + }, } for k, v := range skv2Funcs { diff --git a/codegen/render/kube_crud_test.go b/codegen/render/kube_crud_test.go index d30e18cc7..5c9116cef 100644 --- a/codegen/render/kube_crud_test.go +++ b/codegen/render/kube_crud_test.go @@ -31,12 +31,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" ) +// TODO (dmitri-d): Remove dependency of these tests on crds generated in cmd_test func applyFile(file string, extraArgs ...string) error { path := filepath.Join(util.GetModuleRoot(), "codegen/test/chart/crds", file) b, err := ioutil.ReadFile(path) if err != nil { return err } + return util.KubectlApply(b, extraArgs...) } @@ -117,7 +119,8 @@ var _ = Describe("Generated Code", func() { log.SetLogger(zaputil.New( zaputil.Level(&logLevel), )) - err := applyFile("things.test.io_v1_crds.yaml") + err := applyFile("things.test.io_crds.yaml") + Expect(err).NotTo(HaveOccurred()) ns = randutils.RandString(4) kube = kubehelp.MustKubeClient() diff --git a/codegen/render/kube_multicluster_test.go b/codegen/render/kube_multicluster_test.go index 67fbb4a6b..d8046b913 100644 --- a/codegen/render/kube_multicluster_test.go +++ b/codegen/render/kube_multicluster_test.go @@ -40,6 +40,7 @@ func WithRemoteClusterContextDescribe(text string, body func()) bool { return Describe(text, body) } +// TODO (dmitri-d): Remove dependency of these tests on crds generated in cmd_test var _ = WithRemoteClusterContextDescribe("Multicluster", func() { var ( ctx context.Context @@ -66,7 +67,7 @@ var _ = WithRemoteClusterContextDescribe("Multicluster", func() { Expect(err).NotTo(HaveOccurred()) for _, kubeContext := range []string{"", remoteContext} { - err = applyFile("things.test.io_v1_crds.yaml", "--context", kubeContext) + err = applyFile("things.test.io_crds.yaml", "--context", kubeContext) Expect(err).NotTo(HaveOccurred()) cfg := test.MustConfig(kubeContext) diff --git a/codegen/render/manifests_renderer.go b/codegen/render/manifests_renderer.go index 6b1e6a47c..3f05fff9a 100644 --- a/codegen/render/manifests_renderer.go +++ b/codegen/render/manifests_renderer.go @@ -2,6 +2,7 @@ package render import ( "encoding/json" + "fmt" "regexp" "strings" @@ -26,60 +27,92 @@ import ( // creates a k8s resource for a group // this gets turned into a k8s manifest file -type MakeResourceFunc func(group Group) ([]metav1.Object, error) +type MakeResourceFunc func(groups []*Group) ([]metav1.Object, error) // renders kubernetes from templates type ManifestsRenderer struct { + templateRenderer AppName string // used for labeling ResourceFuncs map[OutFile]MakeResourceFunc ManifestDir string ProtoDir string + // the name of the flag to pass the list of enabled alpha-level crds + // used in codegen/templates/manifests/crd.yamltmpl + EnabledAlphaApiFlagName string +} + +type templateArgs struct { + Crds []apiextv1.CustomResourceDefinition + ShouldSkip map[string]bool + EnabledAlphaApiFlagName string } func RenderManifests( - appName, manifestDir, protoDir string, + appName, manifestDir, protoDir, enabledAlphaApiFlagName string, protoOpts protoutil.Options, groupOptions model.GroupOptions, - grp Group, + grps []*Group, ) ([]OutFile, error) { defaultManifestsRenderer := ManifestsRenderer{ - AppName: appName, - ManifestDir: manifestDir, - ProtoDir: protoDir, - ResourceFuncs: map[OutFile]MakeResourceFunc{ - { - Path: manifestDir + "/crds/" + grp.Group + "_" + grp.Version + "_" + "crds.yaml", - }: func(group Group) ([]metav1.Object, error) { - return kuberesource.CustomResourceDefinitions(group) - }, - }, - } - return defaultManifestsRenderer.RenderManifests(grp, protoOpts, groupOptions) + AppName: appName, + ManifestDir: manifestDir, + ProtoDir: protoDir, + EnabledAlphaApiFlagName: enabledAlphaApiFlagName, + } + return defaultManifestsRenderer.RenderManifests(grps, protoOpts, groupOptions) } -func (r ManifestsRenderer) RenderManifests(grp Group, protoOpts protoutil.Options, groupOptions model.GroupOptions) ([]OutFile, error) { - if !grp.RenderManifests { - return nil, nil +func (r ManifestsRenderer) RenderManifests(grps []*Group, protoOpts protoutil.Options, groupOptions model.GroupOptions) ([]OutFile, error) { + grpsByGroupName := make(map[string][]*Group) + shouldRenderGroups := make(map[string]bool) + shouldSkipCRDManifest := make(map[string]bool) + shouldSkipTemplatedCRDManifest := make(map[string]bool) + grandfatheredGroups := make(map[string]bool) + for _, grp := range grps { + grpsByGroupName[grp.Group] = append(grpsByGroupName[grp.Group], grp) + shouldRenderGroups[grp.Group] = shouldRenderGroups[grp.Group] || grp.RenderManifests + grandfatheredGroups[grp.GroupVersion.String()] = grandfatheredGroups[grp.GroupVersion.String()] || grp.SkipConditionalCRDLoading + shouldSkipCRDManifest[grp.Group] = + shouldSkipCRDManifest[grp.Group] || grp.SkipCRDManifest + shouldSkipTemplatedCRDManifest[grp.Group] = + shouldSkipTemplatedCRDManifest[grp.Group] || grp.SkipTemplatedCRDManifest + } + + for _, grp := range grps { + if grp.RenderValidationSchemas && shouldRenderGroups[grp.Group] { + var err error + oapiSchemas, err := generateOpenApi(*grp, r.ProtoDir, protoOpts, groupOptions) + if err != nil { + return nil, err + } + grp.OpenApiSchemas = oapiSchemas + } } - if grp.RenderValidationSchemas { - var err error - oapiSchemas, err := generateOpenApi(grp, r.ProtoDir, protoOpts, groupOptions) + var renderedFiles []OutFile + + for groupName, selectedGrps := range grpsByGroupName { + if !shouldRenderGroups[groupName] { + continue + } + + crds, err := r.createCrds(r.AppName, selectedGrps) if err != nil { return nil, err } - grp.OpenApiSchemas = oapiSchemas - } + out, err := r.renderCRDManifest(r.AppName, groupName, crds) + if err != nil { + return nil, err + } + renderedFiles = append(renderedFiles, out) - var renderedFiles []OutFile - for out, mkFunc := range r.ResourceFuncs { - content, err := r.renderManifest(r.AppName, mkFunc, grp) + out, err = r.renderTemplatedCRDManifest(r.AppName, groupName, crds, grandfatheredGroups) if err != nil { return nil, err } - out.Content = content renderedFiles = append(renderedFiles, out) } + return renderedFiles, nil } @@ -218,41 +251,100 @@ func SetVersionForObject(obj metav1.Object, version string) { } a[crdutils.CRDVersionKey] = strippedVersion.String() + obj.SetAnnotations(a) } } -func (r ManifestsRenderer) renderManifest(appName string, mk MakeResourceFunc, group Group) (string, error) { - objs, err := mk(group) - if err != nil { - return "", err +// TODO (dmitri-d): this can be removed once we migrate to use platform charts exclusively +func (r ManifestsRenderer) renderCRDManifest(appName, groupName string, objs []apiextv1.CustomResourceDefinition) (OutFile, error) { + outFile := OutFile{ + Path: r.ManifestDir + "/crds/" + groupName + "_" + "crds.yaml", } var objManifests []string for _, obj := range objs { - // find the annotation of the manifest, and add to them - SetVersionForObject(obj, group.AddChartVersion) - manifest, err := marshalObjToYaml(appName, obj) + manifest, err := marshalObjToYaml(appName, &obj) if err != nil { - return "", err + return OutFile{}, err } objManifests = append(objManifests, manifest) } - return strings.Join(objManifests, "\n---\n"), nil + outFile.Content = strings.Join(objManifests, "\n---\n") + return outFile, nil } -func marshalObjToYaml(appName string, obj metav1.Object) (string, error) { - labels := obj.GetLabels() - if labels == nil { - labels = map[string]string{} +func (r ManifestsRenderer) renderTemplatedCRDManifest(appName, groupName string, + objs []apiextv1.CustomResourceDefinition, + grandfatheredGroups map[string]bool) (OutFile, error) { + + renderer := DefaultTemplateRenderer + + // when rendering helm charts, we need + // to use a custom delimiter + renderer.left = "[[" + renderer.right = "]]" + + defaultManifestRenderer := ChartRenderer{ + templateRenderer: renderer, + } + + outFile := OutFile{Path: r.ManifestDir + "/templates/" + groupName + "_" + "crds.yaml"} + templatesToRender := inputTemplates{ + "manifests/crd.yamltmpl": outFile, + } + + if err := r.canRenderCRDTemplate(objs, grandfatheredGroups); err != nil { + return OutFile{}, err + } + + files, err := defaultManifestRenderer.renderCoreTemplates( + templatesToRender, + templateArgs{Crds: objs, ShouldSkip: grandfatheredGroups, EnabledAlphaApiFlagName: r.EnabledAlphaApiFlagName}) + if err != nil { + return OutFile{}, err + } + // if we got here there's one item in []files, + // as we only rendered one template and there were no errors + return files[0], nil +} + +func (r ManifestsRenderer) canRenderCRDTemplate(objs []apiextv1.CustomResourceDefinition, grandfatheredGroups map[string]bool) error { + for _, obj := range objs { + for _, v := range obj.Spec.Versions { + if strings.Contains(v.Name, "alpha") && !grandfatheredGroups[obj.Spec.Group+"/"+v.Name] && r.EnabledAlphaApiFlagName == "" { + return fmt.Errorf("error rendering CRD template for kind %s: 'EnabledAlphaApiFlagName' is not defined", obj.Spec.Names.Kind) + } + } } + return nil +} - labels["app"] = appName - labels["app.kubernetes.io/name"] = appName +func (r ManifestsRenderer) createCrds(appName string, groups []*Group) ([]apiextv1.CustomResourceDefinition, error) { + objs, err := kuberesource.CustomResourceDefinitions(groups) + if err != nil { + return nil, err + } - obj.SetLabels(labels) + for i, obj := range objs { + // find the annotation of the manifest, and add to them + SetVersionForObject(objs[i].GetObjectMeta(), groups[0].AddChartVersion) + labels := obj.GetLabels() + if labels == nil { + labels = map[string]string{} + } + + labels["app"] = appName + labels["app.kubernetes.io/name"] = appName + + objs[i].SetLabels(labels) + } + return objs, nil +} + +func marshalObjToYaml(appName string, obj metav1.Object) (string, error) { yam, err := yaml.Marshal(obj) if err != nil { return "", err diff --git a/codegen/render/manifests_renderer_test.go b/codegen/render/manifests_renderer_test.go index 5d9ed79c3..ae9ef1d3b 100644 --- a/codegen/render/manifests_renderer_test.go +++ b/codegen/render/manifests_renderer_test.go @@ -1,10 +1,14 @@ package render_test import ( + "fmt" + "strings" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" v1 "github.com/solo-io/skv2/codegen/test/api/things.test.io/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "github.com/solo-io/skv2/codegen/model" "github.com/solo-io/skv2/codegen/render" @@ -51,13 +55,13 @@ var _ = Describe("ManifestsRenderer", func() { Expect(obj).To(Equal(expectedObj)) }) - Describe("CRD gen", func() { + Describe("Generate non-alpha versioned CRD", func() { var ( - grp model.Group + grps []*model.Group ) BeforeEach(func() { - grp = model.Group{ + grps = []*model.Group{{ RenderManifests: true, AddChartVersion: "1.0.0", Resources: []model.Resource{ @@ -69,25 +73,375 @@ var _ = Describe("ManifestsRenderer", func() { Message: &v1.AcrylicType{}, }, }, + Stored: true, }, - }, + }}, + } + for i := range grps { + grps[i].Init() } - grp.Init() }) It("Renderse manifests with chart and spec hash", func() { // get api-level code gen options from descriptors outFiles, err := render.RenderManifests( - "appName", "manifestDir", "protoDir", + "appName", "manifestDir", "protoDir", "enabledExperimentalApi", nil, model.GroupOptions{}, - grp, + grps, ) Expect(err).NotTo(HaveOccurred()) - Expect(outFiles).To(HaveLen(1)) + Expect(outFiles).To(HaveLen(2)) // legacy and templated manifests Expect(outFiles[0].Content).To(ContainSubstring(crdutils.CRDVersionKey + ": 1.0.0")) Expect(outFiles[0].Content).To(ContainSubstring(crdutils.CRDSpecHashKey + ": b6ec737002f7d02e")) + // only alpha versioned CRDs contain logic to conditionally render templates + Expect(outFiles[0].Content).To(Equal(outFiles[0].Content)) + }) + }) + + Describe("Generate alpha versioned CRD", func() { + var ( + grps []*model.Group + ) + + BeforeEach(func() { + grps = []*model.Group{{ + GroupVersion: schema.GroupVersion{ + Group: "things.test.io", + Version: "v1alpha1", + }, + RenderManifests: true, + AddChartVersion: "1.0.0", + Resources: []model.Resource{ + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.AcrylicType{}, + }, + }, + Stored: true, + }, + }}, + } + for i := range grps { + grps[i].Init() + } + }) + It("Renders manifests with template and spec hash", func() { + + // get api-level code gen options from descriptors + outFiles, err := render.RenderManifests( + "appName", "manifestDir", "protoDir", "enabledExperimentalApi", + nil, + model.GroupOptions{}, + grps, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(outFiles).To(HaveLen(2)) // legacy and templated manifests + // only alpha versioned CRDs contain logic to conditionally render templates + Expect(outFiles[1].Content).To(HavePrefix("\n{{- if has \"kinds.things.test.io/v1alpha1\" $.Values.enabledExperimentalApi }}")) + Expect(outFiles[1].Content).To(HaveSuffix("{{- end }}\n---\n")) + Expect(outFiles[1].Content).To(ContainSubstring(crdutils.CRDVersionKey + ": 1.0.0")) + Expect(outFiles[1].Content).To(ContainSubstring(crdutils.CRDSpecHashKey + ": 80c06d3e2484e4c8")) + }) + }) + + Describe("Skip template for grandfathered alpha versioned CRD", func() { + var ( + grps []*model.Group + ) + BeforeEach(func() { + grps = []*model.Group{{ + GroupVersion: schema.GroupVersion{ + Group: "things.test.io", + Version: "v1alpha1", + }, + RenderManifests: true, + AddChartVersion: "1.0.0", + SkipConditionalCRDLoading: true, + Resources: []model.Resource{ + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.AcrylicType{}, + }, + }, + Stored: true, + }, + }}, + } + for i := range grps { + grps[i].Init() + } + }) + It("Renders manifests without template", func() { + // get api-level code gen options from descriptors + outFiles, err := render.RenderManifests( + "appName", "manifestDir", "protoDir", "enabledExperimentalApi", + nil, + model.GroupOptions{}, + grps, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(outFiles).To(HaveLen(2)) // legacy and templated manifests + // only alpha versioned CRDs contain logic to conditionally render templates + Expect(outFiles[1].Content).ToNot(ContainSubstring("{{- if has \"kinds.things.test.io/v1alpha1\" $.Values.enabledExperimentalApi }}")) + Expect(outFiles[1].Content).ToNot(ContainSubstring("{{- end }}")) + }) + }) + + Describe("Generate combined alpha, grandfathered alpha, and non-alpha versioned CRD", func() { + var ( + grps []*model.Group + ) + + BeforeEach(func() { + grps = []*model.Group{{ + GroupVersion: schema.GroupVersion{ + Group: "things.test.io", + Version: "v3alpha1", + }, + RenderManifests: true, + AddChartVersion: "1.0.0", + Resources: []model.Resource{ + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.AcrylicType{}, + }, + }, + Stored: false, + }, + }}, + { + GroupVersion: schema.GroupVersion{ + Group: "things.test.io", + Version: "v2", + }, + RenderManifests: true, + AddChartVersion: "1.0.0", + Resources: []model.Resource{ + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.AcrylicType{}, + }, + }, + Stored: true, + }, + }}, + { + GroupVersion: schema.GroupVersion{ + Group: "things.test.io", + Version: "v1alpha1", + }, + RenderManifests: true, + AddChartVersion: "1.0.0", + SkipConditionalCRDLoading: true, + Resources: []model.Resource{ + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.AcrylicType{}, + }, + }, + Stored: true, + }, + }}, + } + for i := range grps { + grps[i].Init() + } + }) + It("Renderse manifests with chart and spec hash", func() { + + // get api-level code gen options from descriptors + outFiles, err := render.RenderManifests( + "appName", "manifestDir", "protoDir", "enabledExperimentalApi", + nil, + model.GroupOptions{}, + grps, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(outFiles).To(HaveLen(2)) // legacy and templated manifests + // only v3alpha1 version of the CRDs is conditionally rendered, v2 and v1alpha1 have no conditions surrounding them + Expect(outFiles[1].Content).To(ContainSubstring("subresources: {}\n {{- if has \"kinds.things.test.io/v3alpha1\" $.Values.enabledExperimentalApi }}")) + Expect(outFiles[1].Content).ToNot(ContainSubstring("{{- if has \"kinds.things.test.io/v1alpha1\" $.Values.enabledExperimentalApi }}\n - name: v2alpha1")) + Expect(outFiles[1].Content).To(ContainSubstring("{{- end }}\n---\n")) + Expect(strings.Count(outFiles[1].Content, "{{- end }}")).To(Equal(1)) + }) + }) + + Describe("Generate combined alpha, grandfathered alpha, and non-alpha versioned CRD", func() { + var ( + grps []*model.Group + ) + + BeforeEach(func() { + grps = []*model.Group{{ + GroupVersion: schema.GroupVersion{ + Group: "things.test.io", + Version: "v3alpha1", + }, + RenderManifests: true, + AddChartVersion: "1.0.0", + Resources: []model.Resource{ + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.AcrylicType{}, + }, + }, + Stored: false, + }, + }}, + { + GroupVersion: schema.GroupVersion{ + Group: "things.test.io", + Version: "v2", + }, + RenderManifests: true, + AddChartVersion: "1.0.0", + Resources: []model.Resource{ + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.AcrylicType{}, + }, + }, + Stored: true, + }, + }}, + { + GroupVersion: schema.GroupVersion{ + Group: "things.test.io", + Version: "v1alpha1", + }, + RenderManifests: true, + AddChartVersion: "1.0.0", + SkipConditionalCRDLoading: true, + Resources: []model.Resource{ + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.AcrylicType{}, + }, + }, + Stored: true, + }, + }}, + } + for i := range grps { + grps[i].Init() + } + }) + It("Renderse manifests with chart and spec hash", func() { + + // get api-level code gen options from descriptors + outFiles, err := render.RenderManifests( + "appName", "manifestDir", "protoDir", "enabledExperimentalApi", + nil, + model.GroupOptions{}, + grps, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(outFiles).To(HaveLen(2)) // legacy and templated manifests + // only v3alpha1 version of the CRDs is conditionally rendered, v2 and v1alpha1 have no conditions surrounding them + expectedTemplateString := "subresources: {}\n {{- if has \"kinds.things.test.io/v3alpha1\" $.Values.enabledExperimentalApi }}" + Expect(outFiles[1].Content).To(ContainSubstring(expectedTemplateString)) + Expect(strings.Count(outFiles[1].Content, expectedTemplateString)).To(Equal(1)) + Expect(outFiles[1].Content).ToNot(ContainSubstring("{{- if has \"kinds.things.test.io/v1alpha1\" $.Values.enabledExperimentalApi }}\n - name: v2alpha1")) + Expect(outFiles[1].Content).To(ContainSubstring("{{- end }}\n---\n")) + Expect(strings.Count(outFiles[1].Content, "{{- end }}\n---\n")).To(Equal(1)) + Expect(strings.Count(outFiles[1].Content, "{{- end }}")).To(Equal(1)) + }) + }) + + Describe("Render CRD template when 'EnabledAlphaApiFlagName' isn't set", func() { + It("and resource contains an alpha version that should not be skipped", func() { + grps := []*model.Group{{ + GroupVersion: schema.GroupVersion{ + Group: "things.test.io", + Version: "v3alpha1", + }, + RenderManifests: true, + AddChartVersion: "1.0.0", + Resources: []model.Resource{ + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.AcrylicType{}, + }, + }, + Stored: false, + }, + }}, + } + for i := range grps { + grps[i].Init() + } + + _, err := render.RenderManifests( + "appName", "manifestDir", "protoDir", "", + nil, + model.GroupOptions{}, + grps, + ) + Expect(err).ToNot(BeNil()) + Expect(err).To(Equal(fmt.Errorf("error rendering CRD template for kind kind: 'EnabledAlphaApiFlagName' is not defined"))) + }) + It("and resource contains an alpha version that should be skipped", func() { + grps := []*model.Group{{ + GroupVersion: schema.GroupVersion{ + Group: "things.test.io", + Version: "v3alpha1", + }, + RenderManifests: true, + AddChartVersion: "1.0.0", + SkipConditionalCRDLoading: true, + Resources: []model.Resource{ + { + Kind: "kind", + Spec: model.Field{ + Type: model.Type{ + Name: "test", + Message: &v1.AcrylicType{}, + }, + }, + Stored: false, + }, + }}, + } + for i := range grps { + grps[i].Init() + } + + _, err := render.RenderManifests( + "appName", "manifestDir", "protoDir", "", + nil, + model.GroupOptions{}, + grps, + ) + Expect(err).To(BeNil()) }) }) }) diff --git a/codegen/templates/manifests/crd.yamltmpl b/codegen/templates/manifests/crd.yamltmpl new file mode 100644 index 000000000..db66b723f --- /dev/null +++ b/codegen/templates/manifests/crd.yamltmpl @@ -0,0 +1,30 @@ +[[- range $crd := .Crds -]] +[[- if render_outer_conditional_crd_template $crd (index $crd.Spec.Versions 0).Name $.ShouldSkip ]] +{{- if has "[[ $crd.GetName ]]/[[ (index $crd.Spec.Versions 0).Name ]]" $.Values.[[ $.EnabledAlphaApiFlagName ]] }} +[[- end ]] +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: [[- $crd.GetAnnotations | toYaml | nindent 4 ]] + labels: [[- $crd.GetLabels | toYaml | nindent 4 ]] + name: [[ $crd.GetName ]] +spec: + group: [[ $crd.Spec.Group ]] + names: [[- $crd.Spec.Names | toYaml | nindent 4 ]] + scope: [[ $crd.Spec.Scope ]] + versions: + [[- range $version := $crd.Spec.Versions ]] + [[- if render_inner_conditional_crd_template $crd $version.Name $.ShouldSkip ]] + {{- if has "[[ $crd.GetName ]]/[[ $version.Name ]]" $.Values.[[ $.EnabledAlphaApiFlagName ]] }} + [[- end ]] + - + [[- $version | toYaml | indent 4 | trimPrefix " " ]] + [[- if render_inner_conditional_crd_template $crd $version.Name $.ShouldSkip ]] + {{- end }} + [[- end ]] + [[- end ]] +[[- if render_outer_conditional_crd_template $crd (index $crd.Spec.Versions 0).Name $.ShouldSkip ]] +{{- end }} +[[- end ]] +--- +[[ end ]] \ No newline at end of file diff --git a/codegen/test/chart-no-desc/crds/things.test.io_v1_crds.yaml b/codegen/test/chart-no-desc/crds/things.test.io_crds.yaml similarity index 100% rename from codegen/test/chart-no-desc/crds/things.test.io_v1_crds.yaml rename to codegen/test/chart-no-desc/crds/things.test.io_crds.yaml index b44f06926..b83d9e1b8 100644 --- a/codegen/test/chart-no-desc/crds/things.test.io_v1_crds.yaml +++ b/codegen/test/chart-no-desc/crds/things.test.io_crds.yaml @@ -1,5 +1,38 @@ # Code generated by skv2. DO NOT EDIT. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + crd.solo.io/specHash: a7f6c51daca2a86e + labels: + app: "" + app.kubernetes.io/name: "" + name: clusterresources.things.test.io +spec: + group: things.test.io + names: + kind: ClusterResource + listKind: ClusterResourceList + plural: clusterresources + singular: clusterresource + scope: Cluster + versions: + - name: v1 + schema: + openAPIV3Schema: + properties: + spec: + properties: + imported: + type: string + type: object + type: object + served: true + storage: true + subresources: {} + +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: @@ -132,36 +165,3 @@ spec: storage: true subresources: status: {} - ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - crd.solo.io/specHash: a7f6c51daca2a86e - labels: - app: "" - app.kubernetes.io/name: "" - name: clusterresources.things.test.io -spec: - group: things.test.io - names: - kind: ClusterResource - listKind: ClusterResourceList - plural: clusterresources - singular: clusterresource - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - spec: - properties: - imported: - type: string - type: object - type: object - served: true - storage: true - subresources: {} diff --git a/codegen/test/chart-no-desc/templates/things.test.io_crds.yaml b/codegen/test/chart-no-desc/templates/things.test.io_crds.yaml new file mode 100644 index 000000000..f5cc1031a --- /dev/null +++ b/codegen/test/chart-no-desc/templates/things.test.io_crds.yaml @@ -0,0 +1,169 @@ +# Code generated by skv2. DO NOT EDIT. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + crd.solo.io/specHash: a7f6c51daca2a86e + labels: + app: "" + app.kubernetes.io/name: "" + name: clusterresources.things.test.io +spec: + group: things.test.io + names: + kind: ClusterResource + listKind: ClusterResourceList + plural: clusterresources + singular: clusterresource + scope: Cluster + versions: + - name: v1 + schema: + openAPIV3Schema: + properties: + spec: + properties: + imported: + type: string + type: object + type: object + served: true + storage: true + subresources: {} +--- + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + crd.solo.io/specHash: 52f9159d46759d37 + labels: + app: "" + app.kubernetes.io/name: "" + name: paints.things.test.io +spec: + group: things.test.io + names: + kind: Paint + listKind: PaintList + plural: paints + singular: paint + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + properties: + spec: + oneOf: + - not: + anyOf: + - required: + - acrylic + - properties: + oil: + oneOf: + - not: + anyOf: + - required: + - powder + - required: + - fluid + - required: + - powder + - required: + - fluid + required: + - oil + - required: + - acrylic + - properties: + oil: + oneOf: + - not: + anyOf: + - required: + - powder + - required: + - fluid + - required: + - powder + - required: + - fluid + required: + - oil + properties: + acrylic: + properties: + body: + enum: + - Light + - Medium + - Heavy + type: string + type: object + color: + properties: + hue: + type: string + value: + format: float + type: number + type: object + myFavorite: + type: object + x-kubernetes-preserve-unknown-fields: true + oil: + properties: + fluid: + type: string + powder: + type: string + waterMixable: + type: boolean + type: object + recursiveType: + properties: + protobufValue: + x-kubernetes-preserve-unknown-fields: true + recursiveField: + type: object + x-kubernetes-preserve-unknown-fields: true + recursiveFieldOutermostScope: + type: object + x-kubernetes-preserve-unknown-fields: true + repeatedRecursiveField: + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + type: object + type: object + status: + properties: + nearbyPaints: + additionalProperties: + properties: + x: + type: object + x-kubernetes-preserve-unknown-fields: true + "y": + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + type: object + observedGeneration: + format: int64 + type: integer + percentRemaining: + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- diff --git a/codegen/test/chart/crds/things.test.io_v1_crds.yaml b/codegen/test/chart/crds/things.test.io_crds.yaml similarity index 100% rename from codegen/test/chart/crds/things.test.io_v1_crds.yaml rename to codegen/test/chart/crds/things.test.io_crds.yaml index 0a333a174..3aa575da9 100644 --- a/codegen/test/chart/crds/things.test.io_v1_crds.yaml +++ b/codegen/test/chart/crds/things.test.io_crds.yaml @@ -1,5 +1,38 @@ # Code generated by skv2. DO NOT EDIT. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + crd.solo.io/specHash: a7f6c51daca2a86e + labels: + app: "" + app.kubernetes.io/name: "" + name: clusterresources.things.test.io +spec: + group: things.test.io + names: + kind: ClusterResource + listKind: ClusterResourceList + plural: clusterresources + singular: clusterresource + scope: Cluster + versions: + - name: v1 + schema: + openAPIV3Schema: + properties: + spec: + properties: + imported: + type: string + type: object + type: object + served: true + storage: true + subresources: {} + +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: @@ -136,36 +169,3 @@ spec: storage: true subresources: status: {} - ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - crd.solo.io/specHash: a7f6c51daca2a86e - labels: - app: "" - app.kubernetes.io/name: "" - name: clusterresources.things.test.io -spec: - group: things.test.io - names: - kind: ClusterResource - listKind: ClusterResourceList - plural: clusterresources - singular: clusterresource - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - spec: - properties: - imported: - type: string - type: object - type: object - served: true - storage: true - subresources: {} diff --git a/codegen/test/chart/templates/things.test.io_crds.yaml b/codegen/test/chart/templates/things.test.io_crds.yaml new file mode 100644 index 000000000..50170ee18 --- /dev/null +++ b/codegen/test/chart/templates/things.test.io_crds.yaml @@ -0,0 +1,173 @@ +# Code generated by skv2. DO NOT EDIT. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + crd.solo.io/specHash: a7f6c51daca2a86e + labels: + app: "" + app.kubernetes.io/name: "" + name: clusterresources.things.test.io +spec: + group: things.test.io + names: + kind: ClusterResource + listKind: ClusterResourceList + plural: clusterresources + singular: clusterresource + scope: Cluster + versions: + - name: v1 + schema: + openAPIV3Schema: + properties: + spec: + properties: + imported: + type: string + type: object + type: object + served: true + storage: true + subresources: {} +--- + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + crd.solo.io/specHash: 888c42601b3e10b6 + labels: + app: "" + app.kubernetes.io/name: "" + name: paints.things.test.io +spec: + group: things.test.io + names: + kind: Paint + listKind: PaintList + plural: paints + singular: paint + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + properties: + spec: + oneOf: + - not: + anyOf: + - required: + - acrylic + - properties: + oil: + oneOf: + - not: + anyOf: + - required: + - powder + - required: + - fluid + - required: + - powder + - required: + - fluid + required: + - oil + - required: + - acrylic + - properties: + oil: + oneOf: + - not: + anyOf: + - required: + - powder + - required: + - fluid + - required: + - powder + - required: + - fluid + required: + - oil + properties: + acrylic: + properties: + body: + enum: + - Light + - Medium + - Heavy + type: string + type: object + color: + properties: + hue: + type: string + value: + format: float + type: number + type: object + myFavorite: + type: object + x-kubernetes-preserve-unknown-fields: true + oil: + properties: + fluid: + type: string + powder: + type: string + waterMixable: + type: boolean + type: object + recursiveType: + description: OpenAPI gen test for recursive fields + properties: + protobufValue: + x-kubernetes-preserve-unknown-fields: true + recursiveField: + type: object + x-kubernetes-preserve-unknown-fields: true + recursiveFieldOutermostScope: + description: |- + Ensure that FieldOptions can be defined using package name resolution that starts from the + outermost scope: https://developers.google.com/protocol-buffers/docs/proto3#packages_and_name_resolution + type: object + x-kubernetes-preserve-unknown-fields: true + repeatedRecursiveField: + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + type: object + type: object + status: + properties: + nearbyPaints: + additionalProperties: + properties: + x: + type: object + x-kubernetes-preserve-unknown-fields: true + "y": + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + type: object + observedGeneration: + format: int64 + type: integer + percentRemaining: + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- diff --git a/codegen/test/name_override_chart/crds/things.test.io_v1_crds.yaml b/codegen/test/name_override_chart/crds/things.test.io_crds.yaml similarity index 100% rename from codegen/test/name_override_chart/crds/things.test.io_v1_crds.yaml rename to codegen/test/name_override_chart/crds/things.test.io_crds.yaml index 69f3cbb1c..649673502 100644 --- a/codegen/test/name_override_chart/crds/things.test.io_v1_crds.yaml +++ b/codegen/test/name_override_chart/crds/things.test.io_crds.yaml @@ -4,19 +4,19 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - crd.solo.io/specHash: 80e7931efac2214e + crd.solo.io/specHash: 2c0000aa4677e683 labels: app: "" app.kubernetes.io/name: "" - name: paints.things.test.io + name: clusterresources.things.test.io spec: group: things.test.io names: - kind: Paint - listKind: PaintList - plural: paints - singular: paint - scope: Namespaced + kind: ClusterResource + listKind: ClusterResourceList + plural: clusterresources + singular: clusterresource + scope: Cluster versions: - name: v1 schema: @@ -25,27 +25,26 @@ spec: x-kubernetes-preserve-unknown-fields: true served: true storage: true - subresources: - status: {} + subresources: {} --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - crd.solo.io/specHash: 2c0000aa4677e683 + crd.solo.io/specHash: 80e7931efac2214e labels: app: "" app.kubernetes.io/name: "" - name: clusterresources.things.test.io + name: paints.things.test.io spec: group: things.test.io names: - kind: ClusterResource - listKind: ClusterResourceList - plural: clusterresources - singular: clusterresource - scope: Cluster + kind: Paint + listKind: PaintList + plural: paints + singular: paint + scope: Namespaced versions: - name: v1 schema: @@ -54,4 +53,5 @@ spec: x-kubernetes-preserve-unknown-fields: true served: true storage: true - subresources: {} + subresources: + status: {} diff --git a/codegen/test/name_override_chart/templates/things.test.io_crds.yaml b/codegen/test/name_override_chart/templates/things.test.io_crds.yaml new file mode 100644 index 000000000..d23e63f00 --- /dev/null +++ b/codegen/test/name_override_chart/templates/things.test.io_crds.yaml @@ -0,0 +1,59 @@ +# Code generated by skv2. DO NOT EDIT. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + crd.solo.io/specHash: 2c0000aa4677e683 + labels: + app: "" + app.kubernetes.io/name: "" + name: clusterresources.things.test.io +spec: + group: things.test.io + names: + kind: ClusterResource + listKind: ClusterResourceList + plural: clusterresources + singular: clusterresource + scope: Cluster + versions: + - name: v1 + schema: + openAPIV3Schema: + type: object + x-kubernetes-preserve-unknown-fields: true + served: true + storage: true + subresources: {} +--- + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + crd.solo.io/specHash: 80e7931efac2214e + labels: + app: "" + app.kubernetes.io/name: "" + name: paints.things.test.io +spec: + group: things.test.io + names: + kind: Paint + listKind: PaintList + plural: paints + singular: paint + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + type: object + x-kubernetes-preserve-unknown-fields: true + served: true + storage: true + subresources: + status: {} +--- diff --git a/crds/multicluster.solo.io_v1alpha1_crds.yaml b/crds/multicluster.solo.io_crds.yaml similarity index 100% rename from crds/multicluster.solo.io_v1alpha1_crds.yaml rename to crds/multicluster.solo.io_crds.yaml diff --git a/go.mod b/go.mod index 3d59df981..80d53e146 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/golang/mock v1.6.0 github.com/golang/protobuf v1.5.3 github.com/google/gnostic v0.5.7-v3refs + github.com/google/go-cmp v0.5.9 github.com/hashicorp/go-multierror v1.1.0 github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 github.com/iancoleman/strcase v0.1.3 @@ -80,7 +81,6 @@ require ( github.com/gobuffalo/packd v0.3.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/go-cmp v0.5.9 // indirect github.com/google/gofuzz v1.1.0 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/uuid v1.3.0 // indirect diff --git a/templates/multicluster.solo.io_crds.yaml b/templates/multicluster.solo.io_crds.yaml new file mode 100644 index 000000000..f430e3c30 --- /dev/null +++ b/templates/multicluster.solo.io_crds.yaml @@ -0,0 +1,161 @@ +# Code generated by skv2. DO NOT EDIT. + + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + crd.solo.io/specHash: 396fd84315636ecb + labels: + app: skv2 + app.kubernetes.io/name: skv2 + name: kubernetesclusters.multicluster.solo.io +spec: + group: multicluster.solo.io + names: + kind: KubernetesCluster + listKind: KubernetesClusterList + plural: kubernetesclusters + singular: kubernetescluster + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + spec: + description: Representation of a Kubernetes cluster that has been registered. + properties: + clusterDomain: + description: |- + name local DNS suffix used by the cluster. + used for building FQDNs for in-cluster services + defaults to 'cluster.local' + type: string + providerInfo: + description: Metadata for clusters provisioned from cloud providers. + oneOf: + - not: + anyOf: + - required: + - eks + - required: + - eks + properties: + eks: + description: Provider info for an AWS EKS provisioned cluster. + properties: + accountId: + description: AWS 12 digit account ID. + type: string + arn: + description: AWS ARN. + type: string + name: + description: EKS resource name. + type: string + region: + description: AWS region. + type: string + type: object + type: object + secretName: + description: name of the secret which contains the kubeconfig with information + to connect to the remote cluster. + type: string + type: object + status: + properties: + namespace: + description: The namespace in which cluster registration resources were + created. + type: string + policyRules: + description: The set of PolicyRules attached to ClusterRoles when this + cluster was registered. + items: + properties: + apiGroups: + description: |- + APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of + the enumerated resources in any API group will be allowed. + +optional + items: + type: string + type: array + nonResourceUrls: + description: |- + NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path + Since non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding. + Rules can either apply to API resources (such as "pods" or "secrets") or non-resource URL paths (such as "/api"), but not both. + +optional + items: + type: string + type: array + resourceNames: + description: |- + ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. + +optional + items: + type: string + type: array + resources: + description: |- + Resources is a list of resources this rule applies to. ResourceAll represents all resources. + +optional + items: + type: string + type: array + verbs: + description: Verbs is a list of Verbs that apply to ALL the ResourceKinds + and AttributeRestrictions contained in this rule. VerbAll represents + all kinds. + items: + type: string + type: array + type: object + type: array + status: + description: |- + List of statuses about the kubernetes cluster. + This list allows for multiple applications/pods to record their connection status. + items: + properties: + message: + description: A human readable message about the current state of + the object + type: string + observedGeneration: + description: |- + The most recently observed generation of the resource. This value corresponds to the `metadata.generation` of + a kubernetes resource + format: int64 + type: integer + owner: + description: |- + (optional) The owner of the status, this value can be used to identify the entity which wrote this status. + This is useful in situations where a given resource may have multiple owners. + nullable: true + type: string + processingTime: + description: The time at which this status was recorded + format: date-time + type: string + state: + description: The current state of the resource + enum: + - PENDING + - PROCESSING + - INVALID + - FAILED + - ACCEPTED + type: string + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +---