use traditional controller tool to generate code

This commit is contained in:
runzexia
2019-08-07 21:05:12 +08:00
parent bd5f916557
commit e5d59b75a8
86 changed files with 9764 additions and 116 deletions

View File

@@ -0,0 +1,287 @@
/*
Copyright 2018 The Kubernetes Authors.
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.
*/
package parse
import (
"fmt"
"path"
"path/filepath"
"strings"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/gengo/types"
"sigs.k8s.io/controller-tools/pkg/internal/codegen"
)
type genUnversionedType struct {
Type *types.Type
Resource *codegen.APIResource
}
func (b *APIs) parseAPIs() {
apis := &codegen.APIs{
Domain: b.Domain,
Package: b.APIsPkg,
Groups: map[string]*codegen.APIGroup{},
Rules: b.Rules,
Informers: b.Informers,
}
for group, versionMap := range b.ByGroupVersionKind {
apiGroup := &codegen.APIGroup{
Group: group,
GroupTitle: strings.Title(group),
Domain: b.Domain,
Versions: map[string]*codegen.APIVersion{},
UnversionedResources: map[string]*codegen.APIResource{},
}
for version, kindMap := range versionMap {
apiVersion := &codegen.APIVersion{
Domain: b.Domain,
Group: group,
Version: version,
Resources: map[string]*codegen.APIResource{},
}
for kind, resource := range kindMap {
apiResource := &codegen.APIResource{
Domain: resource.Domain,
Version: resource.Version,
Group: resource.Group,
Resource: resource.Resource,
Type: resource.Type,
REST: resource.REST,
Kind: resource.Kind,
Subresources: resource.Subresources,
StatusStrategy: resource.StatusStrategy,
Strategy: resource.Strategy,
NonNamespaced: resource.NonNamespaced,
ShortName: resource.ShortName,
}
parseDoc(resource, apiResource)
apiVersion.Resources[kind] = apiResource
// Set the package for the api version
apiVersion.Pkg = b.context.Universe[resource.Type.Name.Package]
// Set the package for the api group
apiGroup.Pkg = b.context.Universe[filepath.Dir(resource.Type.Name.Package)]
if apiGroup.Pkg != nil {
apiGroup.PkgPath = apiGroup.Pkg.Path
}
apiGroup.UnversionedResources[kind] = apiResource
}
apiGroup.Versions[version] = apiVersion
}
b.parseStructs(apiGroup)
apis.Groups[group] = apiGroup
}
apis.Pkg = b.context.Universe[b.APIsPkg]
b.APIs = apis
}
func (b *APIs) parseStructs(apigroup *codegen.APIGroup) {
remaining := []genUnversionedType{}
for _, version := range apigroup.Versions {
for _, resource := range version.Resources {
remaining = append(remaining, genUnversionedType{resource.Type, resource})
}
}
for _, version := range b.SubByGroupVersionKind[apigroup.Group] {
for _, kind := range version {
remaining = append(remaining, genUnversionedType{kind, nil})
}
}
done := sets.String{}
for len(remaining) > 0 {
// Pop the next element from the list
next := remaining[0]
remaining[0] = remaining[len(remaining)-1]
remaining = remaining[:len(remaining)-1]
// Already processed this type. Skip it
if done.Has(next.Type.Name.Name) {
continue
}
done.Insert(next.Type.Name.Name)
// Generate the struct and append to the list
result, additionalTypes := parseType(next.Type)
// This is a resource, so generate the client
if b.genClient(next.Type) {
result.GenClient = true
result.GenDeepCopy = true
}
if next.Resource != nil {
result.NonNamespaced = IsNonNamespaced(next.Type)
}
if b.genDeepCopy(next.Type) {
result.GenDeepCopy = true
}
apigroup.Structs = append(apigroup.Structs, result)
// Add the newly discovered subtypes
for _, at := range additionalTypes {
remaining = append(remaining, genUnversionedType{at, nil})
}
}
}
// parseType parses the type into a Struct, and returns a list of types that
// need to be parsed
func parseType(t *types.Type) (*codegen.Struct, []*types.Type) {
remaining := []*types.Type{}
s := &codegen.Struct{
Name: t.Name.Name,
GenClient: false,
GenUnversioned: true, // Generate unversioned structs by default
}
for _, c := range t.CommentLines {
if strings.Contains(c, "+genregister:unversioned=false") {
// Don't generate the unversioned struct
s.GenUnversioned = false
}
}
for _, member := range t.Members {
uType := member.Type.Name.Name
memberName := member.Name
uImport := ""
// Use the element type for Pointers, Maps and Slices
mSubType := member.Type
hasElem := false
for mSubType.Elem != nil {
mSubType = mSubType.Elem
hasElem = true
}
if hasElem {
// Strip the package from the field type
uType = strings.Replace(member.Type.String(), mSubType.Name.Package+".", "", 1)
}
base := filepath.Base(member.Type.String())
samepkg := t.Name.Package == mSubType.Name.Package
// If not in the same package, calculate the import pkg
if !samepkg {
parts := strings.Split(base, ".")
if len(parts) > 1 {
// Don't generate unversioned types for core types, just use the versioned types
if strings.HasPrefix(mSubType.Name.Package, "k8s.io/api/") {
// Import the package under an alias so it doesn't conflict with other groups
// having the same version
importAlias := path.Base(path.Dir(mSubType.Name.Package)) + path.Base(mSubType.Name.Package)
uImport = fmt.Sprintf("%s \"%s\"", importAlias, mSubType.Name.Package)
if hasElem {
// Replace the full package with the alias when referring to the type
uType = strings.Replace(member.Type.String(), mSubType.Name.Package, importAlias, 1)
} else {
// Replace the full package with the alias when referring to the type
uType = fmt.Sprintf("%s.%s", importAlias, parts[1])
}
} else {
switch member.Type.Name.Package {
case "k8s.io/apimachinery/pkg/apis/meta/v1":
// Use versioned types for meta/v1
uImport = fmt.Sprintf("%s \"%s\"", "metav1", "k8s.io/apimachinery/pkg/apis/meta/v1")
uType = "metav1." + parts[1]
default:
// Use unversioned types for everything else
t := member.Type
if t.Elem != nil {
// handle Pointers, Maps, Slices
// We need to parse the package from the Type String
t = t.Elem
str := member.Type.String()
startPkg := strings.LastIndexAny(str, "*]")
endPkg := strings.LastIndexAny(str, ".")
pkg := str[startPkg+1 : endPkg]
name := str[endPkg+1:]
prefix := str[:startPkg+1]
uImportBase := path.Base(pkg)
uImportName := path.Base(path.Dir(pkg)) + uImportBase
uImport = fmt.Sprintf("%s \"%s\"", uImportName, pkg)
uType = prefix + uImportName + "." + name
} else {
// handle non- Pointer, Maps, Slices
pkg := t.Name.Package
name := t.Name.Name
// Come up with the alias the package is imported under
// Concatenate with directory package to reduce naming collisions
uImportBase := path.Base(pkg)
uImportName := path.Base(path.Dir(pkg)) + uImportBase
// Create the import statement
uImport = fmt.Sprintf("%s \"%s\"", uImportName, pkg)
// Create the field type name - should be <pkgalias>.<TypeName>
uType = uImportName + "." + name
}
}
}
}
}
if member.Embedded {
memberName = ""
}
s.Fields = append(s.Fields, &codegen.Field{
Name: memberName,
VersionedPackage: member.Type.Name.Package,
UnversionedImport: uImport,
UnversionedType: uType,
})
// Add this member Type for processing if it isn't a primitive and
// is part of the same API group
if !mSubType.IsPrimitive() && GetGroup(mSubType) == GetGroup(t) {
remaining = append(remaining, mSubType)
}
}
return s, remaining
}
func (b *APIs) genClient(c *types.Type) bool {
comments := Comments(c.CommentLines)
resource := comments.getTag("resource", ":") + comments.getTag("kubebuilder:resource", ":")
return len(resource) > 0
}
func (b *APIs) genDeepCopy(c *types.Type) bool {
comments := Comments(c.CommentLines)
return comments.hasTag("subresource-request")
}
func parseDoc(resource, apiResource *codegen.APIResource) {
if HasDocAnnotation(resource.Type) {
resource.DocAnnotation = getDocAnnotation(resource.Type, "warning", "note")
apiResource.DocAnnotation = resource.DocAnnotation
}
}

