From c81f98c5893d19d0b6a861ade5caac093f6c475e Mon Sep 17 00:00:00 2001 From: Eric Stroczynski Date: Thu, 31 Jan 2019 12:05:53 -0800 Subject: [PATCH] Create CRD validation specs in CRD manifests and as Go code (#869) * commands/.../generate/*: build and call openapi-gen, and re-scaffold CRD manifests commands/.../add/api.go: call OpenAPI generator function pkg/scaffold/crd*: use CustomRenderer interface to write CRD manifests with validation spec instead of a template pkg/scaffold/gopkgtoml*: include openapi-gen deps pkg/scaffold/types*: add openapi-gen directives * fix comment * only use controller-tools CRD generator if in a Go project * change dir in unit test so Go project is detected * add names and only overwrite dstCrd when a file at path was generated remove status field from resulting struct * use generated crd if one isn't present locally * add test case for non-Go crd generation * internal/util/*util/*: remove pkg/scaffold imports, which cause cycles * test/e2e/memcached_test.go: symlink pkg and internal * pkg/scaffold/*: use a testData dir for scaffold tests instead of test/test-framework * rename test dir to avoid import cycles during e2e test * pkg/scaffold/crd.go: add IsOperatorGo field for controller-tools crd generator condition * use test/test-framework code as test project * revert removing scaffold constants from internal/util * revert SrcDir change and check IsOperatorGo first * change function name * pkg/scaffold/crd.go: simplify unmarshalling crd * use Golang initialism/acronym conventions for naming CR(D) types/variables * Gopkg.lock: revendor * commands/.../openapi.go: return error from CLI func instead of log.Fatal * fix kube-openapi revision * pkg/scaffold/gopkgtoml*.go: force sigs.k8s.io/controller-tools/pkg/crd/generator vendoring * remove extra kube-openapi override * pkg/scaffold/types*.go: add note on adding custom validation * commands/.../api.go: comment on what is generated in add api command usage * commands/.../openapi.go: remove go header file from openapi-gen * new() -> &...{} * correct verbosity settings for deepcopy and openapi generators * commands/operator-sdk/cmd/add/crd.go: only skip overwriting CRD's/CR's if explicitly adding them * verbose code generation in e2e test * add --header-file for Go boilerplate file path * update openapi command comment * override go-openapi/spec to avoid new project dep solve errors * revendor --- Gopkg.lock | 30 +++ commands/operator-sdk/cmd/add.go | 2 +- commands/operator-sdk/cmd/add/api.go | 30 ++- commands/operator-sdk/cmd/add/crd.go | 13 +- commands/operator-sdk/cmd/generate.go | 1 + .../cmd/generate/internal/genutil.go | 106 +++++++++ commands/operator-sdk/cmd/generate/k8s.go | 120 +++------- commands/operator-sdk/cmd/generate/openapi.go | 212 ++++++++++++++++++ commands/operator-sdk/cmd/new.go | 8 +- internal/util/projutil/project_util.go | 19 +- internal/util/yamlutil/manifest.go | 6 +- pkg/scaffold/constants.go | 2 +- pkg/scaffold/cr.go | 8 +- pkg/scaffold/cr_test.go | 8 +- pkg/scaffold/crd.go | 176 +++++++++++++-- pkg/scaffold/crd_test.go | 95 +++++++- pkg/scaffold/gopkgtoml.go | 22 +- pkg/scaffold/gopkgtoml_test.go | 22 +- pkg/scaffold/olm-catalog/config.go | 2 +- pkg/scaffold/olm-catalog/config_test.go | 2 +- pkg/scaffold/types.go | 4 + pkg/scaffold/types_test.go | 4 + test/e2e/memcached_test.go | 10 +- 23 files changed, 739 insertions(+), 163 deletions(-) create mode 100644 commands/operator-sdk/cmd/generate/internal/genutil.go create mode 100644 commands/operator-sdk/cmd/generate/openapi.go diff --git a/Gopkg.lock b/Gopkg.lock index 3a45cc2ded2..b8a9359723a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1278,6 +1278,20 @@ revision = "8d9ed539ba3134352c586810e749e58df4e94e4f" version = "kubernetes-1.13.1" +[[projects]] + branch = "master" + digest = "1:4defc5df1acbcc491730533734e94a8571ad8c005ca70b71e9697113a27a0166" + name = "k8s.io/gengo" + packages = [ + "args", + "generator", + "namer", + "parser", + "types", + ] + pruneopts = "UT" + revision = "fd15ee9cc2f77baa4f31e59e6acbf21146455073" + [[projects]] digest = "1:ca16b131162cb593ef1e9e9bf3508d753f4dfef8ac2440e7a55574c9652bddb9" name = "k8s.io/helm" @@ -1506,6 +1520,21 @@ revision = "12d98582e72927b6cd0123e2b4e819f9341ce62c" version = "v0.1.10" +[[projects]] + digest = "1:682237faf8a777ed193912b47f933c5c900829ce01079da3f41455537c0e471a" + name = "sigs.k8s.io/controller-tools" + packages = [ + "pkg/crd/generator", + "pkg/crd/util", + "pkg/internal/codegen", + "pkg/internal/codegen/parse", + "pkg/internal/general", + "pkg/util", + ] + pruneopts = "UT" + revision = "b072ef59824b16023b0e12c94d0040d99059a961" + version = "v0.1.7" + [[projects]] digest = "1:7719608fe0b52a4ece56c2dde37bedd95b938677d1ab0f84b8a7852e4c59f849" name = "sigs.k8s.io/yaml" @@ -1611,6 +1640,7 @@ "sigs.k8s.io/controller-runtime/pkg/runtime/scheme", "sigs.k8s.io/controller-runtime/pkg/runtime/signals", "sigs.k8s.io/controller-runtime/pkg/source", + "sigs.k8s.io/controller-tools/pkg/crd/generator", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/commands/operator-sdk/cmd/add.go b/commands/operator-sdk/cmd/add.go index f4f87be3ea1..69d6f8ca3af 100644 --- a/commands/operator-sdk/cmd/add.go +++ b/commands/operator-sdk/cmd/add.go @@ -29,6 +29,6 @@ func NewAddCmd() *cobra.Command { upCmd.AddCommand(add.NewApiCmd()) upCmd.AddCommand(add.NewControllerCmd()) - upCmd.AddCommand(add.NewAddCrdCmd()) + upCmd.AddCommand(add.NewAddCRDCmd()) return upCmd } diff --git a/commands/operator-sdk/cmd/add/api.go b/commands/operator-sdk/cmd/add/api.go index 5caf27b1e6d..4b3aa27d092 100644 --- a/commands/operator-sdk/cmd/add/api.go +++ b/commands/operator-sdk/cmd/add/api.go @@ -36,8 +36,17 @@ func NewApiCmd() *cobra.Command { Use: "api", Short: "Adds a new api definition under pkg/apis", Long: `operator-sdk add api --kind= --api-version= creates the -api definition for a new custom resource under pkg/apis. This command must be run from the project root directory. -If the api already exists at pkg/apis// then the command will not overwrite and return an error. +api definition for a new custom resource under pkg/apis. This command must be +run from the project root directory. If the api already exists at +pkg/apis// then the command will not overwrite and return an +error. + +This command runs Kubernetes deepcopy and OpenAPI V3 generators on tagged +types in all paths under pkg/apis. Go code is generated under +pkg/apis///zz_generated.{deepcopy,openapi}.go. CRD's are +generated, or updated if they exist for a particular group + version + kind, +under deploy/crds/___crd.yaml; OpenAPI V3 validation YAML +is generated as a 'validation' object. Example: $ operator-sdk add api --api-version=app.example.com/v1alpha1 --kind=AppService @@ -49,8 +58,12 @@ Example: └── v1alpha1 ├── doc.go ├── register.go - ├── types.go - + ├── appservice_types.go + ├── zz_generated.deepcopy.go + ├── zz_generated.openapi.go + $ tree deploy/crds + ├── deploy/crds/app_v1alpha1_appservice_cr.yaml + ├── deploy/crds/app_v1alpha1_appservice_crd.yaml `, RunE: apiRun, } @@ -96,8 +109,8 @@ func apiRun(cmd *cobra.Command, args []string) error { &scaffold.AddToScheme{Resource: r}, &scaffold.Register{Resource: r}, &scaffold.Doc{Resource: r}, - &scaffold.Cr{Resource: r}, - &scaffold.Crd{Resource: r}, + &scaffold.CR{Resource: r}, + &scaffold.CRD{Resource: r, IsOperatorGo: projutil.IsOperatorGo()}, ) if err != nil { return fmt.Errorf("api scaffold failed: (%v)", err) @@ -113,6 +126,11 @@ func apiRun(cmd *cobra.Command, args []string) error { return err } + // Generate a validation spec for the new CRD. + if err := generate.OpenAPIGen(); err != nil { + return err + } + log.Info("API generation complete.") return nil } diff --git a/commands/operator-sdk/cmd/add/crd.go b/commands/operator-sdk/cmd/add/crd.go index 1a7828d2013..2adb3cb8f0c 100644 --- a/commands/operator-sdk/cmd/add/crd.go +++ b/commands/operator-sdk/cmd/add/crd.go @@ -28,8 +28,8 @@ import ( "github.com/spf13/cobra" ) -// NewAddCrdCmd - add crd command -func NewAddCrdCmd() *cobra.Command { +// NewAddCRDCmd - add crd command +func NewAddCRDCmd() *cobra.Command { crdCmd := &cobra.Command{ Use: "crd", Short: "Adds a Custom Resource Definition (CRD) and the Custom Resource (CR) files", @@ -78,11 +78,12 @@ func crdFunc(cmd *cobra.Command, args []string) error { s := scaffold.Scaffold{} err = s.Execute(cfg, - &scaffold.Crd{ - Input: input.Input{IfExistsAction: input.Skip}, - Resource: resource, + &scaffold.CRD{ + Input: input.Input{IfExistsAction: input.Skip}, + Resource: resource, + IsOperatorGo: projutil.IsOperatorGo(), }, - &scaffold.Cr{ + &scaffold.CR{ Input: input.Input{IfExistsAction: input.Skip}, Resource: resource, }, diff --git a/commands/operator-sdk/cmd/generate.go b/commands/operator-sdk/cmd/generate.go index 39923f518f0..53dbbdcf8c3 100644 --- a/commands/operator-sdk/cmd/generate.go +++ b/commands/operator-sdk/cmd/generate.go @@ -27,5 +27,6 @@ func NewGenerateCmd() *cobra.Command { Long: `The operator-sdk generate command invokes specific generator to generate code as needed.`, } cmd.AddCommand(generate.NewGenerateK8SCmd()) + cmd.AddCommand(generate.NewGenerateOpenAPICmd()) return cmd } diff --git a/commands/operator-sdk/cmd/generate/internal/genutil.go b/commands/operator-sdk/cmd/generate/internal/genutil.go new file mode 100644 index 00000000000..a98aadd5489 --- /dev/null +++ b/commands/operator-sdk/cmd/generate/internal/genutil.go @@ -0,0 +1,106 @@ +// Copyright 2018 The Operator-SDK 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 genutil + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/operator-framework/operator-sdk/internal/util/projutil" + "github.com/operator-framework/operator-sdk/pkg/scaffold" +) + +func BuildCodegenBinaries(genDirs []string, binDir, codegenSrcDir string) error { + for _, gd := range genDirs { + err := runGoBuildCodegen(binDir, codegenSrcDir, gd) + if err != nil { + return err + } + } + return nil +} + +func runGoBuildCodegen(binDir, repoDir, genDir string) error { + binPath := filepath.Join(binDir, filepath.Base(genDir)) + cmd := exec.Command("go", "build", "-o", binPath, genDir) + cmd.Dir = repoDir + if gf, ok := os.LookupEnv(projutil.GoFlagsEnv); ok && len(gf) != 0 { + cmd.Env = append(os.Environ(), projutil.GoFlagsEnv+"="+gf) + } + + if projutil.IsGoVerbose() { + return projutil.ExecCmd(cmd) + } + cmd.Stdout = ioutil.Discard + cmd.Stderr = ioutil.Discard + return cmd.Run() +} + +// ParseGroupVersions parses the layout of pkg/apis to return a map of +// API groups to versions. +func ParseGroupVersions() (map[string][]string, error) { + gvs := make(map[string][]string) + groups, err := ioutil.ReadDir(scaffold.ApisDir) + if err != nil { + return nil, fmt.Errorf("could not read pkg/apis directory to find api Versions: %v", err) + } + + for _, g := range groups { + if g.IsDir() { + groupDir := filepath.Join(scaffold.ApisDir, g.Name()) + versions, err := ioutil.ReadDir(groupDir) + if err != nil { + return nil, fmt.Errorf("could not read %s directory to find api Versions: %v", groupDir, err) + } + + gvs[g.Name()] = make([]string, 0) + for _, v := range versions { + if v.IsDir() && scaffold.ResourceVersionRegexp.MatchString(v.Name()) { + gvs[g.Name()] = append(gvs[g.Name()], v.Name()) + } + } + } + } + + if len(gvs) == 0 { + return nil, fmt.Errorf("no groups or versions found in %s", scaffold.ApisDir) + } + return gvs, nil +} + +// CreateFQApis return a string of all fully qualified pkg + groups + versions +// of pkg and gvs in the format: +// "pkg/groupA/v1,pkg/groupA/v2,pkg/groupB/v1" +func CreateFQApis(pkg string, gvs map[string][]string) string { + gn := 0 + fqb := &strings.Builder{} + for g, vs := range gvs { + for vn, v := range vs { + fqb.WriteString(filepath.Join(pkg, g, v)) + if vn < len(vs)-1 { + fqb.WriteString(",") + } + } + if gn < len(gvs)-1 { + fqb.WriteString(",") + } + gn++ + } + return fqb.String() +} diff --git a/commands/operator-sdk/cmd/generate/k8s.go b/commands/operator-sdk/cmd/generate/k8s.go index e951171a9bd..9dcfeb9b4d5 100644 --- a/commands/operator-sdk/cmd/generate/k8s.go +++ b/commands/operator-sdk/cmd/generate/k8s.go @@ -17,11 +17,11 @@ package generate import ( "fmt" "io/ioutil" - "os" "os/exec" "path/filepath" "strings" + genutil "github.com/operator-framework/operator-sdk/commands/operator-sdk/cmd/generate/internal" "github.com/operator-framework/operator-sdk/internal/util/projutil" "github.com/operator-framework/operator-sdk/pkg/scaffold" @@ -33,8 +33,18 @@ func NewGenerateK8SCmd() *cobra.Command { return &cobra.Command{ Use: "k8s", Short: "Generates Kubernetes code for custom resource", - Long: `k8s generator generates code for custom resource given the API spec -to comply with kube-API requirements. + Long: `k8s generator generates code for custom resources given the API +specs in pkg/apis// directories to comply with kube-API +requirements. Go code is generated under +pkg/apis///zz_generated.deepcopy.go. + +Example: + $ operator-sdk generate k8s + $ tree pkg/apis + pkg/apis/ + └── app + └── v1alpha1 + ├── zz_generated.deepcopy.go `, RunE: k8sFunc, } @@ -66,7 +76,7 @@ func K8sCodegen() error { return err } - gvMap, err := parseGroupVersions() + gvMap, err := genutil.ParseGroupVersions() if err != nil { return fmt.Errorf("failed to parse group versions: (%v)", err) } @@ -75,7 +85,7 @@ func K8sCodegen() error { gvb.WriteString(fmt.Sprintf("%s:%v, ", g, vs)) } - log.Infof("Running code-generation for Custom Resource group versions: [%v]\n", gvb.String()) + log.Infof("Running deepcopy code-generation for Custom Resource group versions: [%v]\n", gvb.String()) if err := deepcopyGen(binDir, repoPkg, gvMap); err != nil { return err @@ -93,100 +103,26 @@ func buildCodegenBinaries(binDir, codegenSrcDir string) error { "./cmd/informer-gen", "./cmd/deepcopy-gen", } - for _, gd := range genDirs { - err := runGoBuildCodegen(binDir, codegenSrcDir, gd) - if err != nil { - return err - } - } - return nil -} - -func runGoBuildCodegen(binDir, repoDir, genDir string) error { - binPath := filepath.Join(binDir, filepath.Base(genDir)) - installCmd := exec.Command("go", "build", "-o", binPath, genDir) - installCmd.Dir = repoDir - isVerbose := false - if gf, ok := os.LookupEnv("GOFLAGS"); ok && len(gf) != 0 { - installCmd.Env = append(os.Environ(), "GOFLAGS="+gf) - if strings.Contains(gf, "-v") { - isVerbose = true - } - } - if isVerbose { - installCmd.Stdout = os.Stdout - installCmd.Stderr = os.Stderr - } else { - installCmd.Stdout = ioutil.Discard - installCmd.Stderr = ioutil.Discard - } - return installCmd.Run() -} - -// parseGroupVersions parses the layout of pkg/apis to return a map of -// API groups to versions. -func parseGroupVersions() (map[string][]string, error) { - gvs := make(map[string][]string) - groups, err := ioutil.ReadDir(scaffold.ApisDir) - if err != nil { - return nil, fmt.Errorf("could not read pkg/apis directory to find api Versions: %v", err) - } - - for _, g := range groups { - if g.IsDir() { - groupDir := filepath.Join(scaffold.ApisDir, g.Name()) - versions, err := ioutil.ReadDir(groupDir) - if err != nil { - return nil, fmt.Errorf("could not read %s directory to find api Versions: %v", groupDir, err) - } - - gvs[g.Name()] = make([]string, 0) - for _, v := range versions { - if v.IsDir() && scaffold.ResourceVersionRegexp.MatchString(v.Name()) { - gvs[g.Name()] = append(gvs[g.Name()], v.Name()) - } - } - } - } - - if len(gvs) == 0 { - return nil, fmt.Errorf("no groups or versions found in %s", scaffold.ApisDir) - } - return gvs, nil + return genutil.BuildCodegenBinaries(genDirs, binDir, codegenSrcDir) } -// createFQApis return a string of all fully qualified pkg + groups + versions -// of pkg and gvs in the format: -// "pkg/groupA/v1,pkg/groupA/v2,pkg/groupB/v1" -func createFQApis(pkg string, gvs map[string][]string) string { - gn := 0 - sb := &strings.Builder{} - for g, vs := range gvs { - for vn, v := range vs { - sb.WriteString(filepath.Join(pkg, g, v)) - if vn < len(vs)-1 { - sb.WriteString(",") - } - } - if gn < len(gvs)-1 { - sb.WriteString(",") - } - gn++ - } - return sb.String() -} - -func deepcopyGen(binDir, repoPkg string, gvMap map[string][]string) error { +func deepcopyGen(binDir, repoPkg string, gvMap map[string][]string) (err error) { apisPkg := filepath.Join(repoPkg, scaffold.ApisDir) args := []string{ - "--input-dirs", createFQApis(apisPkg, gvMap), - "-O", "zz_generated.deepcopy", + "--input-dirs", genutil.CreateFQApis(apisPkg, gvMap), + "--output-file-base", "zz_generated.deepcopy", "--bounding-dirs", apisPkg, } - cgPath := filepath.Join(binDir, "deepcopy-gen") - err := projutil.ExecCmd(exec.Command(cgPath, args...)) + cmd := exec.Command(filepath.Join(binDir, "deepcopy-gen"), args...) + if projutil.IsGoVerbose() { + err = projutil.ExecCmd(cmd) + } else { + cmd.Stdout = ioutil.Discard + cmd.Stderr = ioutil.Discard + err = cmd.Run() + } if err != nil { - return fmt.Errorf("failed to perform code-generation: %v", err) + return fmt.Errorf("failed to perform deepcopy code-generation: %v", err) } return nil } diff --git a/commands/operator-sdk/cmd/generate/openapi.go b/commands/operator-sdk/cmd/generate/openapi.go new file mode 100644 index 00000000000..0b7fdcacecf --- /dev/null +++ b/commands/operator-sdk/cmd/generate/openapi.go @@ -0,0 +1,212 @@ +// Copyright 2018 The Operator-SDK 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 generate + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + genutil "github.com/operator-framework/operator-sdk/commands/operator-sdk/cmd/generate/internal" + "github.com/operator-framework/operator-sdk/internal/util/projutil" + "github.com/operator-framework/operator-sdk/pkg/scaffold" + "github.com/operator-framework/operator-sdk/pkg/scaffold/input" + + "github.com/ghodss/yaml" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var headerFile string + +func NewGenerateOpenAPICmd() *cobra.Command { + openAPICmd := &cobra.Command{ + Use: "openapi", + Short: "Generates OpenAPI specs for API's", + Long: `generate openapi generates OpenAPI validation specs in Go from tagged types +in all pkg/apis// directories. Go code is generated under +pkg/apis///zz_generated.openapi.go. CRD's are generated, or +updated if they exist for a particular group + version + kind, under +deploy/crds/___crd.yaml; OpenAPI V3 validation YAML +is generated as a 'validation' object. + +Example: + $ operator-sdk generate openapi + $ tree pkg/apis + pkg/apis/ + └── app + └── v1alpha1 + ├── zz_generated.openapi.go + $ tree deploy/crds + ├── deploy/crds/app_v1alpha1_appservice_cr.yaml + ├── deploy/crds/app_v1alpha1_appservice_crd.yaml +`, + RunE: openAPIFunc, + } + + openAPICmd.Flags().StringVar(&headerFile, "header-file", "", "Path to file containing headers for generated files.") + + return openAPICmd +} + +func openAPIFunc(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + return fmt.Errorf("command %s doesn't accept any arguments", cmd.CommandPath()) + } + + return OpenAPIGen() +} + +// OpenAPIGen generates OpenAPI validation specs for all CRD's in dirs. +func OpenAPIGen() error { + projutil.MustInProjectRoot() + + absProjectPath := projutil.MustGetwd() + repoPkg := projutil.CheckAndGetProjectGoPkg() + srcDir := filepath.Join(absProjectPath, "vendor", "k8s.io", "kube-openapi") + binDir := filepath.Join(absProjectPath, scaffold.BuildBinDir) + + if err := buildOpenAPIGenBinary(binDir, srcDir); err != nil { + return err + } + + gvMap, err := genutil.ParseGroupVersions() + if err != nil { + return fmt.Errorf("failed to parse group versions: (%v)", err) + } + gvb := &strings.Builder{} + for g, vs := range gvMap { + gvb.WriteString(fmt.Sprintf("%s:%v, ", g, vs)) + } + + log.Infof("Running OpenAPI code-generation for Custom Resource group versions: [%v]\n", gvb.String()) + + apisPkg := filepath.Join(repoPkg, scaffold.ApisDir) + fqApiStr := genutil.CreateFQApis(apisPkg, gvMap) + fqApis := strings.Split(fqApiStr, ",") + if err := openAPIGen(binDir, fqApis); err != nil { + return err + } + + s := &scaffold.Scaffold{} + cfg := &input.Config{ + Repo: repoPkg, + AbsProjectPath: absProjectPath, + ProjectName: filepath.Base(absProjectPath), + } + crdMap, err := getCRDGVKMap() + if err != nil { + return err + } + for g, vs := range gvMap { + for _, v := range vs { + gvks := crdMap[filepath.Join(g, v)] + for _, gvk := range gvks { + r, err := scaffold.NewResource(filepath.Join(gvk.Group, gvk.Version), gvk.Kind) + if err != nil { + return err + } + err = s.Execute(cfg, + &scaffold.CRD{Resource: r, IsOperatorGo: projutil.IsOperatorGo()}, + ) + if err != nil { + return err + } + } + } + } + + log.Info("Code-generation complete.") + return nil +} + +func buildOpenAPIGenBinary(binDir, codegenSrcDir string) error { + genDirs := []string{"./cmd/openapi-gen"} + return genutil.BuildCodegenBinaries(genDirs, binDir, codegenSrcDir) +} + +func openAPIGen(binDir string, fqApis []string) (err error) { + if headerFile == "" { + f, err := ioutil.TempFile(scaffold.BuildBinDir, "") + if err != nil { + return err + } + headerFile = f.Name() + defer func() { + if err = os.RemoveAll(headerFile); err != nil { + log.Error(err) + } + }() + } + cgPath := filepath.Join(binDir, "openapi-gen") + for _, fqApi := range fqApis { + args := []string{ + "--input-dirs", fqApi, + "--output-package", fqApi, + "--output-file-base", "zz_generated.openapi", + // openapi-gen requires a boilerplate file. Either use header or an + // empty file if header is empty. + "--go-header-file", headerFile, + } + cmd := exec.Command(cgPath, args...) + if projutil.IsGoVerbose() { + err = projutil.ExecCmd(cmd) + } else { + cmd.Stdout = ioutil.Discard + cmd.Stderr = ioutil.Discard + err = cmd.Run() + } + if err != nil { + return fmt.Errorf("failed to perform openapi code-generation: %v", err) + } + } + return nil +} + +func getCRDGVKMap() (map[string][]metav1.GroupVersionKind, error) { + crdInfos, err := ioutil.ReadDir(scaffold.CRDsDir) + if err != nil { + return nil, err + } + crdMap := make(map[string][]metav1.GroupVersionKind) + for _, info := range crdInfos { + if filepath.Ext(info.Name()) == ".yaml" { + path := filepath.Join(scaffold.CRDsDir, info.Name()) + b, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + crd := &apiextv1beta1.CustomResourceDefinition{} + if err := yaml.Unmarshal(b, crd); err != nil { + return nil, err + } + if crd.Kind != "CustomResourceDefinition" { + continue + } + gv := filepath.Join(strings.Split(info.Name(), "_")[:2]...) + crdMap[gv] = append(crdMap[gv], metav1.GroupVersionKind{ + Group: crd.Spec.Group, + Version: crd.Spec.Version, + Kind: crd.Spec.Names.Kind, + }) + } + } + return crdMap, nil +} diff --git a/commands/operator-sdk/cmd/new.go b/commands/operator-sdk/cmd/new.go index c4bd27af03e..447739c32db 100644 --- a/commands/operator-sdk/cmd/new.go +++ b/commands/operator-sdk/cmd/new.go @@ -185,8 +185,8 @@ func doAnsibleScaffold() error { &scaffold.ServiceAccount{}, &scaffold.Role{IsClusterScoped: isClusterScoped}, &scaffold.RoleBinding{IsClusterScoped: isClusterScoped}, - &scaffold.Crd{Resource: resource}, - &scaffold.Cr{Resource: resource}, + &scaffold.CRD{Resource: resource}, + &scaffold.CR{Resource: resource}, &ansible.BuildDockerfile{GeneratePlaybook: generatePlaybook}, &ansible.RolesReadme{Resource: *resource}, &ansible.RolesMetaMain{Resource: *resource}, @@ -277,8 +277,8 @@ func doHelmScaffold() error { &scaffold.Role{IsClusterScoped: isClusterScoped}, &scaffold.RoleBinding{IsClusterScoped: isClusterScoped}, &helm.Operator{IsClusterScoped: isClusterScoped}, - &scaffold.Crd{Resource: resource}, - &scaffold.Cr{ + &scaffold.CRD{Resource: resource}, + &scaffold.CR{ Resource: resource, Spec: crSpec, }, diff --git a/internal/util/projutil/project_util.go b/internal/util/projutil/project_util.go index 66311d0c16f..bc30189a25d 100644 --- a/internal/util/projutil/project_util.go +++ b/internal/util/projutil/project_util.go @@ -19,6 +19,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strings" "github.com/operator-framework/operator-sdk/pkg/scaffold" @@ -30,8 +31,9 @@ import ( ) const ( - GopathEnv = "GOPATH" - SrcDir = "src" + GopathEnv = "GOPATH" + GoFlagsEnv = "GOFLAGS" + SrcDir = "src" ) var mainFile = filepath.Join(scaffold.ManagerDir, scaffold.CmdFile) @@ -110,6 +112,10 @@ func GetOperatorType() OperatorType { return OperatorTypeUnknown } +func IsOperatorGo() bool { + return GetOperatorType() == OperatorTypeGo +} + // MustGetGopath gets GOPATH and ensures it is set and non-empty. If GOPATH // is not set or empty, MustGetGopath exits. func MustGetGopath() string { @@ -153,3 +159,12 @@ func ExecCmd(cmd *exec.Cmd) error { } return nil } + +var flagRe = regexp.MustCompile("(.* )?-v(.* )?") + +// IsGoVerbose returns true if GOFLAGS contains "-v". This function is useful +// when deciding whether to make "go" command output verbose. +func IsGoVerbose() bool { + gf, ok := os.LookupEnv(GoFlagsEnv) + return ok && len(gf) != 0 && flagRe.MatchString(gf) +} diff --git a/internal/util/yamlutil/manifest.go b/internal/util/yamlutil/manifest.go index b01df607e20..2913c50331f 100644 --- a/internal/util/yamlutil/manifest.go +++ b/internal/util/yamlutil/manifest.go @@ -110,16 +110,16 @@ func GenerateCombinedGlobalManifest() (*os.File, error) { } }() - files, err := ioutil.ReadDir(scaffold.CrdsDir) + files, err := ioutil.ReadDir(scaffold.CRDsDir) if err != nil { return nil, fmt.Errorf("could not read deploy directory: (%v)", err) } combined := []byte{} for _, file := range files { if strings.HasSuffix(file.Name(), "crd.yaml") { - fileBytes, err := ioutil.ReadFile(filepath.Join(scaffold.CrdsDir, file.Name())) + fileBytes, err := ioutil.ReadFile(filepath.Join(scaffold.CRDsDir, file.Name())) if err != nil { - return nil, fmt.Errorf("could not read file %s: (%v)", filepath.Join(scaffold.CrdsDir, file.Name()), err) + return nil, fmt.Errorf("could not read file %s: (%v)", filepath.Join(scaffold.CRDsDir, file.Name()), err) } combined = CombineManifests(combined, fileBytes) } diff --git a/pkg/scaffold/constants.go b/pkg/scaffold/constants.go index c636cd6519e..77ea258c6ab 100644 --- a/pkg/scaffold/constants.go +++ b/pkg/scaffold/constants.go @@ -34,6 +34,6 @@ const ( BuildScriptDir = BuildDir + filePathSep + "bin" DeployDir = "deploy" OLMCatalogDir = DeployDir + filePathSep + "olm-catalog" - CrdsDir = DeployDir + filePathSep + "crds" + CRDsDir = DeployDir + filePathSep + "crds" VersionDir = "version" ) diff --git a/pkg/scaffold/cr.go b/pkg/scaffold/cr.go index 1d45b07f883..b41a81c82fd 100644 --- a/pkg/scaffold/cr.go +++ b/pkg/scaffold/cr.go @@ -23,8 +23,8 @@ import ( "github.com/operator-framework/operator-sdk/pkg/scaffold/input" ) -// Cr is the input needed to generate a deploy/crds/___cr.yaml file -type Cr struct { +// CR is the input needed to generate a deploy/crds/___cr.yaml file +type CR struct { input.Input // Resource defines the inputs for the new custom resource @@ -35,13 +35,13 @@ type Cr struct { Spec string } -func (s *Cr) GetInput() (input.Input, error) { +func (s *CR) GetInput() (input.Input, error) { if s.Path == "" { fileName := fmt.Sprintf("%s_%s_%s_cr.yaml", strings.ToLower(s.Resource.Group), strings.ToLower(s.Resource.Version), s.Resource.LowerKind) - s.Path = filepath.Join(CrdsDir, fileName) + s.Path = filepath.Join(CRDsDir, fileName) } s.TemplateBody = crTemplate if s.TemplateFuncs == nil { diff --git a/pkg/scaffold/cr_test.go b/pkg/scaffold/cr_test.go index 51a4aa90404..0fbbbab7d04 100644 --- a/pkg/scaffold/cr_test.go +++ b/pkg/scaffold/cr_test.go @@ -20,13 +20,13 @@ import ( "github.com/operator-framework/operator-sdk/internal/util/diffutil" ) -func TestCr(t *testing.T) { +func TestCR(t *testing.T) { r, err := NewResource(appApiVersion, appKind) if err != nil { t.Fatal(err) } s, buf := setupScaffoldAndWriter() - err = s.Execute(appConfig, &Cr{Resource: r}) + err = s.Execute(appConfig, &CR{Resource: r}) if err != nil { t.Fatalf("Failed to execute the scaffold: (%v)", err) } @@ -37,13 +37,13 @@ func TestCr(t *testing.T) { } } -func TestCrCustomSpec(t *testing.T) { +func TestCRCustomSpec(t *testing.T) { r, err := NewResource(appApiVersion, appKind) if err != nil { t.Fatal(err) } s, buf := setupScaffoldAndWriter() - err = s.Execute(appConfig, &Cr{ + err = s.Execute(appConfig, &CR{ Resource: r, Spec: "# Custom spec here\ncustomSize: 6", }) diff --git a/pkg/scaffold/crd.go b/pkg/scaffold/crd.go index a1a386b86d6..30148e530a1 100644 --- a/pkg/scaffold/crd.go +++ b/pkg/scaffold/crd.go @@ -16,46 +16,178 @@ package scaffold import ( "fmt" + "io/ioutil" + "os" "path/filepath" "strings" + "sync" "github.com/operator-framework/operator-sdk/pkg/scaffold/input" + + "github.com/ghodss/yaml" + "github.com/spf13/afero" + apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + crdgenerator "sigs.k8s.io/controller-tools/pkg/crd/generator" ) -// Crd is the input needed to generate a deploy/crds/___crd.yaml file -type Crd struct { +// CRD is the input needed to generate a deploy/crds/___crd.yaml file +type CRD struct { input.Input // Resource defines the inputs for the new custom resource definition Resource *Resource + + // IsOperatorGo is true when the operator is written in Go. + IsOperatorGo bool } -func (s *Crd) GetInput() (input.Input, error) { +func (s *CRD) GetInput() (input.Input, error) { if s.Path == "" { fileName := fmt.Sprintf("%s_%s_%s_crd.yaml", strings.ToLower(s.Resource.Group), strings.ToLower(s.Resource.Version), s.Resource.LowerKind) - s.Path = filepath.Join(CrdsDir, fileName) + s.Path = filepath.Join(CRDsDir, fileName) } - s.TemplateBody = crdTemplate + initCache() return s.Input, nil } -// TODO: Parse pkg/apis to generate CRD with open-api validation instead of using a static template -const crdTemplate = `apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: {{ .Resource.Resource }}.{{ .Resource.FullGroup }} -spec: - group: {{ .Resource.FullGroup }} - names: - kind: {{ .Resource.Kind }} - listKind: {{ .Resource.Kind }}List - plural: {{ .Resource.Resource }} - singular: {{ .Resource.LowerKind }} - scope: Namespaced - version: {{ .Resource.Version }} - subresources: - status: {} -` +type fsCache struct { + afero.Fs +} + +func (c *fsCache) fileExists(path string) bool { + _, err := c.Stat(path) + return err == nil +} + +var ( + // Global cache so users can use new CRD structs. + cache *fsCache + once sync.Once +) + +func initCache() { + once.Do(func() { + cache = &fsCache{Fs: afero.NewMemMapFs()} + }) +} + +func (s *CRD) CustomRender() ([]byte, error) { + i, _ := s.GetInput() + // controller-tools generates crd file names with no _crd.yaml suffix: + // __.yaml. + path := strings.Replace(filepath.Base(i.Path), "_crd.yaml", ".yaml", 1) + + // controller-tools' generators read and make crds for all apis in pkg/apis, + // so generate crds in a cached, in-memory fs to extract the data we need. + if s.IsOperatorGo && !cache.fileExists(path) { + g := &crdgenerator.Generator{ + RootPath: s.AbsProjectPath, + Domain: strings.SplitN(s.Resource.FullGroup, ".", 2)[1], + OutputDir: ".", + SkipMapValidation: false, + OutFs: cache, + } + if err := g.ValidateAndInitFields(); err != nil { + return nil, err + } + if err := g.Do(); err != nil { + return nil, err + } + } + + dstCRD := newCRDForResource(s.Resource) + // Get our generated crd's from the in-memory fs. If it doesn't exist in the + // fs, the corresponding API does not exist yet, so scaffold a fresh crd + // without a validation spec. + // If the crd exists in the fs, and a local crd exists, append the validation + // spec. If a local crd does not exist, use the generated crd. + if _, err := cache.Stat(path); err != nil && !os.IsNotExist(err) { + return nil, err + } else if err == nil { + b, err := afero.ReadFile(cache, path) + if err != nil { + return nil, err + } + dstCRD = &apiextv1beta1.CustomResourceDefinition{} + if err = yaml.Unmarshal(b, dstCRD); err != nil { + return nil, err + } + val := dstCRD.Spec.Validation.DeepCopy() + + // If the crd exists at i.Path, append the validation spec to its crd spec. + if _, err := os.Stat(i.Path); err == nil { + cb, err := ioutil.ReadFile(i.Path) + if err != nil { + return nil, err + } + if len(cb) > 0 { + dstCRD = &apiextv1beta1.CustomResourceDefinition{} + if err = yaml.Unmarshal(cb, dstCRD); err != nil { + return nil, err + } + dstCRD.Spec.Validation = val + } + } + // controller-tools does not set ListKind or Singular names. + dstCRD.Spec.Names = getCRDNamesForResource(s.Resource) + // Remove controller-tools default label. + delete(dstCRD.Labels, "controller-tools.k8s.io") + } + addCRDSubresource(dstCRD) + return getCRDBytes(dstCRD) +} + +func newCRDForResource(r *Resource) *apiextv1beta1.CustomResourceDefinition { + return &apiextv1beta1.CustomResourceDefinition{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apiextensions.k8s.io/v1beta1", + Kind: "CustomResourceDefinition", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: r.Resource + "." + r.FullGroup, + }, + Spec: apiextv1beta1.CustomResourceDefinitionSpec{ + Group: r.FullGroup, + Names: getCRDNamesForResource(r), + Scope: apiextv1beta1.NamespaceScoped, + Version: r.Version, + Subresources: &apiextv1beta1.CustomResourceSubresources{ + Status: &apiextv1beta1.CustomResourceSubresourceStatus{}, + }, + }, + } +} + +func getCRDNamesForResource(r *Resource) apiextv1beta1.CustomResourceDefinitionNames { + return apiextv1beta1.CustomResourceDefinitionNames{ + Kind: r.Kind, + ListKind: r.Kind + "List", + Plural: r.Resource, + Singular: r.LowerKind, + } +} + +func addCRDSubresource(crd *apiextv1beta1.CustomResourceDefinition) { + if crd.Spec.Subresources == nil { + crd.Spec.Subresources = &apiextv1beta1.CustomResourceSubresources{} + } + if crd.Spec.Subresources.Status == nil { + crd.Spec.Subresources.Status = &apiextv1beta1.CustomResourceSubresourceStatus{} + } +} + +func getCRDBytes(crd *apiextv1beta1.CustomResourceDefinition) ([]byte, error) { + // Remove the "status" field from yaml data, which causes a + // resource creation error. + crdMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(crd) + if err != nil { + return nil, err + } + delete(crdMap, "status") + return yaml.Marshal(&crdMap) +} diff --git a/pkg/scaffold/crd_test.go b/pkg/scaffold/crd_test.go index 81f626500ca..9894caabb2d 100644 --- a/pkg/scaffold/crd_test.go +++ b/pkg/scaffold/crd_test.go @@ -15,31 +15,114 @@ package scaffold import ( + "os" + "path/filepath" + "strings" "testing" "github.com/operator-framework/operator-sdk/internal/util/diffutil" + "github.com/operator-framework/operator-sdk/pkg/scaffold/input" ) -func TestCRD(t *testing.T) { +func TestCRDGoProject(t *testing.T) { + r, err := NewResource("cache.example.com/v1alpha1", "Memcached") + if err != nil { + t.Fatal(err) + } + s, buf := setupScaffoldAndWriter() + absPath, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + // Set the project and repo paths to {abs}/test/test-framework, which + // contains pkg/apis for the memcached-operator. + tfDir := filepath.Join("test", "test-framework") + pkgIdx := strings.Index(absPath, "pkg") + cfg := &input.Config{ + Repo: filepath.Join(absPath[strings.Index(absPath, "github.com"):pkgIdx], tfDir), + AbsProjectPath: filepath.Join(absPath[:pkgIdx], tfDir), + ProjectName: tfDir, + } + if err := os.Chdir(cfg.AbsProjectPath); err != nil { + t.Fatal(err) + } + defer func() { os.Chdir(absPath) }() + err = s.Execute(cfg, &CRD{Resource: r, IsOperatorGo: true}) + if err != nil { + t.Fatalf("Failed to execute the scaffold: (%v)", err) + } + + if crdGoExp != buf.String() { + diffs := diffutil.Diff(crdGoExp, buf.String()) + t.Fatalf("Expected vs actual differs.\n%v", diffs) + } +} + +const crdGoExp = `apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + creationTimestamp: null + name: memcacheds.cache.example.com +spec: + group: cache.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + size: + format: int32 + type: integer + required: + - size + type: object + status: + properties: + nodes: + items: + type: string + type: array + required: + - nodes + type: object + version: v1alpha1 +` + +func TestCRDNonGoProject(t *testing.T) { r, err := NewResource(appApiVersion, appKind) if err != nil { t.Fatal(err) } s, buf := setupScaffoldAndWriter() - err = s.Execute(appConfig, &Crd{Resource: r}) + err = s.Execute(appConfig, &CRD{Resource: r}) if err != nil { t.Fatalf("Failed to execute the scaffold: (%v)", err) } - if crdExp != buf.String() { - diffs := diffutil.Diff(crdExp, buf.String()) + if crdNonGoExp != buf.String() { + diffs := diffutil.Diff(crdNonGoExp, buf.String()) t.Fatalf("Expected vs actual differs.\n%v", diffs) } } -const crdExp = `apiVersion: apiextensions.k8s.io/v1beta1 +const crdNonGoExp = `apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: + creationTimestamp: null name: appservices.app.example.com spec: group: app.example.com @@ -49,7 +132,7 @@ spec: plural: appservices singular: appservice scope: Namespaced - version: v1alpha1 subresources: status: {} + version: v1alpha1 ` diff --git a/pkg/scaffold/gopkgtoml.go b/pkg/scaffold/gopkgtoml.go index c92932988cc..18293238412 100644 --- a/pkg/scaffold/gopkgtoml.go +++ b/pkg/scaffold/gopkgtoml.go @@ -49,7 +49,9 @@ required = [ "k8s.io/code-generator/cmd/client-gen", "k8s.io/code-generator/cmd/lister-gen", "k8s.io/code-generator/cmd/informer-gen", + "k8s.io/kube-openapi/cmd/openapi-gen", "k8s.io/gengo/args", + "sigs.k8s.io/controller-tools/pkg/crd/generator", ] [[override]] @@ -57,6 +59,18 @@ required = [ # revision for tag "kubernetes-1.13.1" revision = "c2090bec4d9b1fb25de3812f868accc2bc9ecbae" +[[override]] + name = "k8s.io/kube-openapi" + revision = "0cf8f7e6ed1d2e3d47d02e3b6e559369af24d803" + +[[override]] + name = "github.com/go-openapi/spec" + branch = "master" + +[[override]] + name = "sigs.k8s.io/controller-tools" + version = "=v0.1.8" + [[override]] name = "k8s.io/api" # revision for tag "kubernetes-1.13.1" @@ -91,10 +105,6 @@ required = [ branch = "master" #osdk_branch_annotation # version = "=v0.4.0" #osdk_version_annotation -[[override]] - name = "k8s.io/kube-openapi" - revision = "0cf8f7e6ed1d2e3d47d02e3b6e559369af24d803" - [prune] go-tests = true non-go = true @@ -102,6 +112,10 @@ required = [ [[prune.project]] name = "k8s.io/code-generator" non-go = false + + [[prune.project]] + name = "k8s.io/gengo" + non-go = false ` func PrintDepsAsFile() { diff --git a/pkg/scaffold/gopkgtoml_test.go b/pkg/scaffold/gopkgtoml_test.go index c98f41256cf..aa2093b1846 100644 --- a/pkg/scaffold/gopkgtoml_test.go +++ b/pkg/scaffold/gopkgtoml_test.go @@ -41,7 +41,9 @@ required = [ "k8s.io/code-generator/cmd/client-gen", "k8s.io/code-generator/cmd/lister-gen", "k8s.io/code-generator/cmd/informer-gen", + "k8s.io/kube-openapi/cmd/openapi-gen", "k8s.io/gengo/args", + "sigs.k8s.io/controller-tools/pkg/crd/generator", ] [[override]] @@ -49,6 +51,18 @@ required = [ # revision for tag "kubernetes-1.13.1" revision = "c2090bec4d9b1fb25de3812f868accc2bc9ecbae" +[[override]] + name = "k8s.io/kube-openapi" + revision = "0cf8f7e6ed1d2e3d47d02e3b6e559369af24d803" + +[[override]] + name = "github.com/go-openapi/spec" + branch = "master" + +[[override]] + name = "sigs.k8s.io/controller-tools" + version = "=v0.1.8" + [[override]] name = "k8s.io/api" # revision for tag "kubernetes-1.13.1" @@ -83,10 +97,6 @@ required = [ branch = "master" #osdk_branch_annotation # version = "=v0.4.0" #osdk_version_annotation -[[override]] - name = "k8s.io/kube-openapi" - revision = "0cf8f7e6ed1d2e3d47d02e3b6e559369af24d803" - [prune] go-tests = true non-go = true @@ -94,4 +104,8 @@ required = [ [[prune.project]] name = "k8s.io/code-generator" non-go = false + + [[prune.project]] + name = "k8s.io/gengo" + non-go = false ` diff --git a/pkg/scaffold/olm-catalog/config.go b/pkg/scaffold/olm-catalog/config.go index 5d862bb18ea..a8318f62e4b 100644 --- a/pkg/scaffold/olm-catalog/config.go +++ b/pkg/scaffold/olm-catalog/config.go @@ -77,7 +77,7 @@ func (c *CSVConfig) setFields() error { } if len(c.CRDCRPaths) == 0 { - paths, err := getManifestPathsFromDir(scaffold.CrdsDir) + paths, err := getManifestPathsFromDir(scaffold.CRDsDir) if err != nil { return err } diff --git a/pkg/scaffold/olm-catalog/config_test.go b/pkg/scaffold/olm-catalog/config_test.go index c1dd800663e..da5582493fc 100644 --- a/pkg/scaffold/olm-catalog/config_test.go +++ b/pkg/scaffold/olm-catalog/config_test.go @@ -23,7 +23,7 @@ import ( ) func TestConfig(t *testing.T) { - crdsDir := filepath.Join(testDataDir, scaffold.CrdsDir) + crdsDir := filepath.Join(testDataDir, scaffold.CRDsDir) cfg := &CSVConfig{ CRDCRPaths: []string{crdsDir}, diff --git a/pkg/scaffold/types.go b/pkg/scaffold/types.go index ccf5d7d4364..c4bacca9f78 100644 --- a/pkg/scaffold/types.go +++ b/pkg/scaffold/types.go @@ -52,15 +52,19 @@ import ( // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // {{.Resource.Kind}}Spec defines the desired state of {{.Resource.Kind}} +// +k8s:openapi-gen=true type {{.Resource.Kind}}Spec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file + // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html } // {{.Resource.Kind}}Status defines the observed state of {{.Resource.Kind}} +// +k8s:openapi-gen=true type {{.Resource.Kind}}Status struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file + // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/scaffold/types_test.go b/pkg/scaffold/types_test.go index 984d540c141..b0c3bc68381 100644 --- a/pkg/scaffold/types_test.go +++ b/pkg/scaffold/types_test.go @@ -47,15 +47,19 @@ import ( // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // AppServiceSpec defines the desired state of AppService +// +k8s:openapi-gen=true type AppServiceSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file + // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html } // AppServiceStatus defines the observed state of AppService +// +k8s:openapi-gen=true type AppServiceStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file + // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/test/e2e/memcached_test.go b/test/e2e/memcached_test.go index a956aa2af97..4c9a847cf61 100644 --- a/test/e2e/memcached_test.go +++ b/test/e2e/memcached_test.go @@ -153,11 +153,17 @@ func TestMemcached(t *testing.T) { // t.Fatalf("Could not write deploy/operator.yaml: %v", err) // } - cmdOut, err = exec.Command("operator-sdk", + cmd := exec.Command("operator-sdk", "add", "api", "--api-version=cache.example.com/v1alpha1", - "--kind=Memcached").CombinedOutput() + "--kind=Memcached") + // Generators will print errors if -v is set. + if !projutil.IsGoVerbose() { + os.Setenv(projutil.GoFlagsEnv, os.Getenv(projutil.GoFlagsEnv)+" -v") + } + cmd.Env = os.Environ() + cmdOut, err = cmd.CombinedOutput() if err != nil { t.Fatalf("Error: %v\nCommand Output: %s\n", err, string(cmdOut)) }