Skip to content

Commit 7a2cb8e

Browse files
authored
Merge pull request #299 from droot/feature/limit-description
Limiting description of fields in CRD's OpenAPI Schema
2 parents 4dfb257 + e6e252b commit 7a2cb8e

File tree

5 files changed

+172
-3
lines changed

5 files changed

+172
-3
lines changed

pkg/crd/desc_visitor.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package crd
18+
19+
import (
20+
"strings"
21+
"unicode"
22+
23+
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
24+
)
25+
26+
// TruncateDescription truncates the description of fields in given schema if it
27+
// exceeds maxLen.
28+
// It tries to chop off the description at the closest sentence boundary.
29+
func TruncateDescription(schema *apiext.JSONSchemaProps, maxLen int) {
30+
EditSchema(schema, descVisitor{maxLen: maxLen})
31+
}
32+
33+
// descVisitor recursively visits all fields in the schema and truncates the
34+
// description of the fields to specified maxLen.
35+
type descVisitor struct {
36+
// maxLen is the maximum allowed length for decription of a field
37+
maxLen int
38+
}
39+
40+
func (v descVisitor) Visit(schema *apiext.JSONSchemaProps) SchemaVisitor {
41+
if schema == nil {
42+
return v
43+
}
44+
if v.maxLen < 0 {
45+
return nil /* no further work to be done for this schema */
46+
}
47+
if v.maxLen == 0 {
48+
schema.Description = ""
49+
return v
50+
}
51+
if len(schema.Description) > v.maxLen {
52+
schema.Description = truncateString(schema.Description, v.maxLen)
53+
return v
54+
}
55+
return v
56+
}
57+
58+
// truncateString truncates given desc string if it exceeds maxLen. It may
59+
// return string with length less than maxLen even in cases where original desc
60+
// exceeds maxLen because it tries to chop off the desc at the closest sentence
61+
// boundary to avoid incomplete sentences.
62+
func truncateString(desc string, maxLen int) string {
63+
desc = desc[0:maxLen]
64+
65+
// Trying to chop off at closest sentence boundary.
66+
if n := strings.LastIndexFunc(desc, isSentenceTerminal); n > 0 {
67+
return desc[0 : n+1]
68+
}
69+
// TODO(droot): Improve the logic to chop off at closest word boundary
70+
// or add elipses (...) to indicate that it's chopped incase no closest
71+
// sentence found within maxLen.
72+
return desc
73+
}
74+
75+
// helper function to determine if given rune is a sentence terminal or not.
76+
func isSentenceTerminal(r rune) bool {
77+
return unicode.Is(unicode.STerm, r)
78+
}

pkg/crd/desc_visitor_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package crd_test
18+
19+
import (
20+
. "github.com/onsi/ginkgo"
21+
. "github.com/onsi/gomega"
22+
23+
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
24+
25+
"sigs.k8s.io/controller-tools/pkg/crd"
26+
)
27+
28+
var _ = Describe("TruncateDescription", func() {
29+
30+
It("should drop the description for all fields for zero value of maxLen", func() {
31+
schema := &apiext.JSONSchemaProps{
32+
Description: "top level description",
33+
Properties: map[string]apiext.JSONSchemaProps{
34+
"Spec": apiext.JSONSchemaProps{
35+
Description: "specification for the API type",
36+
},
37+
},
38+
}
39+
crd.TruncateDescription(schema, 0)
40+
Expect(schema).To(Equal(&apiext.JSONSchemaProps{
41+
Properties: map[string]apiext.JSONSchemaProps{
42+
"Spec": apiext.JSONSchemaProps{},
43+
},
44+
}))
45+
46+
})
47+
48+
It("should truncate the description only in cases where description exceeds maxLen", func() {
49+
schema := &apiext.JSONSchemaProps{
50+
Description: "top level description of the root object.",
51+
Properties: map[string]apiext.JSONSchemaProps{
52+
"Spec": apiext.JSONSchemaProps{
53+
Description: "specification of a field",
54+
},
55+
},
56+
}
57+
original := schema.DeepCopy()
58+
crd.TruncateDescription(schema, len(schema.Description))
59+
Expect(schema).To(Equal(original))
60+
})
61+
62+
It("should truncate the description at closest sentence boundary", func() {
63+
schema := &apiext.JSONSchemaProps{
64+
Description: `This is top level description. There is an empty schema. More to come`,
65+
}
66+
crd.TruncateDescription(schema, len(schema.Description)-5)
67+
Expect(schema).To(Equal(&apiext.JSONSchemaProps{
68+
Description: `This is top level description. There is an empty schema.`,
69+
}))
70+
})
71+
72+
It("should truncate the description at maxLen in absence of sentence boundary", func() {
73+
schema := &apiext.JSONSchemaProps{
74+
Description: `This is top level description of the root object`,
75+
}
76+
crd.TruncateDescription(schema, len(schema.Description)-2)
77+
Expect(schema).To(Equal(&apiext.JSONSchemaProps{
78+
Description: `This is top level description of the root obje`,
79+
}))
80+
})
81+
})