View File

@@ -0,0 +1,42 @@
/*
Copyright 2018 The Kubernetes Authors.
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.
*/
package parse
import (
"k8s.io/gengo/generator"
"k8s.io/gengo/namer"
"k8s.io/gengo/parser"
)
// NewContext returns a new Context from the builder
func NewContext(p *parser.Builder) (*generator.Context, error) {
return generator.NewContext(p, NameSystems(), DefaultNameSystem())
}
// DefaultNameSystem returns public by default.
func DefaultNameSystem() string {
return "public"
}
// NameSystems returns the name system used by the generators in this package.
// e.g. black-magic
func NameSystems() namer.NameSystems {
return namer.NameSystems{
"public": namer.NewPublicNamer(1),
"raw": namer.NewRawNamer("", nil),
}
}

View File

@@ -0,0 +1,639 @@
/*
Copyright 2018 The Kubernetes Authors.
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.
*/
package parse
import (
"bytes"
"encoding/json"
"fmt"
"log"
"regexp"
"strconv"
"strings"
"text/template"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/gengo/types"
)
// parseCRDs populates the CRD field of each Group.Version.Resource,
// creating validations using the annotations on type fields.
func (b *APIs) parseCRDs() {
for _, group := range b.APIs.Groups {
for _, version := range group.Versions {
for _, resource := range version.Resources {
if IsAPIResource(resource.Type) {
resource.JSONSchemaProps, resource.Validation =
b.typeToJSONSchemaProps(resource.Type, sets.NewString(), []string{}, true)
// Note: Drop the Type field at the root level of validation
// schema. Refer to following issue for details.
// https://github.com/kubernetes/kubernetes/issues/65293
resource.JSONSchemaProps.Type = ""
j, err := json.MarshalIndent(resource.JSONSchemaProps, "", " ")
if err != nil {
log.Fatalf("Could not Marshall validation %v\n", err)
}
resource.ValidationComments = string(j)
resource.CRD = v1beta1.CustomResourceDefinition{
TypeMeta: metav1.TypeMeta{
APIVersion: "apiextensions.k8s.io/v1beta1",
Kind: "CustomResourceDefinition",
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s.%s.%s", resource.Resource, resource.Group, resource.Domain),
Labels: map[string]string{"controller-tools.k8s.io": "1.0"},
},
Spec: v1beta1.CustomResourceDefinitionSpec{
Group: fmt.Sprintf("%s.%s", resource.Group, resource.Domain),
Version: resource.Version,
Names: v1beta1.CustomResourceDefinitionNames{
Kind: resource.Kind,
Plural: resource.Resource,
},
Validation: &v1beta1.CustomResourceValidation{
OpenAPIV3Schema: &resource.JSONSchemaProps,
},
},
}
if resource.NonNamespaced {
resource.CRD.Spec.Scope = "Cluster"
} else {
resource.CRD.Spec.Scope = "Namespaced"
}
if hasCategories(resource.Type) {
categoriesTag := getCategoriesTag(resource.Type)
categories := strings.Split(categoriesTag, ",")
resource.CRD.Spec.Names.Categories = categories
resource.Categories = categories
}
if hasSingular(resource.Type) {
singularName := getSingularName(resource.Type)
resource.CRD.Spec.Names.Singular = singularName
}
if hasStatusSubresource(resource.Type) {
if resource.CRD.Spec.Subresources == nil {
resource.CRD.Spec.Subresources = &v1beta1.CustomResourceSubresources{}
}
resource.CRD.Spec.Subresources.Status = &v1beta1.CustomResourceSubresourceStatus{}
}
resource.CRD.Status.Conditions = []v1beta1.CustomResourceDefinitionCondition{}
resource.CRD.Status.StoredVersions = []string{}
if hasScaleSubresource(resource.Type) {
if resource.CRD.Spec.Subresources == nil {
resource.CRD.Spec.Subresources = &v1beta1.CustomResourceSubresources{}
}
jsonPath, err := parseScaleParams(resource.Type)
if err != nil {
log.Fatalf("failed in parsing CRD, error: %v", err.Error())
}
resource.CRD.Spec.Subresources.Scale = &v1beta1.CustomResourceSubresourceScale{
SpecReplicasPath: jsonPath[specReplicasPath],
StatusReplicasPath: jsonPath[statusReplicasPath],
}
labelSelctor, ok := jsonPath[labelSelectorPath]
if ok && labelSelctor != "" {
resource.CRD.Spec.Subresources.Scale.LabelSelectorPath = &labelSelctor
}
}
if hasPrintColumn(resource.Type) {
result, err := parsePrintColumnParams(resource.Type)
if err != nil {
log.Fatalf("failed to parse printcolumn annotations, error: %v", err.Error())
}
resource.CRD.Spec.AdditionalPrinterColumns = result
}
if len(resource.ShortName) > 0 {
resource.CRD.Spec.Names.ShortNames = strings.Split(resource.ShortName, ";")
}
}
}
}
}
}
func (b *APIs) getTime() string {
return `v1beta1.JSONSchemaProps{
Type: "string",
Format: "date-time",
}`
}
func (b *APIs) getDuration() string {
return `v1beta1.JSONSchemaProps{
Type: "string",
}`
}
func (b *APIs) getQuantity() string {
return `v1beta1.JSONSchemaProps{
Type: "string",
}`
}
func (b *APIs) objSchema() string {
return `v1beta1.JSONSchemaProps{
Type: "object",
}`
}
// typeToJSONSchemaProps returns a JSONSchemaProps object and its serialization
// in Go that describe the JSONSchema validations for the given type.
func (b *APIs) typeToJSONSchemaProps(t *types.Type, found sets.String, comments []string, isRoot bool) (v1beta1.JSONSchemaProps, string) {
// Special cases
time := types.Name{Name: "Time", Package: "k8s.io/apimachinery/pkg/apis/meta/v1"}
duration := types.Name{Name: "Duration", Package: "k8s.io/apimachinery/pkg/apis/meta/v1"}
quantity := types.Name{Name: "Quantity", Package: "k8s.io/apimachinery/pkg/api/resource"}
meta := types.Name{Name: "ObjectMeta", Package: "k8s.io/apimachinery/pkg/apis/meta/v1"}
unstructured := types.Name{Name: "Unstructured", Package: "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"}
rawExtension := types.Name{Name: "RawExtension", Package: "k8s.io/apimachinery/pkg/runtime"}
intOrString := types.Name{Name: "IntOrString", Package: "k8s.io/apimachinery/pkg/util/intstr"}
// special types first
specialTypeProps := v1beta1.JSONSchemaProps{
Description: parseDescription(comments),
}
for _, l := range comments {
getValidation(l, &specialTypeProps)
}
switch t.Name {
case time:
specialTypeProps.Type = "string"
specialTypeProps.Format = "date-time"
return specialTypeProps, b.getTime()
case duration:
specialTypeProps.Type = "string"
return specialTypeProps, b.getDuration()
case quantity:
specialTypeProps.Type = "string"
return specialTypeProps, b.getQuantity()
case meta, unstructured, rawExtension:
specialTypeProps.Type = "object"
return specialTypeProps, b.objSchema()
case intOrString:
specialTypeProps.AnyOf = []v1beta1.JSONSchemaProps{
{
Type: "string",
},
{
Type: "integer",
},
}
return specialTypeProps, b.objSchema()
}
var v v1beta1.JSONSchemaProps
var s string
switch t.Kind {
case types.Builtin:
v, s = b.parsePrimitiveValidation(t, found, comments)
case types.Struct:
v, s = b.parseObjectValidation(t, found, comments, isRoot)
case types.Map:
v, s = b.parseMapValidation(t, found, comments)
case types.Slice:
v, s = b.parseArrayValidation(t, found, comments)
case types.Array:
v, s = b.parseArrayValidation(t, found, comments)
case types.Pointer:
v, s = b.typeToJSONSchemaProps(t.Elem, found, comments, false)
case types.Alias:
v, s = b.typeToJSONSchemaProps(t.Underlying, found, comments, false)
default:
log.Fatalf("Unknown supported Kind %v\n", t.Kind)
}
return v, s
}
var jsonRegex = regexp.MustCompile("json:\"([a-zA-Z0-9,]+)\"")
type primitiveTemplateArgs struct {
v1beta1.JSONSchemaProps
Value string
Format string
EnumValue string // TODO check type of enum value to match the type of field
Description string
}
var primitiveTemplate = template.Must(template.New("map-template").Parse(
`v1beta1.JSONSchemaProps{
{{ if .Pattern -}}
Pattern: "{{ .Pattern }}",
{{ end -}}
{{ if .Maximum -}}
Maximum: getFloat({{ .Maximum }}),
{{ end -}}
{{ if .ExclusiveMaximum -}}
ExclusiveMaximum: {{ .ExclusiveMaximum }},
{{ end -}}
{{ if .Minimum -}}
Minimum: getFloat({{ .Minimum }}),
{{ end -}}
{{ if .ExclusiveMinimum -}}
ExclusiveMinimum: {{ .ExclusiveMinimum }},
{{ end -}}
Type: "{{ .Value }}",
{{ if .Format -}}
Format: "{{ .Format }}",
{{ end -}}
{{ if .EnumValue -}}
Enum: {{ .EnumValue }},
{{ end -}}
{{ if .MaxLength -}}
MaxLength: getInt({{ .MaxLength }}),
{{ end -}}
{{ if .MinLength -}}
MinLength: getInt({{ .MinLength }}),
{{ end -}}
}`))
// parsePrimitiveValidation returns a JSONSchemaProps object and its
// serialization in Go that describe the validations for the given primitive
// type.
func (b *APIs) parsePrimitiveValidation(t *types.Type, found sets.String, comments []string) (v1beta1.JSONSchemaProps, string) {
props := v1beta1.JSONSchemaProps{Type: string(t.Name.Name)}
for _, l := range comments {
getValidation(l, &props)
}
buff := &bytes.Buffer{}
var n, f, s, d string
switch t.Name.Name {
case "int", "int64", "uint64":
n = "integer"
f = "int64"
case "int32", "uint32":
n = "integer"
f = "int32"
case "float", "float32":
n = "number"
f = "float"
case "float64":
n = "number"
f = "double"
case "bool":
n = "boolean"
case "string":
n = "string"
f = props.Format
default:
n = t.Name.Name
}
if props.Enum != nil {
s = parseEnumToString(props.Enum)
}
d = parseDescription(comments)
if err := primitiveTemplate.Execute(buff, primitiveTemplateArgs{props, n, f, s, d}); err != nil {
log.Fatalf("%v", err)
}
props.Type = n
props.Format = f
props.Description = d
return props, buff.String()
}
type mapTempateArgs struct {
Result string
SkipMapValidation bool
}
var mapTemplate = template.Must(template.New("map-template").Parse(
`v1beta1.JSONSchemaProps{
Type: "object",
{{if not .SkipMapValidation}}AdditionalProperties: &v1beta1.JSONSchemaPropsOrBool{
Allows: true,
Schema: &{{.Result}},
},{{end}}
}`))
// parseMapValidation returns a JSONSchemaProps object and its serialization in
// Go that describe the validations for the given map type.
func (b *APIs) parseMapValidation(t *types.Type, found sets.String, comments []string) (v1beta1.JSONSchemaProps, string) {
additionalProps, result := b.typeToJSONSchemaProps(t.Elem, found, comments, false)
additionalProps.Description = ""
props := v1beta1.JSONSchemaProps{
Type: "object",
Description: parseDescription(comments),
}
parseOption := b.arguments.CustomArgs.(*Options)
if !parseOption.SkipMapValidation {
props.AdditionalProperties = &v1beta1.JSONSchemaPropsOrBool{
Allows: true,
Schema: &additionalProps}
}
for _, l := range comments {
getValidation(l, &props)
}
buff := &bytes.Buffer{}
if err := mapTemplate.Execute(buff, mapTempateArgs{Result: result, SkipMapValidation: parseOption.SkipMapValidation}); err != nil {
log.Fatalf("%v", err)
}
return props, buff.String()
}
var arrayTemplate = template.Must(template.New("array-template").Parse(
`v1beta1.JSONSchemaProps{
Type: "{{.Type}}",
{{ if .Format -}}
Format: "{{.Format}}",
{{ end -}}
{{ if .MaxItems -}}
MaxItems: getInt({{ .MaxItems }}),
{{ end -}}
{{ if .MinItems -}}
MinItems: getInt({{ .MinItems }}),
{{ end -}}
{{ if .UniqueItems -}}
UniqueItems: {{ .UniqueItems }},
{{ end -}}
{{ if .Items -}}
Items: &v1beta1.JSONSchemaPropsOrArray{
Schema: &{{.ItemsSchema}},
},
{{ end -}}
}`))
type arrayTemplateArgs struct {
v1beta1.JSONSchemaProps
ItemsSchema string
}
// parseArrayValidation returns a JSONSchemaProps object and its serialization in
// Go that describe the validations for the given array type.
func (b *APIs) parseArrayValidation(t *types.Type, found sets.String, comments []string) (v1beta1.JSONSchemaProps, string) {
items, result := b.typeToJSONSchemaProps(t.Elem, found, comments, false)
items.Description = ""
props := v1beta1.JSONSchemaProps{
Type: "array",
Items: &v1beta1.JSONSchemaPropsOrArray{Schema: &items},
Description: parseDescription(comments),
}
// To represent byte arrays in the generated code, the property of the OpenAPI definition
// should have string as its type and byte as its format.
if t.Name.Name == "[]byte" {
props.Type = "string"
props.Format = "byte"
props.Items = nil
props.Description = parseDescription(comments)
}
for _, l := range comments {
getValidation(l, &props)
}
if t.Name.Name != "[]byte" {
// Except for the byte array special case above, the "format" property
// should be applied to the array items and not the array itself.
props.Format = ""
}
buff := &bytes.Buffer{}
if err := arrayTemplate.Execute(buff, arrayTemplateArgs{props, result}); err != nil {
log.Fatalf("%v", err)
}
return props, buff.String()
}
type objectTemplateArgs struct {
v1beta1.JSONSchemaProps
Fields map[string]string
Required []string
IsRoot bool
}
var objectTemplate = template.Must(template.New("object-template").Parse(
`v1beta1.JSONSchemaProps{
{{ if not .IsRoot -}}
Type: "object",
{{ end -}}
Properties: map[string]v1beta1.JSONSchemaProps{
{{ range $k, $v := .Fields -}}
"{{ $k }}": {{ $v }},
{{ end -}}
},
{{if .Required}}Required: []string{
{{ range $k, $v := .Required -}}
"{{ $v }}",
{{ end -}}
},{{ end -}}
}`))
// parseObjectValidation returns a JSONSchemaProps object and its serialization in
// Go that describe the validations for the given object type.
func (b *APIs) parseObjectValidation(t *types.Type, found sets.String, comments []string, isRoot bool) (v1beta1.JSONSchemaProps, string) {
buff := &bytes.Buffer{}
props := v1beta1.JSONSchemaProps{
Type: "object",
Description: parseDescription(comments),
}
for _, l := range comments {
getValidation(l, &props)
}
if strings.HasPrefix(t.Name.String(), "k8s.io/api") {
if err := objectTemplate.Execute(buff, objectTemplateArgs{props, nil, nil, false}); err != nil {
log.Fatalf("%v", err)
}
} else {
m, result, required := b.getMembers(t, found)
props.Properties = m
props.Required = required
if err := objectTemplate.Execute(buff, objectTemplateArgs{props, result, required, isRoot}); err != nil {
log.Fatalf("%v", err)
}
}
return props, buff.String()
}
// getValidation parses the validation tags from the comment and sets the
// validation rules on the given JSONSchemaProps.
func getValidation(comment string, props *v1beta1.JSONSchemaProps) {
comment = strings.TrimLeft(comment, " ")
if !strings.HasPrefix(comment, "+kubebuilder:validation:") {
return
}
c := strings.Replace(comment, "+kubebuilder:validation:", "", -1)
parts := strings.Split(c, "=")
if len(parts) != 2 {
log.Fatalf("Expected +kubebuilder:validation:<key>=<value> actual: %s", comment)
return
}
switch parts[0] {
case "Maximum":
f, err := strconv.ParseFloat(parts[1], 64)
if err != nil {
log.Fatalf("Could not parse float from %s: %v", comment, err)
return
}
props.Maximum = &f
case "ExclusiveMaximum":
b, err := strconv.ParseBool(parts[1])
if err != nil {
log.Fatalf("Could not parse bool from %s: %v", comment, err)
return
}
props.ExclusiveMaximum = b
case "Minimum":
f, err := strconv.ParseFloat(parts[1], 64)
if err != nil {
log.Fatalf("Could not parse float from %s: %v", comment, err)
return
}
props.Minimum = &f
case "ExclusiveMinimum":
b, err := strconv.ParseBool(parts[1])
if err != nil {
log.Fatalf("Could not parse bool from %s: %v", comment, err)
return
}
props.ExclusiveMinimum = b
case "MaxLength":
i, err := strconv.Atoi(parts[1])
v := int64(i)
if err != nil {
log.Fatalf("Could not parse int from %s: %v", comment, err)
return
}
props.MaxLength = &v
case "MinLength":
i, err := strconv.Atoi(parts[1])
v := int64(i)
if err != nil {
log.Fatalf("Could not parse int from %s: %v", comment, err)
return
}
props.MinLength = &v
case "Pattern":
props.Pattern = parts[1]
case "MaxItems":
if props.Type == "array" {
i, err := strconv.Atoi(parts[1])
v := int64(i)
if err != nil {
log.Fatalf("Could not parse int from %s: %v", comment, err)
return
}
props.MaxItems = &v
}
case "MinItems":
if props.Type == "array" {
i, err := strconv.Atoi(parts[1])
v := int64(i)
if err != nil {
log.Fatalf("Could not parse int from %s: %v", comment, err)
return
}
props.MinItems = &v
}
case "UniqueItems":
if props.Type == "array" {
b, err := strconv.ParseBool(parts[1])
if err != nil {
log.Fatalf("Could not parse bool from %s: %v", comment, err)
return
}
props.UniqueItems = b
}
case "MultipleOf":
f, err := strconv.ParseFloat(parts[1], 64)
if err != nil {
log.Fatalf("Could not parse float from %s: %v", comment, err)
return
}
props.MultipleOf = &f
case "Enum":
if props.Type != "array" {
value := strings.Split(parts[1], ",")
enums := []v1beta1.JSON{}
for _, s := range value {
checkType(props, s, &enums)
}
props.Enum = enums
}
case "Format":
props.Format = parts[1]
default:
log.Fatalf("Unsupport validation: %s", comment)
}
}
// getMembers builds maps by field name of the JSONSchemaProps and their Go
// serializations.
func (b *APIs) getMembers(t *types.Type, found sets.String) (map[string]v1beta1.JSONSchemaProps, map[string]string, []string) {
members := map[string]v1beta1.JSONSchemaProps{}
result := map[string]string{}
required := []string{}
// Don't allow recursion until we support it through refs
// TODO: Support recursion
if found.Has(t.Name.String()) {
fmt.Printf("Breaking recursion for type %s", t.Name.String())
return members, result, required
}
found.Insert(t.Name.String())
for _, member := range t.Members {
tags := jsonRegex.FindStringSubmatch(member.Tags)
if len(tags) == 0 {
// Skip fields without json tags
//fmt.Printf("Skipping member %s %s\n", member.Name, member.Type.Name.String())
continue
}
ts := strings.Split(tags[1], ",")
name := member.Name
strat := ""
if len(ts) > 0 && len(ts[0]) > 0 {
name = ts[0]
}
if len(ts) > 1 {
strat = ts[1]
}
// Inline "inline" structs
if strat == "inline" {
m, r, re := b.getMembers(member.Type, found)
for n, v := range m {
members[n] = v
}
for n, v := range r {
result[n] = v
}
required = append(required, re...)
} else {
m, r := b.typeToJSONSchemaProps(member.Type, found, member.CommentLines, false)
members[name] = m
result[name] = r
if !strings.HasSuffix(strat, "omitempty") {
required = append(required, name)
}
}
}
defer found.Delete(t.Name.String())
return members, result, required
}

