Skip to content

Commit 3abdb9c

Browse files
pedjakclaude
andcommitted
feat: add kubebuilder:externalDoc marker
Implement the `kubebuilder:externalDoc` marker to allow specifying external documentation (url and description) on fields and types, populating the externalDocs field in the generated OpenAPI schema. The url field is required and validated to be a well-formed URL. The description field is optional. Ref: https://spec.openapis.org/oas/v3.0.0.html#external-documentation-object Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 51ae3bf commit 3abdb9c

File tree

5 files changed

+185
-0
lines changed

5 files changed

+185
-0
lines changed

pkg/crd/markers/crd.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package markers
1818

1919
import (
2020
"fmt"
21+
"net/url"
2122
"strings"
2223

2324
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -57,6 +58,11 @@ var CRDMarkers = []*definitionWithHelp{
5758

5859
must(markers.MakeDefinition("kubebuilder:selectablefield", markers.DescribesType, SelectableField{})).
5960
WithHelp(SelectableField{}.Help()),
61+
62+
must(markers.MakeDefinition("kubebuilder:externalDoc", markers.DescribesField, ExternalDoc{})).
63+
WithHelp(ExternalDoc{}.Help()),
64+
must(markers.MakeDefinition("kubebuilder:externalDoc", markers.DescribesType, ExternalDoc{})).
65+
WithHelp(ExternalDoc{}.Help()),
6066
}
6167

6268
// TODO: categories and singular used to be annotations types
@@ -419,3 +425,28 @@ func (s SelectableField) ApplyToCRD(crd *apiextensionsv1.CustomResourceDefinitio
419425

420426
return nil
421427
}
428+
429+
// +controllertools:marker:generateHelp:category=CRD
430+
431+
// ExternalDoc specifies external documentation for this field or type.
432+
//
433+
// The url is required and must be a valid URL. The description is optional
434+
// and provides a short description of the external documentation.
435+
type ExternalDoc struct {
436+
// URL specifies the URL for the target documentation.
437+
URL string `marker:"url"`
438+
439+
// Description is a short description of the target documentation.
440+
Description string `marker:",optional"`
441+
}
442+
443+
func (m ExternalDoc) ApplyToSchema(schema *apiextensionsv1.JSONSchemaProps) error {
444+
if _, err := url.ParseRequestURI(m.URL); err != nil {
445+
return fmt.Errorf("invalid url %q in kubebuilder:externalDoc marker: %w", m.URL, err)
446+
}
447+
schema.ExternalDocs = &apiextensionsv1.ExternalDocumentation{
448+
URL: m.URL,
449+
Description: m.Description,
450+
}
451+
return nil
452+
}

pkg/crd/markers/zz_generated.markerhelp.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/crd/parser_integration_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,16 @@ var _ = Describe("CRD Generation From Parsing to CustomResourceDefinition", func
224224
})
225225
})
226226

227+
Context("ExternalDoc API", func() {
228+
BeforeEach(func() {
229+
pkgPaths = []string{"./external_docs/..."}
230+
expPkgLen = 1
231+
})
232+
It("should successfully generate the CRD with external documentation", func() {
233+
assertCRD(pkgs[0], "ExternalDoc", "testdata.kubebuilder.io_externaldocs.yaml")
234+
})
235+
})
236+
227237
Context("CronJob API without group", func() {
228238
BeforeEach(func() {
229239
pkgPaths = []string{"./nogroup"}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
16+
// +groupName=testdata.kubebuilder.io
17+
// +versionName=v1
18+
package external_docs
19+
20+
import (
21+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
)
23+
24+
// +kubebuilder:object:root=true
25+
26+
// ExternalDocSpec defines the desired state of ExternalDoc
27+
type ExternalDocSpec struct {
28+
// This tests that external documentation can be attached to a field with url and description.
29+
// +kubebuilder:externalDoc:url="https://example.com/docs",description="external docs description"
30+
FieldWithExternalDoc string `json:"fieldWithExternalDoc,omitempty"`
31+
32+
// This tests that external documentation can be attached with only url.
33+
// +kubebuilder:externalDoc:url="https://example.com/docs"
34+
FieldWithExternalDocURLOnly string `json:"fieldWithExternalDocURLOnly,omitempty"`
35+
36+
// This tests that external documentation from a type is propagated.
37+
TypeWithExternalDoc TypeWithExternalDoc `json:"typeWithExternalDoc,omitempty"`
38+
}
39+
40+
// TypeWithExternalDoc is a type with external documentation.
41+
// +kubebuilder:externalDoc:url="https://example.com/type-docs",description="type-level external docs"
42+
type TypeWithExternalDoc string
43+
44+
// ExternalDoc is the Schema for the external docs API
45+
type ExternalDoc struct {
46+
metav1.TypeMeta `json:",inline"`
47+
metav1.ObjectMeta `json:"metadata,omitempty"`
48+
49+
Spec ExternalDocSpec `json:"spec,omitempty"`
50+
}
51+
52+
// +kubebuilder:object:root=true
53+
54+
// ExternalDocList contains a list of ExternalDoc
55+
type ExternalDocList struct {
56+
metav1.TypeMeta `json:",inline"`
57+
metav1.ListMeta `json:"metadata,omitempty"`
58+
Items []ExternalDoc `json:"items"`
59+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
apiVersion: apiextensions.k8s.io/v1
3+
kind: CustomResourceDefinition
4+
metadata:
5+
annotations:
6+
controller-gen.kubebuilder.io/version: (devel)
7+
name: externaldocs.testdata.kubebuilder.io
8+
spec:
9+
group: testdata.kubebuilder.io
10+
names:
11+
kind: ExternalDoc
12+
listKind: ExternalDocList
13+
plural: externaldocs
14+
singular: externaldoc
15+
scope: Namespaced
16+
versions:
17+
- name: v1
18+
schema:
19+
openAPIV3Schema:
20+
description: ExternalDoc is the Schema for the external docs API
21+
properties:
22+
apiVersion:
23+
description: |-
24+
APIVersion defines the versioned schema of this representation of an object.
25+
Servers should convert recognized schemas to the latest internal value, and
26+
may reject unrecognized values.
27+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
28+
type: string
29+
kind:
30+
description: |-
31+
Kind is a string value representing the REST resource this object represents.
32+
Servers may infer this from the endpoint the client submits requests to.
33+
Cannot be updated.
34+
In CamelCase.
35+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
36+
type: string
37+
metadata:
38+
type: object
39+
spec:
40+
description: ExternalDocSpec defines the desired state of ExternalDoc
41+
properties:
42+
fieldWithExternalDoc:
43+
description: This tests that external documentation can be attached
44+
to a field with url and description.
45+
externalDocs:
46+
description: external docs description
47+
url: https://example.com/docs
48+
type: string
49+
fieldWithExternalDocURLOnly:
50+
description: This tests that external documentation can be attached
51+
with only url.
52+
externalDocs:
53+
url: https://example.com/docs
54+
type: string
55+
typeWithExternalDoc:
56+
description: This tests that external documentation from a type is
57+
propagated.
58+
externalDocs:
59+
description: type-level external docs
60+
url: https://example.com/type-docs
61+
type: string
62+
type: object
63+
type: object
64+
served: true
65+
storage: true

0 commit comments

Comments
 (0)