| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- /*
- 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 crd
- import (
- "fmt"
- "go/ast"
- "go/types"
- "sort"
- "strings"
- apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
- "k8s.io/apimachinery/pkg/runtime/schema"
- crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers"
- "sigs.k8s.io/controller-tools/pkg/genall"
- "sigs.k8s.io/controller-tools/pkg/loader"
- "sigs.k8s.io/controller-tools/pkg/markers"
- "sigs.k8s.io/controller-tools/pkg/version"
- )
- // The identifier for v1 CustomResourceDefinitions.
- const v1 = "v1"
- // The default CustomResourceDefinition version to generate.
- const defaultVersion = v1
- // +controllertools:marker:generateHelp
- // Generator generates CustomResourceDefinition objects.
- type Generator struct {
- // IgnoreUnexportedFields indicates that we should skip unexported fields.
- //
- // Left unspecified, the default is false.
- IgnoreUnexportedFields *bool `marker:",optional"`
- // AllowDangerousTypes allows types which are usually omitted from CRD generation
- // because they are not recommended.
- //
- // Currently the following additional types are allowed when this is true:
- // float32
- // float64
- //
- // Left unspecified, the default is false
- AllowDangerousTypes *bool `marker:",optional"`
- // MaxDescLen specifies the maximum description length for fields in CRD's OpenAPI schema.
- //
- // 0 indicates drop the description for all fields completely.
- // n indicates limit the description to at most n characters and truncate the description to
- // closest sentence boundary if it exceeds n characters.
- MaxDescLen *int `marker:",optional"`
- // CRDVersions specifies the target API versions of the CRD type itself to
- // generate. Defaults to v1.
- //
- // Currently, the only supported value is v1.
- //
- // The first version listed will be assumed to be the "default" version and
- // will not get a version suffix in the output filename.
- //
- // You'll need to use "v1" to get support for features like defaulting,
- // along with an API server that supports it (Kubernetes 1.16+).
- CRDVersions []string `marker:"crdVersions,optional"`
- // GenerateEmbeddedObjectMeta specifies if any embedded ObjectMeta in the CRD should be generated
- GenerateEmbeddedObjectMeta *bool `marker:",optional"`
- // HeaderFile specifies the header text (e.g. license) to prepend to generated files.
- HeaderFile string `marker:",optional"`
- // Year specifies the year to substitute for " YEAR" in the header file.
- Year string `marker:",optional"`
- }
- func (Generator) CheckFilter() loader.NodeFilter {
- return filterTypesForCRDs
- }
- func (Generator) RegisterMarkers(into *markers.Registry) error {
- return crdmarkers.Register(into)
- }
- // transformRemoveCRDStatus ensures we do not write the CRD status field.
- func transformRemoveCRDStatus(obj map[string]interface{}) error {
- delete(obj, "status")
- return nil
- }
- func (g Generator) Generate(ctx *genall.GenerationContext) error {
- parser := &Parser{
- Collector: ctx.Collector,
- Checker: ctx.Checker,
- // Perform defaulting here to avoid ambiguity later
- IgnoreUnexportedFields: g.IgnoreUnexportedFields != nil && *g.IgnoreUnexportedFields == true,
- AllowDangerousTypes: g.AllowDangerousTypes != nil && *g.AllowDangerousTypes == true,
- // Indicates the parser on whether to register the ObjectMeta type or not
- GenerateEmbeddedObjectMeta: g.GenerateEmbeddedObjectMeta != nil && *g.GenerateEmbeddedObjectMeta == true,
- }
- AddKnownTypes(parser)
- for _, root := range ctx.Roots {
- parser.NeedPackage(root)
- }
- metav1Pkg := FindMetav1(ctx.Roots)
- if metav1Pkg == nil {
- // no objects in the roots, since nothing imported metav1
- return nil
- }
- // TODO: allow selecting a specific object
- kubeKinds := FindKubeKinds(parser, metav1Pkg)
- if len(kubeKinds) == 0 {
- // no objects in the roots
- return nil
- }
- crdVersions := g.CRDVersions
- if len(crdVersions) == 0 {
- crdVersions = []string{defaultVersion}
- }
- var headerText string
- if g.HeaderFile != "" {
- headerBytes, err := ctx.ReadFile(g.HeaderFile)
- if err != nil {
- return err
- }
- headerText = string(headerBytes)
- }
- headerText = strings.ReplaceAll(headerText, " YEAR", " "+g.Year)
- for _, groupKind := range kubeKinds {
- parser.NeedCRDFor(groupKind, g.MaxDescLen)
- crdRaw := parser.CustomResourceDefinitions[groupKind]
- addAttribution(&crdRaw)
- // Prevent the top level metadata for the CRD to be generate regardless of the intention in the arguments
- FixTopLevelMetadata(crdRaw)
- versionedCRDs := make([]interface{}, len(crdVersions))
- for i, ver := range crdVersions {
- conv, err := AsVersion(crdRaw, schema.GroupVersion{Group: apiext.SchemeGroupVersion.Group, Version: ver})
- if err != nil {
- return err
- }
- versionedCRDs[i] = conv
- }
- for i, crd := range versionedCRDs {
- removeDescriptionFromMetadata(crd.(*apiext.CustomResourceDefinition))
- var fileName string
- if i == 0 {
- fileName = fmt.Sprintf("%s_%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural)
- } else {
- fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i])
- }
- if err := ctx.WriteYAML(fileName, headerText, []interface{}{crd}, genall.WithTransform(transformRemoveCRDStatus), genall.WithTransform(genall.TransformRemoveCreationTimestamp)); err != nil {
- return err
- }
- }
- }
- return nil
- }
- func removeDescriptionFromMetadata(crd *apiext.CustomResourceDefinition) {
- for _, versionSpec := range crd.Spec.Versions {
- if versionSpec.Schema != nil {
- removeDescriptionFromMetadataProps(versionSpec.Schema.OpenAPIV3Schema)
- }
- }
- }
- func removeDescriptionFromMetadataProps(v *apiext.JSONSchemaProps) {
- if m, ok := v.Properties["metadata"]; ok {
- meta := &m
- if meta.Description != "" {
- meta.Description = ""
- v.Properties["metadata"] = m
- }
- }
- }
- // FixTopLevelMetadata resets the schema for the top-level metadata field which is needed for CRD validation
- func FixTopLevelMetadata(crd apiext.CustomResourceDefinition) {
- for _, v := range crd.Spec.Versions {
- if v.Schema != nil && v.Schema.OpenAPIV3Schema != nil && v.Schema.OpenAPIV3Schema.Properties != nil {
- schemaProperties := v.Schema.OpenAPIV3Schema.Properties
- if _, ok := schemaProperties["metadata"]; ok {
- schemaProperties["metadata"] = apiext.JSONSchemaProps{Type: "object"}
- }
- }
- }
- }
- // addAttribution adds attribution info to indicate controller-gen tool was used
- // to generate this CRD definition along with the version info.
- func addAttribution(crd *apiext.CustomResourceDefinition) {
- if crd.ObjectMeta.Annotations == nil {
- crd.ObjectMeta.Annotations = map[string]string{}
- }
- crd.ObjectMeta.Annotations["controller-gen.kubebuilder.io/version"] = version.Version()
- }
- // FindMetav1 locates the actual package representing metav1 amongst
- // the imports of the roots.
- func FindMetav1(roots []*loader.Package) *loader.Package {
- for _, root := range roots {
- pkg := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"]
- if pkg != nil {
- return pkg
- }
- }
- return nil
- }
- // FindKubeKinds locates all types that contain TypeMeta and ObjectMeta
- // (and thus may be a Kubernetes object), and returns the corresponding
- // group-kinds.
- func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) []schema.GroupKind {
- // TODO(directxman12): technically, we should be finding metav1 per-package
- kubeKinds := map[schema.GroupKind]struct{}{}
- for typeIdent, info := range parser.Types {
- hasObjectMeta := false
- hasTypeMeta := false
- pkg := typeIdent.Package
- pkg.NeedTypesInfo()
- typesInfo := pkg.TypesInfo
- for _, field := range info.Fields {
- if field.Name != "" {
- // type and object meta are embedded,
- // so they can't be this
- continue
- }
- fieldType := typesInfo.TypeOf(field.RawField.Type)
- namedField, isNamed := fieldType.(*types.Named)
- if !isNamed {
- // ObjectMeta and TypeMeta are named types
- continue
- }
- if namedField.Obj().Pkg() == nil {
- // Embedded non-builtin universe type (specifically, it's probably `error`),
- // so it can't be ObjectMeta or TypeMeta
- continue
- }
- fieldPkgPath := loader.NonVendorPath(namedField.Obj().Pkg().Path())
- fieldPkg := pkg.Imports()[fieldPkgPath]
- // Compare the metav1 package by ID and not by the actual instance
- // of the object. The objects in memory could be different due to
- // loading from different root paths, even when they both refer to
- // the same metav1 package.
- if fieldPkg == nil || fieldPkg.ID != metav1Pkg.ID {
- continue
- }
- switch namedField.Obj().Name() {
- case "ObjectMeta":
- hasObjectMeta = true
- case "TypeMeta":
- hasTypeMeta = true
- }
- }
- if !hasObjectMeta || !hasTypeMeta {
- continue
- }
- groupKind := schema.GroupKind{
- Group: parser.GroupVersions[pkg].Group,
- Kind: typeIdent.Name,
- }
- kubeKinds[groupKind] = struct{}{}
- }
- groupKindList := make([]schema.GroupKind, 0, len(kubeKinds))
- for groupKind := range kubeKinds {
- groupKindList = append(groupKindList, groupKind)
- }
- sort.Slice(groupKindList, func(i, j int) bool {
- return groupKindList[i].String() < groupKindList[j].String()
- })
- return groupKindList
- }
- // filterTypesForCRDs filters out all nodes that aren't used in CRD generation,
- // like interfaces and struct fields without JSON tag.
- func filterTypesForCRDs(node ast.Node) bool {
- switch node := node.(type) {
- case *ast.InterfaceType:
- // skip interfaces, we never care about references in them
- return false
- case *ast.StructType:
- return true
- case *ast.Field:
- _, hasTag := loader.ParseAstTag(node.Tag).Lookup("json")
- // fields without JSON tags mean we have custom serialization,
- // so only visit fields with tags.
- return hasTag
- default:
- return true
- }
- }
|