View File

@@ -0,0 +1,161 @@
/*
Copyright 2018 The Kubernetes Authors.
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.
*/
package parse
import (
"fmt"
"log"
"strings"
"github.com/gobuffalo/flect"
"k8s.io/gengo/types"
"sigs.k8s.io/controller-tools/pkg/internal/codegen"
"sigs.k8s.io/controller-tools/pkg/internal/general"
)
// parseIndex indexes all types with the comment "// +resource=RESOURCE" by GroupVersionKind and
// GroupKindVersion
func (b *APIs) parseIndex() {
// Index resource by group, version, kind
b.ByGroupVersionKind = map[string]map[string]map[string]*codegen.APIResource{}
// Index resources by group, kind, version
b.ByGroupKindVersion = map[string]map[string]map[string]*codegen.APIResource{}
// Index subresources by group, version, kind
b.SubByGroupVersionKind = map[string]map[string]map[string]*types.Type{}
for _, c := range b.context.Order {
// The type is a subresource, add it to the subresource index
if IsAPISubresource(c) {
group := GetGroup(c)
version := GetVersion(c, group)
kind := GetKind(c, group)
if _, f := b.SubByGroupVersionKind[group]; !f {
b.SubByGroupVersionKind[group] = map[string]map[string]*types.Type{}
}
if _, f := b.SubByGroupVersionKind[group][version]; !f {
b.SubByGroupVersionKind[group][version] = map[string]*types.Type{}
}
b.SubByGroupVersionKind[group][version][kind] = c
}
// If it isn't a subresource or resource, continue to the next type
if !IsAPIResource(c) {
continue
}
// Parse out the resource information
r := &codegen.APIResource{
Type: c,
NonNamespaced: IsNonNamespaced(c),
}
r.Group = GetGroup(c)
r.Version = GetVersion(c, r.Group)
r.Kind = GetKind(c, r.Group)
r.Domain = b.Domain
// TODO: revisit the part...
if r.Resource == "" {
r.Resource = flect.Pluralize(strings.ToLower(r.Kind))
}
rt, err := parseResourceAnnotation(c)
if err != nil {
log.Fatalf("failed to parse resource annotations, error: %v", err.Error())
}
if rt.Resource != "" {
r.Resource = rt.Resource
}
r.ShortName = rt.ShortName
// Copy the Status strategy to mirror the non-status strategy
r.StatusStrategy = strings.TrimSuffix(r.Strategy, "Strategy")
r.StatusStrategy = fmt.Sprintf("%sStatusStrategy", r.StatusStrategy)
// Initialize the map entries so they aren't nill
if _, f := b.ByGroupKindVersion[r.Group]; !f {
b.ByGroupKindVersion[r.Group] = map[string]map[string]*codegen.APIResource{}
}
if _, f := b.ByGroupKindVersion[r.Group][r.Kind]; !f {
b.ByGroupKindVersion[r.Group][r.Kind] = map[string]*codegen.APIResource{}
}
if _, f := b.ByGroupVersionKind[r.Group]; !f {
b.ByGroupVersionKind[r.Group] = map[string]map[string]*codegen.APIResource{}
}
if _, f := b.ByGroupVersionKind[r.Group][r.Version]; !f {
b.ByGroupVersionKind[r.Group][r.Version] = map[string]*codegen.APIResource{}
}
// Add the resource to the map
b.ByGroupKindVersion[r.Group][r.Kind][r.Version] = r
b.ByGroupVersionKind[r.Group][r.Version][r.Kind] = r
r.Type = c
}
}
// resourceTags contains the tags present in a "+resource=" comment
type resourceTags struct {
Resource string
REST string
Strategy string
ShortName string
}
// resourceAnnotationValue is a helper function to extract resource annotation.
func resourceAnnotationValue(tag string) (resourceTags, error) {
res := resourceTags{}
for _, elem := range strings.Split(tag, ",") {
key, value, err := general.ParseKV(elem)
if err != nil {
return resourceTags{}, fmt.Errorf("// +kubebuilder:resource: tags must be key value pairs. Expected "+
"keys [path=<resourcepath>] "+
"Got string: [%s]", tag)
}
switch key {
case "path":
res.Resource = value
case "shortName":
res.ShortName = value
default:
return resourceTags{}, fmt.Errorf("The given input %s is invalid", value)
}
}
return res, nil
}
// parseResourceAnnotation parses the tags in a "+resource=" comment into a resourceTags struct.
func parseResourceAnnotation(t *types.Type) (resourceTags, error) {
finalResult := resourceTags{}
var resourceAnnotationFound bool
for _, comment := range t.CommentLines {
anno := general.GetAnnotation(comment, "kubebuilder:resource")
if len(anno) == 0 {
continue
}
result, err := resourceAnnotationValue(anno)
if err != nil {
return resourceTags{}, err
}
if resourceAnnotationFound {
return resourceTags{}, fmt.Errorf("resource annotation should only exists once per type")
}
resourceAnnotationFound = true
finalResult = result
}
return finalResult, nil
}

