diff --git a/pkg/crd/markers/crd.go b/pkg/crd/markers/crd.go index f02e8f7ee..fe482fe12 100644 --- a/pkg/crd/markers/crd.go +++ b/pkg/crd/markers/crd.go @@ -18,6 +18,7 @@ package markers import ( "fmt" + "net/url" "strings" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -57,6 +58,11 @@ var CRDMarkers = []*definitionWithHelp{ must(markers.MakeDefinition("kubebuilder:selectablefield", markers.DescribesType, SelectableField{})). WithHelp(SelectableField{}.Help()), + + must(markers.MakeDefinition("kubebuilder:externalDocs", markers.DescribesField, ExternalDocs{})). + WithHelp(ExternalDocs{}.Help()), + must(markers.MakeDefinition("kubebuilder:externalDocs", markers.DescribesType, ExternalDocs{})). + WithHelp(ExternalDocs{}.Help()), } // TODO: categories and singular used to be annotations types @@ -419,3 +425,28 @@ func (s SelectableField) ApplyToCRD(crd *apiextensionsv1.CustomResourceDefinitio return nil } + +// +controllertools:marker:generateHelp:category=CRD + +// ExternalDocs specifies external documentation for this field or type. +// +// The url is required and must be a valid URL. The description is optional +// and provides a short description of the external documentation. +type ExternalDocs struct { + // URL specifies the URL for the target documentation. + URL string `marker:"url"` + + // Description is a short description of the target documentation. + Description string `marker:",optional"` +} + +func (m ExternalDocs) ApplyToSchema(schema *apiextensionsv1.JSONSchemaProps) error { + if _, err := url.ParseRequestURI(m.URL); err != nil { + return fmt.Errorf("invalid url %q in kubebuilder:externalDocs marker: %w", m.URL, err) + } + schema.ExternalDocs = &apiextensionsv1.ExternalDocumentation{ + URL: m.URL, + Description: m.Description, + } + return nil +} diff --git a/pkg/crd/markers/zz_generated.markerhelp.go b/pkg/crd/markers/zz_generated.markerhelp.go index bf650ab23..61b667685 100644 --- a/pkg/crd/markers/zz_generated.markerhelp.go +++ b/pkg/crd/markers/zz_generated.markerhelp.go @@ -78,6 +78,26 @@ func (DeprecatedVersion) Help() *markers.DefinitionHelp { } } +func (ExternalDocs) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies external documentation for this field or type.", + Details: "The url is required and must be a valid URL. The description is optional\nand provides a short description of the external documentation.", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "URL": { + Summary: "specifies the URL for the target documentation.", + Details: "", + }, + "Description": { + Summary: "is a short description of the target documentation.", + Details: "", + }, + }, + } +} + func (Enum) 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 6b3223db4..7df9e1cdd 100644 --- a/pkg/crd/parser_integration_test.go +++ b/pkg/crd/parser_integration_test.go @@ -224,6 +224,16 @@ var _ = Describe("CRD Generation From Parsing to CustomResourceDefinition", func }) }) + Context("ExternalDoc API", func() { + BeforeEach(func() { + pkgPaths = []string{"./external_docs/..."} + expPkgLen = 1 + }) + It("should successfully generate the CRD with external documentation", func() { + assertCRD(pkgs[0], "ExternalDoc", "testdata.kubebuilder.io_externaldocs.yaml") + }) + }) + Context("CronJob API without group", func() { BeforeEach(func() { pkgPaths = []string{"./nogroup"} diff --git a/pkg/crd/testdata/external_docs/types.go b/pkg/crd/testdata/external_docs/types.go new file mode 100644 index 000000000..75d554809 --- /dev/null +++ b/pkg/crd/testdata/external_docs/types.go @@ -0,0 +1,59 @@ +/* + +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. +*/ + +// +groupName=testdata.kubebuilder.io +// +versionName=v1 +package external_docs + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true + +// ExternalDocSpec defines the desired state of ExternalDoc +type ExternalDocSpec struct { + // This tests that external documentation can be attached to a field with url and description. + // +kubebuilder:externalDocs:url="https://example.com/docs",description="external docs description" + FieldWithExternalDoc string `json:"fieldWithExternalDoc,omitempty"` + + // This tests that external documentation can be attached with only url. + // +kubebuilder:externalDocs:url="https://example.com/docs" + FieldWithExternalDocURLOnly string `json:"fieldWithExternalDocURLOnly,omitempty"` + + // This tests that external documentation from a type is propagated. + TypeWithExternalDoc TypeWithExternalDoc `json:"typeWithExternalDoc,omitempty"` +} + +// TypeWithExternalDoc is a type with external documentation. +// +kubebuilder:externalDocs:url="https://example.com/type-docs",description="type-level external docs" +type TypeWithExternalDoc string + +// ExternalDoc is the Schema for the external docs API +type ExternalDoc struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ExternalDocSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// ExternalDocList contains a list of ExternalDoc +type ExternalDocList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ExternalDoc `json:"items"` +} diff --git a/pkg/crd/testdata/testdata.kubebuilder.io_externaldocs.yaml b/pkg/crd/testdata/testdata.kubebuilder.io_externaldocs.yaml new file mode 100644 index 000000000..a9410e47f --- /dev/null +++ b/pkg/crd/testdata/testdata.kubebuilder.io_externaldocs.yaml @@ -0,0 +1,65 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (devel) + name: externaldocs.testdata.kubebuilder.io +spec: + group: testdata.kubebuilder.io + names: + kind: ExternalDoc + listKind: ExternalDocList + plural: externaldocs + singular: externaldoc + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: ExternalDoc is the Schema for the external docs API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ExternalDocSpec defines the desired state of ExternalDoc + properties: + fieldWithExternalDoc: + description: This tests that external documentation can be attached + to a field with url and description. + externalDocs: + description: external docs description + url: https://example.com/docs + type: string + fieldWithExternalDocURLOnly: + description: This tests that external documentation can be attached + with only url. + externalDocs: + url: https://example.com/docs + type: string + typeWithExternalDoc: + description: This tests that external documentation from a type is + propagated. + externalDocs: + description: type-level external docs + url: https://example.com/type-docs + type: string + type: object + type: object + served: true + storage: true