2 Copyright 2018 The Kubernetes Authors.
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
8 http://www.apache.org/licenses/LICENSE-2.0
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.
26 "github.com/ghodss/yaml"
27 "github.com/spf13/afero"
28 extensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
31 crdutil "sigs.k8s.io/controller-tools/pkg/crd/util"
32 "sigs.k8s.io/controller-tools/pkg/internal/codegen"
33 "sigs.k8s.io/controller-tools/pkg/internal/codegen/parse"
34 "sigs.k8s.io/controller-tools/pkg/util"
37 // Generator generates CRD manifests from API resource definitions defined in Go source files.
38 type Generator struct {
44 SkipMapValidation bool
46 // OutFs is filesystem to be used for writing out the result
49 // apisPkg is the absolute Go pkg name for current project's 'pkg/apis' pkg.
50 // This is needed to determine if a Type belongs to the project or it is a referred Type.
53 // APIsPath and APIsPkg allow customized generation for Go types existing under directories other than pkg/apis
58 // ValidateAndInitFields validate and init generator fields.
59 func (c *Generator) ValidateAndInitFields() error {
63 c.OutFs = afero.NewOsFs()
66 if len(c.RootPath) == 0 {
67 // Take current path as root path if not specified.
68 c.RootPath, err = os.Getwd()
74 // Validate PROJECT file if Domain or Repo are not set manually
75 if len(c.Domain) == 0 || len(c.Repo) == 0 {
76 if !crdutil.PathHasProjectFile(c.RootPath) {
77 return fmt.Errorf("PROJECT file missing in dir %s", c.RootPath)
82 c.Repo = crdutil.GetRepoFromProject(c.RootPath)
85 // If Domain is not explicitly specified,
86 // try to search for PROJECT file as a basis.
87 if len(c.Domain) == 0 {
88 c.Domain = crdutil.GetDomainFromProject(c.RootPath)
96 // Init output directory
97 if c.OutputDir == "" {
98 c.OutputDir = path.Join(c.RootPath, "config/crds")
104 // Do manages CRD generation.
105 func (c *Generator) Do() error {
106 arguments := args.Default()
107 b, err := arguments.NewBuilder()
109 return fmt.Errorf("failed making a parser: %v", err)
112 // Switch working directory to root path.
113 wd, err := os.Getwd()
117 if err := os.Chdir(c.RootPath); err != nil {
118 return fmt.Errorf("failed switching working dir: %v", err)
121 if err := os.Chdir(wd); err != nil {
122 log.Fatalf("Failed to switch back to original working dir: %v", err)
126 if err := b.AddDirRecursive(fmt.Sprintf("%s/%s", c.Repo, c.APIsPath)); err != nil {
127 return fmt.Errorf("failed making a parser: %v", err)
130 ctx, err := parse.NewContext(b)
132 return fmt.Errorf("failed making a context: %v", err)
135 arguments.CustomArgs = &parse.Options{SkipMapValidation: c.SkipMapValidation}
137 // TODO: find an elegant way to fulfill the domain in APIs.
138 p := parse.NewAPIs(ctx, arguments, c.Domain, c.apisPkg)
141 return c.writeCRDs(crds)
144 func (c *Generator) writeCRDs(crds map[string][]byte) error {
145 // Ensure output dir exists.
146 if err := c.OutFs.MkdirAll(c.OutputDir, os.FileMode(0700)); err != nil {
150 for file, crd := range crds {
151 outFile := path.Join(c.OutputDir, file)
152 if err := (&util.FileWriter{Fs: c.OutFs}).WriteFile(outFile, crd); err != nil {
159 func getCRDFileName(resource *codegen.APIResource) string {
160 elems := []string{resource.Group, resource.Version, strings.ToLower(resource.Kind)}
161 return strings.Join(elems, "_") + ".yaml"
164 func (c *Generator) getCrds(p *parse.APIs) map[string][]byte {
165 crds := map[string]extensionsv1beta1.CustomResourceDefinition{}
166 for _, g := range p.APIs.Groups {
167 for _, v := range g.Versions {
168 for _, r := range v.Resources {
170 // ignore types which do not belong to this project
171 if !c.belongsToAPIsPkg(r.Type) {
174 if len(c.Namespace) > 0 {
175 crd.Namespace = c.Namespace
177 fileName := getCRDFileName(r)
183 result := map[string][]byte{}
184 for file, crd := range crds {
185 b, err := yaml.Marshal(crd)
187 log.Fatalf("Error: %v", err)
195 // belongsToAPIsPkg returns true if type t is defined under pkg/apis pkg of
197 func (c *Generator) belongsToAPIsPkg(t *types.Type) bool {
198 return strings.HasPrefix(t.Name.Package, c.apisPkg)
201 func (c *Generator) setAPIsPkg() error {
202 if c.APIsPath == "" {
203 c.APIsPath = "pkg/apis"
206 c.apisPkg = c.APIsPkg
208 // Validate apis directory exists under working path
209 apisPath := path.Join(c.RootPath, c.APIsPath)
210 if _, err := os.Stat(apisPath); err != nil {
211 return fmt.Errorf("error validating apis path %s: %v", apisPath, err)
214 c.apisPkg = path.Join(c.Repo, c.APIsPath)