Skip to content

Commit b836f7a

Browse files
✨ Add Title marker (#1175)
* feat: add title marker * tests: add validation fail tests * test: change test name
1 parent f3ae3e4 commit b836f7a

File tree

6 files changed

+129
-0
lines changed

6 files changed

+129
-0
lines changed

pkg/crd/markers/validation.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ var FieldOnlyMarkers = []*definitionWithHelp{
103103

104104
must(markers.MakeDefinition(SchemalessName, markers.DescribesField, Schemaless{})).
105105
WithHelp(Schemaless{}.Help()),
106+
107+
must(markers.MakeAnyTypeDefinition("kubebuilder:title", markers.DescribesField, Title{})).
108+
WithHelp(Title{}.Help()),
106109
}
107110

108111
// ValidationIshMarkers are field-and-type markers that don't fall under the
@@ -242,6 +245,17 @@ type Default struct {
242245
Value interface{}
243246
}
244247

248+
// +controllertools:marker:generateHelp:category="CRD validation"
249+
// Title sets the title for this field.
250+
//
251+
// The title is metadata that makes the OpenAPI documentation more user-friendly,
252+
// making the schema more understandable when viewed in documentation tools.
253+
// It's a metadata field that doesn't affect validation but provides
254+
// important context about what the schema represents.
255+
type Title struct {
256+
Value interface{}
257+
}
258+
245259
// +controllertools:marker:generateHelp:category="CRD validation"
246260
// Default sets the default value for this field.
247261
//
@@ -527,6 +541,19 @@ func (m Default) ApplyPriority() ApplyPriority {
527541
return 10
528542
}
529543

544+
func (m Title) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
545+
if m.Value == nil {
546+
// only apply to the schema if we have a non-nil title
547+
return nil
548+
}
549+
title, isStr := m.Value.(string)
550+
if !isStr {
551+
return fmt.Errorf("expected string, got %T", m.Value)
552+
}
553+
schema.Title = title
554+
return nil
555+
}
556+
530557
func (m *KubernetesDefault) ParseMarker(_ string, _ string, restFields string) error {
531558
if strings.HasPrefix(strings.TrimSpace(restFields), "ref(") {
532559
// Skip +default=ref(...) values for now, since we don't have a good way to evaluate go constant values via AST.

pkg/crd/markers/zz_generated.markerhelp.go

Lines changed: 16 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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,19 @@ var _ = Describe("CRD Generation From Parsing to CustomResourceDefinition", func
187187
})
188188
})
189189

190+
Context("Field with unvalid title format", func() {
191+
BeforeEach(func() {
192+
pkgPaths = []string{"./wrong_title_format"}
193+
expPkgLen = 1
194+
})
195+
It("cannot generate title field from integer", func() {
196+
assertError(pkgs[0], "JobSpec", "expected string, got int")
197+
})
198+
It("cannot generate title field from map", func() {
199+
assertError(pkgs[0], "TestType", "expected string, got map[string]interface {}")
200+
})
201+
})
202+
190203
Context("CronJob API without group", func() {
191204
BeforeEach(func() {
192205
pkgPaths = []string{"./nogroup"}

pkg/crd/testdata/cronjob_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,20 +113,24 @@ type CronJobSpec struct {
113113
// This tests that primitive defaulting can be performed.
114114
// +kubebuilder:default=forty-two
115115
// +kubebuilder:example=forty-two
116+
// +kubebuilder:title=DefaultedString
116117
DefaultedString string `json:"defaultedString"`
117118

118119
// This tests that slice defaulting can be performed.
119120
// +kubebuilder:default={a,b}
120121
// +kubebuilder:example={a,b}
122+
// +kubebuilder:title=DefaultedSlice
121123
DefaultedSlice []string `json:"defaultedSlice"`
122124

123125
// This tests that slice and object defaulting can be performed.
124126
// +kubebuilder:default={{nested: {foo: "baz", bar: true}},{nested: {foo: "qux", bar: false}}}
125127
// +kubebuilder:example={{nested: {foo: "baz", bar: true}},{nested: {foo: "qux", bar: false}}}
128+
// +kubebuilder:title="124"
126129
DefaultedObject []RootObject `json:"defaultedObject"`
127130

128131
// This tests that empty slice defaulting can be performed.
129132
// +kubebuilder:default={}
133+
// +kubebuilder:title="{}"
130134
DefaultedEmptySlice []string `json:"defaultedEmptySlice"`
131135

132136
// This tests that an empty object defaulting can be performed on a map.

pkg/crd/testdata/testdata.kubebuilder.io_cronjobs.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ spec:
131131
description: This tests that empty slice defaulting can be performed.
132132
items:
133133
type: string
134+
title: '{}'
134135
type: array
135136
defaultedObject:
136137
default:
@@ -163,6 +164,7 @@ spec:
163164
required:
164165
- nested
165166
type: object
167+
title: "124"
166168
type: array
167169
defaultedSlice:
168170
default:
@@ -174,11 +176,13 @@ spec:
174176
- b
175177
items:
176178
type: string
179+
title: DefaultedSlice
177180
type: array
178181
defaultedString:
179182
default: forty-two
180183
description: This tests that primitive defaulting can be performed.
181184
example: forty-two
185+
title: DefaultedString
182186
type: string
183187
doubleDefaultedString:
184188
default: kubebuilder-default
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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=v1beta1
18+
package job
19+
20+
import (
21+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
23+
"testdata.kubebuilder.io/cronjob/unserved"
24+
)
25+
26+
// +kubebuilder:object:root=true
27+
// +kubebuilder:subresource:status
28+
// +kubebuilder:resource:singular=job
29+
30+
type TestType struct {
31+
// Count is the number of times a job may be executed.
32+
//
33+
// +kubebuilder:title={}
34+
Count int32 `json:"count"`
35+
}
36+
37+
// JobSpec is the spec for the jobs API.
38+
type JobSpec struct {
39+
// FriendlyName is the friendly name for the job.
40+
//
41+
// +kubebuilder:title=123
42+
FriendlyName string `json:"friendlyName"`
43+
44+
// CronJob is the spec for the related CrongJob.
45+
CronnJob unserved.CronJobSpec `json:"crongJob"`
46+
}
47+
48+
// Job is the Schema for the jobs API
49+
type Job struct {
50+
/*
51+
*/
52+
metav1.TypeMeta `json:",inline"`
53+
metav1.ObjectMeta `json:"metadata,omitempty"`
54+
55+
Spec JobSpec `json:"spec"`
56+
}
57+
58+
// +kubebuilder:object:root=true
59+
60+
// JobList contains a list of Job
61+
type JobList struct {
62+
metav1.TypeMeta `json:",inline"`
63+
metav1.ListMeta `json:"metadata,omitempty"`
64+
Items []Job `json:"items"`
65+
}

0 commit comments

Comments
 (0)