Skip to content

Commit c0ac8a4

Browse files
committed
Add auto-generated documentation on the cluster spec.
1 parent a61d524 commit c0ac8a4

File tree

7 files changed

+768
-3
lines changed

7 files changed

+768
-3
lines changed

ACKNOWLEDGEMENTS

+14-1
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,17 @@ Onsi Fakhouri (ginkgo, gomega)
8484
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
8585
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
8686
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
87-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
87+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
88+
89+
The prometheus-operator Authors (prometheus-operator)
90+
Licensed under the Apache License, Version 2.0 (the "License");
91+
you may not use this file except in compliance with the License.
92+
You may obtain a copy of the License at
93+
94+
http://www.apache.org/licenses/LICENSE-2.0
95+
96+
Unless required by applicable law or agreed to in writing, software
97+
distributed under the License is distributed on an "AS IS" BASIS,
98+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
99+
See the License for the specific language governing permissions and
100+
limitations under the License.

Makefile

+10-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ else
1515
GOBIN=$(shell go env GOBIN)
1616
endif
1717

18-
all: manager samples
18+
all: manager samples documentation
1919

2020
# Run tests
2121
test: generate fmt vet manifests
@@ -80,6 +80,15 @@ config/samples/deployment/crd.yaml: config/crd/bases/apps.foundationdb.org_found
8080
config/samples/deployment.yaml: manifests config/samples/deployment/crd.yaml
8181
kustomize build config/samples/deployment > config/samples/deployment.yaml
8282

83+
bin/po-docgen: cmd/po-docgen/main.go cmd/po-docgen/api.go
84+
go build -o bin/po-docgen cmd/po-docgen/main.go cmd/po-docgen/api.go
85+
86+
87+
docs/cluster_spec.md: bin/po-docgen api/v1beta1/foundationdbcluster_types.go
88+
bin/po-docgen api api/v1beta1/foundationdbcluster_types.go > docs/cluster_spec.md
89+
90+
documentation: docs/cluster_spec.md
91+
8392

8493
# find or download controller-gen
8594
# download controller-gen if necessary

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ For more information about using the operator, see the [user manual](docs/user_m
2929
For more information on version compatibility, see our [compatibility guide](docs/compatibility.md).
3030

3131
For more information on the fields you can define on the cluster resource, see
32-
the [go docs](https://godoc.org/github.com/FoundationDB/fdb-kubernetes-operator/pkg/apis/apps/v1beta1#FoundationDBCluster).
32+
the [API documentation](docs/cluster_spec.md).
3333

3434
# Local Development
3535

cmd/po-docgen/api.go

+270
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
// Copyright 2020 The prometheus-operator Authors and the FoundationDB project authors.
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+
package main
16+
17+
import (
18+
"bytes"
19+
"fmt"
20+
"go/ast"
21+
"go/doc"
22+
"go/parser"
23+
"go/token"
24+
"reflect"
25+
"strings"
26+
)
27+
28+
const (
29+
firstParagraph = `<br>
30+
# API Docs
31+
This Document documents the types introduced by the FoundationDB Operator to be consumed by users.
32+
> Note this document is generated from code comments. When contributing a change to this document please do so by changing the code comments.`
33+
)
34+
35+
var (
36+
links = map[string]string{
37+
"metav1.ObjectMeta": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectmeta-v1-meta",
38+
"metav1.ListMeta": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#listmeta-v1-meta",
39+
"metav1.LabelSelector": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#labelselector-v1-meta",
40+
"corev1.ResourceRequirements": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#resourcerequirements-v1-core",
41+
"corev1.LocalObjectReference": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#localobjectreference-v1-core",
42+
"corev1.SecretKeySelector": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#secretkeyselector-v1-core",
43+
"corev1.PersistentVolumeClaim": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#persistentvolumeclaim-v1-core",
44+
"corev1.EmptyDirVolumeSource": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#emptydirvolumesource-v1-core",
45+
"corev1.Container": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#container-v1-core",
46+
"corev1.PodSecurityContext": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#podsecuritycontext-v1-core",
47+
"corev1.SecurityContext": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#securitycontext-v1-core",
48+
"corev1.EnvVar": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#envvar-v1-core",
49+
"corev1.VolumeMount": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#volumemount-v1-core",
50+
"corev1.PodTemplateSpec": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#podtemplatespec-v1-core",
51+
"corev1.ConfigMap": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#configmap-v1-core",
52+
}
53+
54+
selfLinks = map[string]string{}
55+
)
56+
57+
func toSectionLink(name string) string {
58+
name = strings.ToLower(name)
59+
name = strings.Replace(name, " ", "-", -1)
60+
return name
61+
}
62+
63+
func printTOC(types []KubeTypes) {
64+
fmt.Printf("\n## Table of Contents\n")
65+
for _, t := range types {
66+
strukt := t[0]
67+
if len(t) > 1 {
68+
fmt.Printf("* [%s](#%s)\n", strukt.Name, toSectionLink(strukt.Name))
69+
}
70+
}
71+
}
72+
73+
func printAPIDocs(paths []string) {
74+
fmt.Println(firstParagraph)
75+
76+
types := ParseDocumentationFrom(paths)
77+
for _, t := range types {
78+
strukt := t[0]
79+
selfLinks[strukt.Name] = "#" + strings.ToLower(strukt.Name)
80+
}
81+
82+
// we need to parse once more to now add the self links
83+
types = ParseDocumentationFrom(paths)
84+
85+
printTOC(types)
86+
87+
for _, t := range types {
88+
strukt := t[0]
89+
if len(t) > 1 {
90+
fmt.Printf("\n## %s\n\n%s\n\n", strukt.Name, strukt.Doc)
91+
92+
fmt.Println("| Field | Description | Scheme | Required |")
93+
fmt.Println("| ----- | ----------- | ------ | -------- |")
94+
fields := t[1:(len(t))]
95+
for _, f := range fields {
96+
fmt.Println("|", f.Name, "|", f.Doc, "|", f.Type, "|", f.Mandatory, "|")
97+
}
98+
fmt.Println("")
99+
fmt.Println("[Back to TOC](#table-of-contents)")
100+
}
101+
}
102+
}
103+
104+
// Pair of strings. We keed the name of fields and the doc
105+
type Pair struct {
106+
Name, Doc, Type string
107+
Mandatory bool
108+
}
109+
110+
// KubeTypes is an array to represent all available types in a parsed file. [0] is for the type itself
111+
type KubeTypes []Pair
112+
113+
// ParseDocumentationFrom gets all types' documentation and returns them as an
114+
// array. Each type is again represented as an array (we have to use arrays as we
115+
// need to be sure for the order of the fields). This function returns fields and
116+
// struct definitions that have no documentation as {name, ""}.
117+
func ParseDocumentationFrom(srcs []string) []KubeTypes {
118+
var docForTypes []KubeTypes
119+
120+
for _, src := range srcs {
121+
pkg := astFrom(src)
122+
123+
for _, kubType := range pkg.Types {
124+
if structType, ok := kubType.Decl.Specs[0].(*ast.TypeSpec).Type.(*ast.StructType); ok {
125+
var ks KubeTypes
126+
ks = append(ks, Pair{kubType.Name, fmtRawDoc(kubType.Doc), "", false})
127+
128+
for _, field := range structType.Fields.List {
129+
typeString := fieldType(field.Type)
130+
fieldMandatory := fieldRequired(field)
131+
if n := fieldName(field); n != "-" {
132+
fieldDoc := fmtRawDoc(field.Doc.Text())
133+
ks = append(ks, Pair{n, fieldDoc, typeString, fieldMandatory})
134+
}
135+
}
136+
docForTypes = append(docForTypes, ks)
137+
}
138+
}
139+
}
140+
141+
return docForTypes
142+
}
143+
144+
func astFrom(filePath string) *doc.Package {
145+
fset := token.NewFileSet()
146+
m := make(map[string]*ast.File)
147+
148+
f, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
149+
if err != nil {
150+
fmt.Println(err)
151+
return nil
152+
}
153+
154+
m[filePath] = f
155+
apkg, _ := ast.NewPackage(fset, m, nil, nil)
156+
157+
return doc.New(apkg, "", 0)
158+
}
159+
160+
func fmtRawDoc(rawDoc string) string {
161+
var buffer bytes.Buffer
162+
delPrevChar := func() {
163+
if buffer.Len() > 0 {
164+
buffer.Truncate(buffer.Len() - 1) // Delete the last " " or "\n"
165+
}
166+
}
167+
168+
// Ignore all lines after ---
169+
rawDoc = strings.Split(rawDoc, "---")[0]
170+
171+
for _, line := range strings.Split(rawDoc, "\n") {
172+
line = strings.TrimRight(line, " ")
173+
leading := strings.TrimLeft(line, " ")
174+
switch {
175+
case len(line) == 0: // Keep paragraphs
176+
delPrevChar()
177+
buffer.WriteString("\n\n")
178+
case strings.HasPrefix(leading, "TODO"): // Ignore one line TODOs
179+
case strings.HasPrefix(leading, "+"): // Ignore instructions to go2idl
180+
default:
181+
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
182+
delPrevChar()
183+
line = "\n" + line + "\n" // Replace it with newline. This is useful when we have a line with: "Example:\n\tJSON-someting..."
184+
} else {
185+
line += " "
186+
}
187+
buffer.WriteString(line)
188+
}
189+
}
190+
191+
postDoc := strings.TrimRight(buffer.String(), "\n")
192+
postDoc = strings.Replace(postDoc, "\\\"", "\"", -1) // replace user's \" to "
193+
postDoc = strings.Replace(postDoc, "\"", "\\\"", -1) // Escape "
194+
postDoc = strings.Replace(postDoc, "\n", "\\n", -1)
195+
postDoc = strings.Replace(postDoc, "\t", "\\t", -1)
196+
postDoc = strings.Replace(postDoc, "|", "\\|", -1)
197+
198+
return postDoc
199+
}
200+
201+
func toLink(typeName string) string {
202+
selfLink, hasSelfLink := selfLinks[typeName]
203+
if hasSelfLink {
204+
return wrapInLink(typeName, selfLink)
205+
}
206+
207+
link, hasLink := links[typeName]
208+
if hasLink {
209+
return wrapInLink(typeName, link)
210+
}
211+
212+
return typeName
213+
}
214+
215+
func wrapInLink(text, link string) string {
216+
return fmt.Sprintf("[%s](%s)", text, link)
217+
}
218+
219+
// fieldName returns the name of the field as it should appear in JSON format
220+
// "-" indicates that this field is not part of the JSON representation
221+
func fieldName(field *ast.Field) string {
222+
jsonTag := ""
223+
if field.Tag != nil {
224+
jsonTag = reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("json") // Delete first and last quotation
225+
if strings.Contains(jsonTag, "inline") {
226+
return "-"
227+
}
228+
}
229+
230+
jsonTag = strings.Split(jsonTag, ",")[0] // This can return "-"
231+
if jsonTag == "" {
232+
if field.Names != nil {
233+
return field.Names[0].Name
234+
}
235+
return field.Type.(*ast.Ident).Name
236+
}
237+
return jsonTag
238+
}
239+
240+
// fieldRequired returns whether a field is a required field.
241+
func fieldRequired(field *ast.Field) bool {
242+
jsonTag := ""
243+
if field.Tag != nil {
244+
jsonTag = reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("json") // Delete first and last quotation
245+
return !strings.Contains(jsonTag, "omitempty")
246+
}
247+
248+
return false
249+
}
250+
251+
func fieldType(typ ast.Expr) string {
252+
switch typ.(type) {
253+
case *ast.Ident:
254+
return toLink(typ.(*ast.Ident).Name)
255+
case *ast.StarExpr:
256+
return "*" + toLink(fieldType(typ.(*ast.StarExpr).X))
257+
case *ast.SelectorExpr:
258+
e := typ.(*ast.SelectorExpr)
259+
pkg := e.X.(*ast.Ident)
260+
t := e.Sel
261+
return toLink(pkg.Name + "." + t.Name)
262+
case *ast.ArrayType:
263+
return "[]" + toLink(fieldType(typ.(*ast.ArrayType).Elt))
264+
case *ast.MapType:
265+
mapType := typ.(*ast.MapType)
266+
return "map[" + toLink(fieldType(mapType.Key)) + "]" + toLink(fieldType(mapType.Value))
267+
default:
268+
return ""
269+
}
270+
}

cmd/po-docgen/doc.go

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// This is adapted from the po-docgen tool from the
2+
// Prometheus Operator project:
3+
// https://github.com/coreos/prometheus-operator

cmd/po-docgen/main.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2020 The prometheus-operator Authors and the FoundationDB project authors.
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+
package main
16+
17+
import (
18+
"os"
19+
)
20+
21+
func main() {
22+
switch os.Args[1] {
23+
case "api":
24+
printAPIDocs(os.Args[2:])
25+
}
26+
}

0 commit comments

Comments
 (0)