View File

@@ -0,0 +1,151 @@
/*
Copyright 2018 The Kubernetes Authors.
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.
*/
package parse
import (
"bufio"
"go/build"
"log"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/gengo/args"
"k8s.io/gengo/generator"
"k8s.io/gengo/types"
"sigs.k8s.io/controller-tools/pkg/internal/codegen"
)
// APIs is the information of a collection of API
type APIs struct {
context *generator.Context
arguments *args.GeneratorArgs
Domain string
VersionedPkgs sets.String
UnversionedPkgs sets.String
APIsPkg string
APIsPkgRaw *types.Package
GroupNames sets.String
APIs *codegen.APIs
Controllers []codegen.Controller
ByGroupKindVersion map[string]map[string]map[string]*codegen.APIResource
ByGroupVersionKind map[string]map[string]map[string]*codegen.APIResource
SubByGroupVersionKind map[string]map[string]map[string]*types.Type
Groups map[string]types.Package
Rules []rbacv1.PolicyRule
Informers map[v1.GroupVersionKind]bool
}
// NewAPIs returns a new APIs instance with given context.
func NewAPIs(context *generator.Context, arguments *args.GeneratorArgs, domain, apisPkg string) *APIs {
b := &APIs{
context: context,
arguments: arguments,
Domain: domain,
APIsPkg: apisPkg,
}
b.parsePackages()
b.parseGroupNames()
b.parseIndex()
b.parseAPIs()
b.parseCRDs()
if len(b.Domain) == 0 {
b.parseDomain()
}
return b
}
// parseGroupNames initializes b.GroupNames with the set of all groups
func (b *APIs) parseGroupNames() {
b.GroupNames = sets.String{}
for p := range b.UnversionedPkgs {
pkg := b.context.Universe[p]
if pkg == nil {
// If the input had no Go files, for example.
continue
}
b.GroupNames.Insert(filepath.Base(p))
}
}
// parsePackages parses out the sets of Versioned, Unversioned packages and identifies the root Apis package.
func (b *APIs) parsePackages() {
b.VersionedPkgs = sets.NewString()
b.UnversionedPkgs = sets.NewString()
for _, o := range b.context.Order {
if IsAPIResource(o) {
versioned := o.Name.Package
b.VersionedPkgs.Insert(versioned)
unversioned := filepath.Dir(versioned)
b.UnversionedPkgs.Insert(unversioned)
}
}
}
// parseDomain parses the domain from the apis/doc.go file comment "// +domain=YOUR_DOMAIN".
func (b *APIs) parseDomain() {
pkg := b.context.Universe[b.APIsPkg]
if pkg == nil {
// If the input had no Go files, for example.
panic(errors.Errorf("Missing apis package."))
}
comments := Comments(pkg.Comments)
b.Domain = comments.getTag("domain", "=")
if len(b.Domain) == 0 {
b.Domain = parseDomainFromFiles(b.context.Inputs)
if len(b.Domain) == 0 {
panic("Could not find string matching // +domain=.+ in apis/doc.go")
}
}
}
func parseDomainFromFiles(paths []string) string {
var domain string
for _, path := range paths {
if strings.HasSuffix(path, "pkg/apis") {
filePath := strings.Join([]string{build.Default.GOPATH, "src", path, "doc.go"}, "/")
lines := []string{}
file, err := os.Open(filePath)
if err != nil {
log.Fatal(err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "//") {
lines = append(lines, strings.Replace(scanner.Text(), "// ", "", 1))
}
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
comments := Comments(lines)
domain = comments.getTag("domain", "=")
break
}
}
return domain
}

View File

@@ -0,0 +1,539 @@
/*
Copyright 2018 The Kubernetes Authors.
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.
*/
package parse
import (
"fmt"
"log"
"path/filepath"
"strconv"
"strings"
"github.com/pkg/errors"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/gengo/types"
)
const (
specReplicasPath = "specpath"
statusReplicasPath = "statuspath"
labelSelectorPath = "selectorpath"
jsonPathError = "invalid scale path. specpath, statuspath key-value pairs are required, only selectorpath key-value is optinal. For example: // +kubebuilder:subresource:scale:specpath=.spec.replica,statuspath=.status.replica,selectorpath=.spec.Label"
printColumnName = "name"
printColumnType = "type"
printColumnDescr = "description"
printColumnPath = "JSONPath"
printColumnFormat = "format"
printColumnPri = "priority"
printColumnError = "invalid printcolumn path. name,type, and JSONPath are required kye-value pairs and rest of the fields are optinal. For example: // +kubebuilder:printcolumn:name=abc,type=string,JSONPath=status"
)
// Options contains the parser options
type Options struct {
SkipMapValidation bool
// SkipRBACValidation flag determines whether to check RBAC annotations
// for the controller or not at parse stage.
SkipRBACValidation bool
}
// IsAPIResource returns true if either of the two conditions become true:
// 1. t has a +resource/+kubebuilder:resource comment tag
// 2. t has TypeMeta and ObjectMeta in its member list.
func IsAPIResource(t *types.Type) bool {
for _, c := range t.CommentLines {
if strings.Contains(c, "+resource") || strings.Contains(c, "+kubebuilder:resource") {
return true
}
}
typeMetaFound, objMetaFound := false, false
for _, m := range t.Members {
if m.Name == "TypeMeta" && m.Type.String() == "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta" {
typeMetaFound = true
}
if m.Name == "ObjectMeta" && m.Type.String() == "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta" {
objMetaFound = true
}
if typeMetaFound && objMetaFound {
return true
}
}
return false
}
// IsNonNamespaced returns true if t has a +nonNamespaced comment tag
func IsNonNamespaced(t *types.Type) bool {
if !IsAPIResource(t) {
return false
}
for _, c := range t.CommentLines {
if strings.Contains(c, "+genclient:nonNamespaced") {
return true
}
}
for _, c := range t.SecondClosestCommentLines {
if strings.Contains(c, "+genclient:nonNamespaced") {
return true
}
}
return false
}
// IsController returns true if t has a +controller or +kubebuilder:controller tag
func IsController(t *types.Type) bool {
for _, c := range t.CommentLines {
if strings.Contains(c, "+controller") || strings.Contains(c, "+kubebuilder:controller") {
return true
}
}
return false
}
// IsRBAC returns true if t has a +rbac or +kubebuilder:rbac tag
func IsRBAC(t *types.Type) bool {
for _, c := range t.CommentLines {
if strings.Contains(c, "+rbac") || strings.Contains(c, "+kubebuilder:rbac") {
return true
}
}
return false
}
// hasPrintColumn returns true if t has a +printcolumn or +kubebuilder:printcolumn annotation.
func hasPrintColumn(t *types.Type) bool {
for _, c := range t.CommentLines {
if strings.Contains(c, "+printcolumn") || strings.Contains(c, "+kubebuilder:printcolumn") {
return true
}
}
return false
}
// IsInformer returns true if t has a +informers or +kubebuilder:informers tag
func IsInformer(t *types.Type) bool {
for _, c := range t.CommentLines {
if strings.Contains(c, "+informers") || strings.Contains(c, "+kubebuilder:informers") {
return true
}
}
return false
}
// IsAPISubresource returns true if t has a +subresource-request comment tag
func IsAPISubresource(t *types.Type) bool {
for _, c := range t.CommentLines {
if strings.Contains(c, "+subresource-request") {
return true
}
}
return false
}
// HasSubresource returns true if t is an APIResource with one or more Subresources
func HasSubresource(t *types.Type) bool {
if !IsAPIResource(t) {
return false
}
for _, c := range t.CommentLines {
if strings.Contains(c, "subresource") {
return true
}
}
return false
}
// hasStatusSubresource returns true if t is an APIResource annotated with
// +kubebuilder:subresource:status
func hasStatusSubresource(t *types.Type) bool {
if !IsAPIResource(t) {
return false
}
for _, c := range t.CommentLines {
if strings.Contains(c, "+kubebuilder:subresource:status") {
return true
}
}
return false
}
// hasScaleSubresource returns true if t is an APIResource annotated with
// +kubebuilder:subresource:scale
func hasScaleSubresource(t *types.Type) bool {
if !IsAPIResource(t) {
return false
}
for _, c := range t.CommentLines {
if strings.Contains(c, "+kubebuilder:subresource:scale") {
return true
}
}
return false
}
// hasCategories returns true if t is an APIResource annotated with
// +kubebuilder:categories
func hasCategories(t *types.Type) bool {
if !IsAPIResource(t) {
return false
}
for _, c := range t.CommentLines {
if strings.Contains(c, "+kubebuilder:categories") {
return true
}
}
return false
}
// HasDocAnnotation returns true if t is an APIResource with doc annotation
// +kubebuilder:doc
func HasDocAnnotation(t *types.Type) bool {
if !IsAPIResource(t) {
return false
}
for _, c := range t.CommentLines {
if strings.Contains(c, "+kubebuilder:doc") {
return true
}
}
return false
}
// hasSingular returns true if t is an APIResource annotated with
// +kubebuilder:singular
func hasSingular(t *types.Type) bool {
if !IsAPIResource(t) {
return false
}
for _, c := range t.CommentLines{
if strings.Contains(c, "+kubebuilder:singular"){
return true
}
}
return false
}
// IsUnversioned returns true if t is in given group, and not in versioned path.
func IsUnversioned(t *types.Type, group string) bool {
return IsApisDir(filepath.Base(filepath.Dir(t.Name.Package))) && GetGroup(t) == group
}
// IsVersioned returns true if t is in given group, and in versioned path.
func IsVersioned(t *types.Type, group string) bool {
dir := filepath.Base(filepath.Dir(filepath.Dir(t.Name.Package)))
return IsApisDir(dir) && GetGroup(t) == group
}
// GetVersion returns version of t.
func GetVersion(t *types.Type, group string) string {
if !IsVersioned(t, group) {
panic(errors.Errorf("Cannot get version for unversioned type %v", t.Name))
}
return filepath.Base(t.Name.Package)
}
// GetGroup returns group of t.
func GetGroup(t *types.Type) string {
return filepath.Base(GetGroupPackage(t))
}
// GetGroupPackage returns group package of t.
func GetGroupPackage(t *types.Type) string {
if IsApisDir(filepath.Base(filepath.Dir(t.Name.Package))) {
return t.Name.Package
}
return filepath.Dir(t.Name.Package)
}
// GetKind returns kind of t.
func GetKind(t *types.Type, group string) string {
if !IsVersioned(t, group) && !IsUnversioned(t, group) {
panic(errors.Errorf("Cannot get kind for type not in group %v", t.Name))
}
return t.Name.Name
}
// IsApisDir returns true if a directory path is a Kubernetes api directory
func IsApisDir(dir string) bool {
return dir == "apis" || dir == "api"
}
// Comments is a structure for using comment tags on go structs and fields
type Comments []string
// GetTags returns the value for the first comment with a prefix matching "+name="
// e.g. "+name=foo\n+name=bar" would return "foo"
func (c Comments) getTag(name, sep string) string {
for _, c := range c {
prefix := fmt.Sprintf("+%s%s", name, sep)
if strings.HasPrefix(c, prefix) {
return strings.Replace(c, prefix, "", 1)
}
}
return ""
}
// hasTag returns true if the Comments has a tag with the given name
func (c Comments) hasTag(name string) bool {
for _, c := range c {
prefix := fmt.Sprintf("+%s", name)
if strings.HasPrefix(c, prefix) {
return true
}
}
return false
}
// GetTags returns the value for all comments with a prefix and separator. E.g. for "name" and "="
// "+name=foo\n+name=bar" would return []string{"foo", "bar"}
func (c Comments) getTags(name, sep string) []string {
tags := []string{}
for _, c := range c {
prefix := fmt.Sprintf("+%s%s", name, sep)
if strings.HasPrefix(c, prefix) {
tags = append(tags, strings.Replace(c, prefix, "", 1))
}
}
return tags
}
// getCategoriesTag returns the value of the +kubebuilder:categories tags
func getCategoriesTag(c *types.Type) string {
comments := Comments(c.CommentLines)
resource := comments.getTag("kubebuilder:categories", "=")
if len(resource) == 0 {
panic(errors.Errorf("Must specify +kubebuilder:categories comment for type %v", c.Name))
}
return resource
}
// getSingularName returns the value of the +kubebuilder:singular tag
func getSingularName(c *types.Type) string {
comments := Comments(c.CommentLines)
singular := comments.getTag("kubebuilder:singular", "=")
if len(singular) == 0 {
panic(errors.Errorf("Must specify a value to use with +kubebuilder:singular comment for type %v", c.Name))
}
return singular
}
// getDocAnnotation parse annotations of "+kubebuilder:doc:" with tags of "warning" or "doc" for control generating doc config.
// E.g. +kubebuilder:doc:warning=foo +kubebuilder:doc:note=bar
func getDocAnnotation(t *types.Type, tags ...string) map[string]string {
annotation := make(map[string]string)
for _, tag := range tags {
for _, c := range t.CommentLines {
prefix := fmt.Sprintf("+kubebuilder:doc:%s=", tag)
if strings.HasPrefix(c, prefix) {
annotation[tag] = strings.Replace(c, prefix, "", 1)
}
}
}
return annotation
}
// parseByteValue returns the literal digital number values from a byte array
func parseByteValue(b []byte) string {
elem := strings.Join(strings.Fields(fmt.Sprintln(b)), ",")
elem = strings.TrimPrefix(elem, "[")
elem = strings.TrimSuffix(elem, "]")
return elem
}
// parseDescription parse comments above each field in the type definition.
func parseDescription(res []string) string {
var temp strings.Builder
var desc string
for _, comment := range res {
if !(strings.Contains(comment, "+kubebuilder") || strings.Contains(comment, "+optional")) {
temp.WriteString(comment)
temp.WriteString(" ")
desc = strings.TrimRight(temp.String(), " ")
}
}
return desc
}
// parseEnumToString returns a representive validated go format string from JSONSchemaProps schema
func parseEnumToString(value []v1beta1.JSON) string {
res := "[]v1beta1.JSON{"
prefix := "v1beta1.JSON{[]byte{"
for _, v := range value {
res = res + prefix + parseByteValue(v.Raw) + "}},"
}
return strings.TrimSuffix(res, ",") + "}"
}
// check type of enum element value to match type of field
func checkType(props *v1beta1.JSONSchemaProps, s string, enums *[]v1beta1.JSON) {
// TODO support more types check
switch props.Type {
case "int", "int64", "uint64":
if _, err := strconv.ParseInt(s, 0, 64); err != nil {
log.Fatalf("Invalid integer value [%v] for a field of integer type", s)
}
*enums = append(*enums, v1beta1.JSON{Raw: []byte(fmt.Sprintf("%v", s))})
case "int32", "unit32":
if _, err := strconv.ParseInt(s, 0, 32); err != nil {
log.Fatalf("Invalid integer value [%v] for a field of integer32 type", s)
}
*enums = append(*enums, v1beta1.JSON{Raw: []byte(fmt.Sprintf("%v", s))})
case "float", "float32":
if _, err := strconv.ParseFloat(s, 32); err != nil {
log.Fatalf("Invalid float value [%v] for a field of float32 type", s)
}
*enums = append(*enums, v1beta1.JSON{Raw: []byte(fmt.Sprintf("%v", s))})
case "float64":
if _, err := strconv.ParseFloat(s, 64); err != nil {
log.Fatalf("Invalid float value [%v] for a field of float type", s)
}
*enums = append(*enums, v1beta1.JSON{Raw: []byte(fmt.Sprintf("%v", s))})
case "string":
*enums = append(*enums, v1beta1.JSON{Raw: []byte(`"` + s + `"`)})
}
}
// Scale subresource requires specpath, statuspath, selectorpath key values, represents for JSONPath of
// SpecReplicasPath, StatusReplicasPath, LabelSelectorPath separately. e.g.
// +kubebuilder:subresource:scale:specpath=.spec.replica,statuspath=.status.replica,selectorpath=
func parseScaleParams(t *types.Type) (map[string]string, error) {
jsonPath := make(map[string]string)
for _, c := range t.CommentLines {
if strings.Contains(c, "+kubebuilder:subresource:scale") {
paths := strings.Replace(c, "+kubebuilder:subresource:scale:", "", -1)
path := strings.Split(paths, ",")
if len(path) < 2 {
return nil, fmt.Errorf(jsonPathError)
}
for _, s := range path {
kv := strings.Split(s, "=")
if kv[0] == specReplicasPath || kv[0] == statusReplicasPath || kv[0] == labelSelectorPath {
jsonPath[kv[0]] = kv[1]
} else {
return nil, fmt.Errorf(jsonPathError)
}
}
var ok bool
_, ok = jsonPath[specReplicasPath]
if !ok {
return nil, fmt.Errorf(jsonPathError)
}
_, ok = jsonPath[statusReplicasPath]
if !ok {
return nil, fmt.Errorf(jsonPathError)
}
return jsonPath, nil
}
}
return nil, fmt.Errorf(jsonPathError)
}
// printColumnKV parses key-value string formatted as "foo=bar" and returns key and value.
func printColumnKV(s string) (key, value string, err error) {
kv := strings.SplitN(s, "=", 2)
if len(kv) != 2 {
err = fmt.Errorf("invalid key value pair")
return key, value, err
}
key, value = kv[0], kv[1]
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
value = value[1 : len(value)-1]
}
return key, value, err
}
// helperPrintColumn is a helper function for the parsePrintColumnParams to compute printer columns.
func helperPrintColumn(parts string, comment string) (v1beta1.CustomResourceColumnDefinition, error) {
config := v1beta1.CustomResourceColumnDefinition{}
var count int
part := strings.Split(parts, ",")
if len(part) < 3 {
return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf(printColumnError)
}
for _, elem := range strings.Split(parts, ",") {
key, value, err := printColumnKV(elem)
if err != nil {
return v1beta1.CustomResourceColumnDefinition{},
fmt.Errorf("//+kubebuilder:printcolumn: tags must be key value pairs.Expected "+
"keys [name=<name>,type=<type>,description=<descr>,format=<format>] "+
"Got string: [%s]", parts)
}
if key == printColumnName || key == printColumnType || key == printColumnPath {
count++
}
switch key {
case printColumnName:
config.Name = value
case printColumnType:
if value == "integer" || value == "number" || value == "string" || value == "boolean" || value == "date" {
config.Type = value
} else {
return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf("invalid value for %s printcolumn", printColumnType)
}
case printColumnFormat:
if config.Type == "integer" && (value == "int32" || value == "int64") {
config.Format = value
} else if config.Type == "number" && (value == "float" || value == "double") {
config.Format = value
} else if config.Type == "string" && (value == "byte" || value == "date" || value == "date-time" || value == "password") {
config.Format = value
} else {
return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf("invalid value for %s printcolumn", printColumnFormat)
}
case printColumnPath:
config.JSONPath = value
case printColumnPri:
i, err := strconv.Atoi(value)
v := int32(i)
if err != nil {
return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf("invalid value for %s printcolumn", printColumnPri)
}
config.Priority = v
case printColumnDescr:
config.Description = value
default:
return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf(printColumnError)
}
}
if count != 3 {
return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf(printColumnError)
}
return config, nil
}
// printcolumn requires name,type,JSONPath fields and rest of the field are optional
// +kubebuilder:printcolumn:name=<name>,type=<type>,description=<desc>,JSONPath:<.spec.Name>,priority=<int32>,format=<format>
func parsePrintColumnParams(t *types.Type) ([]v1beta1.CustomResourceColumnDefinition, error) {
result := []v1beta1.CustomResourceColumnDefinition{}
for _, comment := range t.CommentLines {
if strings.Contains(comment, "+kubebuilder:printcolumn") {
parts := strings.Replace(comment, "+kubebuilder:printcolumn:", "", -1)
res, err := helperPrintColumn(parts, comment)
if err != nil {
return []v1beta1.CustomResourceColumnDefinition{}, err
}
result = append(result, res)
}
}
return result, nil
}