pkg/crd/gen.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ type Generator struct {
3939
// Kubernetes API servers. The storage version's schema will be used as
4040
// the CRD's schema.
4141
TrivialVersions bool `marker:",optional"`
42+
43+
// MaxDescLen limits the description length of each field in CRD's OpenAPI schema.
44+
//
45+
// nil (default) indicates no limit on description of fields
46+
// 0 indicates drop the description completely
47+
// n means at most n characters
48+
MaxDescLen *int `marker:",optional"`
4249
}
4350

4451
func (Generator) RegisterMarkers(into *markers.Registry) error {
@@ -69,7 +76,7 @@ func (g Generator) Generate(ctx *genall.GenerationContext) error {
6976
}
7077

7178
for _, groupKind := range kubeKinds {
72-
parser.NeedCRDFor(groupKind)
79+
parser.NeedCRDFor(groupKind, g.MaxDescLen)
7380
crd := parser.CustomResourceDefinitions[groupKind]
7481
if g.TrivialVersions {
7582
toTrivialVersions(&crd)

pkg/crd/parser_integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ var _ = Describe("CRD Generation From Parsing to CustomResourceDefinition", func
7979

8080
By("requesting that the CRD be generated")
8181
groupKind := schema.GroupKind{Kind: "CronJob", Group: "testdata.kubebuilder.io"}
82-
parser.NeedCRDFor(groupKind)
82+
parser.NeedCRDFor(groupKind, nil)
8383

8484
By("checking that no errors occurred along the way (expect for type errors)")
8585
Expect(packageErrors(cronJobPkg, packages.TypeError)).NotTo(HaveOccurred())

pkg/crd/spec.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func MergeIdenticalVersionInfo(crd *apiext.CustomResourceDefinition) {
121121
// NeedCRDFor requests the full CRD for the given group-kind. It requires
122122
// that the packages containing the Go structs for that CRD have already
123123
// been loaded with NeedPackage.
124-
func (p *Parser) NeedCRDFor(groupKind schema.GroupKind) {
124+
func (p *Parser) NeedCRDFor(groupKind schema.GroupKind, maxDescLen *int) {
125125
p.init()
126126

127127
if _, exists := p.CustomResourceDefinitions[groupKind]; exists {
@@ -161,6 +161,9 @@ func (p *Parser) NeedCRDFor(groupKind schema.GroupKind) {
161161
continue
162162
}
163163
fullSchema := FlattenEmbedded(p.flattener.FlattenType(typeIdent), pkg)
164+
if maxDescLen != nil {
165+
TruncateDescription(fullSchema, *maxDescLen)
166+
}
164167
ver := apiext.CustomResourceDefinitionVersion{
165168
Name: p.GroupVersions[pkg].Version,
166169
Served: true,

0 commit comments

Comments
 (0)