From e8aea0c3c1c14993a4c692cfb928601233dfee50 Mon Sep 17 00:00:00 2001 From: Siyu Wang Date: Mon, 13 Jun 2022 19:11:30 +0800 Subject: [PATCH] Support api-approved annotation for CRD with k8s group --- pkg/crd/markers/crd.go | 30 +++++++++++++++++++ pkg/crd/markers/zz_generated.markerhelp.go | 16 ++++++++++ pkg/crd/parser_integration_test.go | 7 +++-- pkg/crd/spec.go | 24 ++++++++++----- pkg/crd/testdata/cronjob_types.go | 1 + .../testdata.kubebuilder.io_cronjobs.yaml | 1 + 6 files changed, 70 insertions(+), 9 deletions(-) diff --git a/pkg/crd/markers/crd.go b/pkg/crd/markers/crd.go index 5e99256c1..ed76f9a28 100644 --- a/pkg/crd/markers/crd.go +++ b/pkg/crd/markers/crd.go @@ -19,6 +19,7 @@ package markers import ( "fmt" + "k8s.io/apiextensions-apiserver/pkg/apihelpers" apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "sigs.k8s.io/controller-tools/pkg/markers" @@ -51,6 +52,9 @@ var CRDMarkers = []*definitionWithHelp{ must(markers.MakeDefinition("kubebuilder:deprecatedversion", markers.DescribesType, DeprecatedVersion{})). WithHelp(DeprecatedVersion{}.Help()), + + must(markers.MakeDefinition("kubebuilder:apiapprovedk8sgroup", markers.DescribesType, APIApprovedKubernetesGroup{})). + WithHelp(APIApprovedKubernetesGroup{}.Help()), } // TODO: categories and singular used to be annotations types @@ -345,3 +349,29 @@ func (s DeprecatedVersion) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, } return nil } + +// +controllertools:marker:generateHelp:category=CRD + +// APIApprovedKubernetesGroup configures the api-approved.kubernetes.io for a CRD +// with Kubernetes groups, including "k8s.io", "*.k8s.io", "kubernetes.io", "*.kubernetes.io". +type APIApprovedKubernetesGroup struct { + // URL is the value of api-approved.kubernetes.io annotation on CRD, + // it should be a valid URL which contains Scheme and Host and + // should not start with "unapproved". + URL string `marker:"URL"` +} + +func (s APIApprovedKubernetesGroup) ApplyToCRD(crd *apiext.CustomResourceDefinition, version string) error { + if crd.Annotations == nil { + crd.Annotations = map[string]string{} + } + crd.Annotations[apiext.KubeAPIApprovedAnnotation] = s.URL + + // validate if the annotation is valid + state, reason := apihelpers.GetAPIApprovalState(crd.Annotations) + if state != apihelpers.APIApproved { + return fmt.Errorf("invalid URL %s that is not approved: %v", s.URL, reason) + } + + return nil +} diff --git a/pkg/crd/markers/zz_generated.markerhelp.go b/pkg/crd/markers/zz_generated.markerhelp.go index a3c054748..da99a8b1c 100644 --- a/pkg/crd/markers/zz_generated.markerhelp.go +++ b/pkg/crd/markers/zz_generated.markerhelp.go @@ -25,6 +25,22 @@ import ( "sigs.k8s.io/controller-tools/pkg/markers" ) +func (APIApprovedKubernetesGroup) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD", + DetailedHelp: markers.DetailedHelp{ + Summary: "configures the api-approved.kubernetes.io for a CRD with Kubernetes groups, including \"k8s.io\", \"*.k8s.io\", \"kubernetes.io\", \"*.kubernetes.io\".", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "URL": { + Summary: "is the value of api-approved.kubernetes.io annotation on CRD, it should be a valid URL which contains Scheme and Host and should not start with \"unapproved\".", + Details: "", + }, + }, + } +} + func (Default) Help() *markers.DefinitionHelp { return &markers.DefinitionHelp{ Category: "CRD validation", diff --git a/pkg/crd/parser_integration_test.go b/pkg/crd/parser_integration_test.go index 46ce7c149..772e21da7 100644 --- a/pkg/crd/parser_integration_test.go +++ b/pkg/crd/parser_integration_test.go @@ -122,8 +122,11 @@ var _ = Describe("CRD Generation From Parsing to CustomResourceDefinition", func By(fmt.Sprintf("parsing the desired %s YAML", kind)) var crd apiext.CustomResourceDefinition ExpectWithOffset(1, yaml.Unmarshal(expectedFile, &crd)).To(Succeed()) - // clear the annotations -- we don't care about the attribution annotation - crd.Annotations = nil + // clear the version annotation -- we don't care about the attribution annotation + delete(crd.Annotations, "controller-gen.kubebuilder.io/version") + if len(crd.Annotations) == 0 { + crd.Annotations = nil + } By(fmt.Sprintf("comparing the two %s CRDs", kind)) ExpectWithOffset(1, parser.CustomResourceDefinitions[groupKind]).To(Equal(crd), "type not as expected, check pkg/crd/testdata/README.md for more details.\n\nDiff:\n\n%s", cmp.Diff(parser.CustomResourceDefinitions[groupKind], crd)) diff --git a/pkg/crd/spec.go b/pkg/crd/spec.go index fc0099528..69b816925 100644 --- a/pkg/crd/spec.go +++ b/pkg/crd/spec.go @@ -30,13 +30,21 @@ import ( ) // SpecMarker is a marker that knows how to apply itself to a particular -// version in a CRD. +// version in a CRD Spec. type SpecMarker interface { // ApplyToCRD applies this marker to the given CRD, in the given version // within that CRD. It's called after everything else in the CRD is populated. ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error } +// Marker is a marker that knows how to apply itself to a particular +// version in a CRD. +type Marker interface { + // ApplyToCRD applies this marker to the given CRD, in the given version + // within that CRD. It's called after everything else in the CRD is populated. + ApplyToCRD(crd *apiext.CustomResourceDefinition, version string) error +} + // NeedCRDFor requests the full CRD for the given group-kind. It requires // that the packages containing the Go structs for that CRD have already // been loaded with NeedPackage. @@ -109,12 +117,14 @@ func (p *Parser) NeedCRDFor(groupKind schema.GroupKind, maxDescLen *int) { for _, markerVals := range typeInfo.Markers { for _, val := range markerVals { - crdMarker, isCrdMarker := val.(SpecMarker) - if !isCrdMarker { - continue - } - if err := crdMarker.ApplyToCRD(&crd.Spec, ver); err != nil { - pkg.AddError(loader.ErrFromNode(err /* an okay guess */, typeInfo.RawSpec)) + if specMarker, isSpecMarker := val.(SpecMarker); isSpecMarker { + if err := specMarker.ApplyToCRD(&crd.Spec, ver); err != nil { + pkg.AddError(loader.ErrFromNode(err /* an okay guess */, typeInfo.RawSpec)) + } + } else if crdMarker, isCRDMarker := val.(Marker); isCRDMarker { + if err := crdMarker.ApplyToCRD(&crd, ver); err != nil { + pkg.AddError(loader.ErrFromNode(err /* an okay guess */, typeInfo.RawSpec)) + } } } } diff --git a/pkg/crd/testdata/cronjob_types.go b/pkg/crd/testdata/cronjob_types.go index 5089c9ad2..e978d0ea4 100644 --- a/pkg/crd/testdata/cronjob_types.go +++ b/pkg/crd/testdata/cronjob_types.go @@ -478,6 +478,7 @@ type CronJobStatus struct { // +kubebuilder:subresource:status // +kubebuilder:resource:singular=mycronjob // +kubebuilder:storageversion +// +kubebuilder:apiapprovedk8sgroup:URL="https://github.com/kubernetes-sigs/controller-tools" // CronJob is the Schema for the cronjobs API type CronJob struct { diff --git a/pkg/crd/testdata/testdata.kubebuilder.io_cronjobs.yaml b/pkg/crd/testdata/testdata.kubebuilder.io_cronjobs.yaml index fab9e194e..be70641fd 100644 --- a/pkg/crd/testdata/testdata.kubebuilder.io_cronjobs.yaml +++ b/pkg/crd/testdata/testdata.kubebuilder.io_cronjobs.yaml @@ -4,6 +4,7 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: (devel) + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/controller-tools creationTimestamp: null name: cronjobs.testdata.kubebuilder.io spec: