diff --git a/changelog/fragments/remove-add-crd.yaml b/changelog/fragments/remove-add-crd.yaml new file mode 100644 index 0000000000..841731cb94 --- /dev/null +++ b/changelog/fragments/remove-add-crd.yaml @@ -0,0 +1,7 @@ +entries: + - description: Remove the `add crd` subcommand. + kind: removal + breaking: true + migration: + header: Remove the `add crd` subcommand + body: TBD diff --git a/cmd/operator-sdk/add/cmd.go b/cmd/operator-sdk/add/cmd.go deleted file mode 100644 index 37bb058eca..0000000000 --- a/cmd/operator-sdk/add/cmd.go +++ /dev/null @@ -1,30 +0,0 @@ -// 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 add - -import ( - "github.com/spf13/cobra" -) - -func NewCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "add", - Short: "Adds a controller or resource to the project", - Long: "", - } - - cmd.AddCommand(newAddCRDCmd()) - return cmd -} diff --git a/cmd/operator-sdk/add/crd.go b/cmd/operator-sdk/add/crd.go deleted file mode 100644 index 9f4628c57b..0000000000 --- a/cmd/operator-sdk/add/crd.go +++ /dev/null @@ -1,163 +0,0 @@ -// 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 add - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - - gencrd "github.com/operator-framework/operator-sdk/internal/generate/crd" - "github.com/operator-framework/operator-sdk/internal/scaffold" - "github.com/operator-framework/operator-sdk/internal/scaffold/input" - "github.com/operator-framework/operator-sdk/internal/util/projutil" -) - -var ( - apiVersion string - kind string - crdVersion string -) - -// newAddCRDCmd - add crd command -func newAddCRDCmd() *cobra.Command { - crdCmd := &cobra.Command{ - Deprecated: `use 'operator-sdk add api' instead to create or update API definitions. -Run 'operator-sdk add api --help' for more details. - `, - Use: "crd", - Short: "Adds a Custom Resource Definition (CRD) and the Custom Resource (CR) files", - Long: `The operator-sdk add crd command will create a Custom Resource Definition (CRD)` + - `and the Custom Resource (CR) files for the specified api-version and kind. - -Generated CRD filename: /deploy/crds/__crd.yaml -Generated CR filename: /deploy/crds/___cr.yaml - - /deploy path must already exist - --api-version and --kind are required flags to generate the new operator application. -`, - RunE: crdFunc, - } - crdCmd.Flags().StringVar(&apiVersion, "api-version", "", - "Kubernetes apiVersion and has a format of $GROUP_NAME/$VERSION (e.g app.example.com/v1alpha1)") - if err := crdCmd.MarkFlagRequired("api-version"); err != nil { - log.Fatalf("Failed to mark `api-version` flag for `add crd` subcommand as required") - } - crdCmd.Flags().StringVar(&kind, "kind", "", - "Kubernetes CustomResourceDefintion kind. (e.g AppService)") - if err := crdCmd.MarkFlagRequired("kind"); err != nil { - log.Fatalf("Failed to mark `kind` flag for `add crd` subcommand as required") - } - crdCmd.Flags().StringVar(&crdVersion, "crd-version", gencrd.DefaultCRDVersion, - "CRD version to generate") - return crdCmd -} - -func crdFunc(cmd *cobra.Command, args []string) error { - projutil.MustInProjectRoot() - - cfg := &input.Config{ - AbsProjectPath: projutil.MustGetwd(), - } - if projutil.IsOperatorGo() { - cfg.Repo = projutil.GetGoPkg() - } - - if len(args) != 0 { - return fmt.Errorf("command %s doesn't accept any arguments", cmd.CommandPath()) - } - if err := verifyCRDFlags(); err != nil { - return err - } - if err := verifyCRDDeployPath(); err != nil { - return err - } - - log.Infof("Generating CustomResourceDefinition (CRD) version %s for kind %s.", apiVersion, kind) - - // generate CR/CRD file - resource, err := scaffold.NewResource(apiVersion, kind) - if err != nil { - log.Fatal(err) - } - - s := scaffold.Scaffold{} - err = s.Execute(cfg, - &scaffold.CR{ - Input: input.Input{IfExistsAction: input.Skip}, - Resource: resource, - }, - ) - if err != nil { - log.Fatalf("CRD scaffold failed: %v", err) - } - - // This command does not consider an APIs dir. Instead it adds a plain CRD - // for the provided resource. We can use NewCRDNonGo to get this behavior. - crd := gencrd.Generator{ - IsOperatorGo: false, - Resource: *resource, - CRDVersion: crdVersion, - } - if err := crd.Generate(); err != nil { - log.Fatalf("Error generating CRD for %s: %v", resource, err) - } - - // update deploy/role.yaml for the given resource r. - if err := scaffold.UpdateRoleForResource(resource, cfg.AbsProjectPath); err != nil { - log.Fatalf("Failed to update the RBAC manifest for the resource (%v, %v): (%v)", - resource.APIVersion, resource.Kind, err) - } - - log.Info("CRD generation complete.") - return nil -} - -func verifyCRDFlags() error { - if len(apiVersion) == 0 { - return fmt.Errorf("value of --api-version must not have empty value") - } - if len(kind) == 0 { - return fmt.Errorf("value of --kind must not have empty value") - } - kindFirstLetter := string(kind[0]) - if kindFirstLetter != strings.ToUpper(kindFirstLetter) { - return fmt.Errorf("value of --kind must start with an uppercase letter") - } - if strings.Count(apiVersion, "/") != 1 { - return fmt.Errorf("value of --api-version has wrong format (%v);"+ - "format must be $GROUP_NAME/$VERSION(e.g app.example.com/v1alpha1)", apiVersion) - } - return nil -} - -// verifyCRDDeployPath checks if the /deploy directory exists. -func verifyCRDDeployPath() error { - wd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to determine the full path of the current directory: %v", err) - } - // check if the deploy sub-directory exist - _, err = os.Stat(filepath.Join(wd, scaffold.DeployDir)) - if err != nil { - return fmt.Errorf("the path (./%v) does not exist. run this command in your project directory", - scaffold.DeployDir) - } - return nil -} diff --git a/internal/generate/crd/crd.go b/internal/generate/crd/crd.go deleted file mode 100644 index ded43c6d84..0000000000 --- a/internal/generate/crd/crd.go +++ /dev/null @@ -1,404 +0,0 @@ -// Copyright 2019 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 crd - -import ( - "bytes" - "errors" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "sort" - "strings" - - log "github.com/sirupsen/logrus" - "github.com/spf13/afero" - apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" - apiextinstall "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install" - apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer" - "sigs.k8s.io/yaml" - - "github.com/operator-framework/operator-sdk/internal/generate/gen" - "github.com/operator-framework/operator-sdk/internal/scaffold" - "github.com/operator-framework/operator-sdk/internal/util/fileutil" - "github.com/operator-framework/operator-sdk/internal/util/k8sutil" -) - -const DefaultCRDVersion = "v1" - -// Generator configures the CustomResourceDefintion manifest generator -// for Go and non-Go projects. -type Generator struct { - // OperatorName is the operator's name, ex. app-operator - OperatorName string - // OutputDir is the root directory where the output files will be generated. - OutputDir string - // isOperatorGo is true when the operator is written in Go. - IsOperatorGo bool - // resource contains API information used to configure single-CRD generation. - // This is only required when isOperatorGo is false. - Resource scaffold.Resource - // crdVersion is the API version of the CRD that will be generated. - // Should be one of [v1, v1beta1] - CRDVersion string - // CRDsDir is for the location of the CRD manifests directory e.g "deploy/crds" - // Both the CRD and CR manifests from this path will be used to populate CSV fields - // metadata.annotations.alm-examples for CR examples - // and spec.customresourcedefinitions.owned for owned CRDs - CRDsDir string - // ApisDir is for the location of the API types directory e.g "pkg/apis" - // The CSV annotation comments will be parsed from the types under this path. - ApisDir string -} - -func (g Generator) validate() error { - if g.CRDsDir == "" { - return errors.New("input CRDs dir cannot be empty") - } - if g.IsOperatorGo && g.ApisDir == "" { - return errors.New("input APIs dir cannot be empty") - } - if g.OutputDir == "" { - return errors.New("output dir cannot be empty") - } - if !g.IsOperatorGo { - if err := g.Resource.Validate(); err != nil { - return fmt.Errorf("resource is invalid: %w", err) - } - } - switch g.CRDVersion { - case "v1", "v1beta1": - default: - return fmt.Errorf("crd version %q is invalid", g.CRDVersion) - } - return nil -} - -// Generate generates CRD manifests and writes them to g.OutputDir. -func (g Generator) Generate() (err error) { - if g.CRDsDir == "" { - g.CRDsDir = scaffold.CRDsDir - } - if g.ApisDir == "" { - g.ApisDir = scaffold.ApisDir - } - if g.OutputDir == "" { - g.OutputDir = g.CRDsDir - } - if err = g.validate(); err != nil { - return fmt.Errorf("error validating generator configuration: %w", err) - } - var fileMap map[string][]byte - if g.IsOperatorGo { - fileMap, err = g.generateGo() - } else { - fileMap, err = g.generateNonGo() - } - if err != nil { - return fmt.Errorf("error generating CRD manifests: %w", err) - } - if err = os.MkdirAll(g.OutputDir, fileutil.DefaultDirFileMode); err != nil { - return fmt.Errorf("error mkdir %s: %w", g.OutputDir, err) - } - for fileName, b := range fileMap { - path := filepath.Join(g.OutputDir, fileName) - if err := ioutil.WriteFile(path, b, fileutil.DefaultFileMode); err != nil { - return fmt.Errorf("error writing CRD manifests: %w", err) - } - } - return nil -} - -func getFileNameForResource(r scaffold.Resource) string { - return fmt.Sprintf("%s_%s_crd.yaml", r.FullGroup, r.Resource) -} - -// generateGo generates CRDs for Go projects using Go API files. -func (g Generator) generateGo() (map[string][]byte, error) { - fileMap := map[string][]byte{} - // Generate files in the generator's cache so we can modify the file name - // and annotations. - defName := "output:crd:cache" - cacheOutputDir := filepath.Clean(g.OutputDir) - rawOpts := []string{ - fmt.Sprintf("crd:crdVersions={%s}", g.CRDVersion), - fmt.Sprintf("paths=%s/...", fileutil.DotPath(g.ApisDir)), - fmt.Sprintf("%s:dir=%s", defName, cacheOutputDir), - } - - runner := gen.NewCachedRunner() - runner.AddOutputRule(defName, gen.OutputToCachedDirectory{}) - if err := runner.Run(rawOpts); err != nil { - return nil, fmt.Errorf("error running CRD generator: %w", err) - } - cache := gen.GetCache() - infos, err := afero.ReadDir(cache, cacheOutputDir) - if err != nil { - return nil, fmt.Errorf("error reading CRD cache dir %s: %w", cacheOutputDir, err) - } - for _, info := range infos { - if info.IsDir() { - continue - } - path := filepath.Join(cacheOutputDir, info.Name()) - b, err := afero.ReadFile(cache, path) - if err != nil { - return nil, fmt.Errorf("error reading cached CRD file %s: %w", path, err) - } - scanner := k8sutil.NewYAMLScanner(bytes.NewBuffer(b)) - modifiedCRD := []byte{} - for scanner.Scan() { - crd := unstructured.Unstructured{} - if err = yaml.Unmarshal(scanner.Bytes(), &crd); err != nil { - return nil, fmt.Errorf("error unmarshalling CRD manifest %s: %w", path, err) - } - // controller-tools inserts an annotation and assumes that the binary - // that creates the CRD is controller-gen. In this case, we don't use - // controller-gen. Instead, we vendor and use the same library that - // controller-gen does. - // - // The value that gets populated in the annotation is based on the - // build info of the compiled binary, not on the version of the - // vendored controller-tools library. - // - // See: https://github.com/kubernetes-sigs/controller-tools/issues/348 - // - // TODO(joelanford): Sort out what to do with this. Until then, let's - // just remove it. - annotations := crd.GetAnnotations() - delete(annotations, "controller-gen.kubebuilder.io/version") - if len(annotations) == 0 { - annotations = nil - } - crd.SetAnnotations(annotations) - b, err := k8sutil.GetObjectBytes(&crd, yaml.Marshal) - if err != nil { - return nil, fmt.Errorf("error marshalling CRD %s: %w", crd.GetName(), err) - } - modifiedCRD = k8sutil.CombineManifests(modifiedCRD, b) - } - if err = scanner.Err(); err != nil { - return nil, fmt.Errorf("error scanning CRD manifest %s: %w", path, err) - } - if len(modifiedCRD) != 0 { - fileNameNoExt := strings.TrimSuffix(info.Name(), filepath.Ext(info.Name())) - fileMap[fileNameNoExt+"_crd.yaml"] = modifiedCRD - } - } - if len(fileMap) == 0 { - return nil, errors.New("no generated CRD files found") - } - return fileMap, nil -} - -// generateNonGo generates a CRD for non-Go projects using a resource. -func (g Generator) generateNonGo() (map[string][]byte, error) { - // Since this method is usually only run once, we can initialize this - // scheme when called. - scheme := runtime.NewScheme() - apiextinstall.Install(scheme) - dec := serializer.NewCodecFactory(scheme).UniversalDeserializer() - - var crd *apiextv1beta1.CustomResourceDefinition - var preserveUnknownFields *bool - fileMap := map[string][]byte{} - fileName := getFileNameForResource(g.Resource) - path := filepath.Join(g.CRDsDir, fileName) - if _, err := os.Stat(path); err != nil { - if !os.IsNotExist(err) { - return nil, fmt.Errorf("error stating CRD file %s: %w", path, err) - } - crd = newCRDForResource(g.Resource) - } else { - b, err := ioutil.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("error reading CRD file %s: %w", path, err) - } - - // Decode the CRD manifest in a GVK-aware manner. - obj, _, err := dec.Decode(b, nil, nil) - if err != nil { - return nil, err - } - - switch t := obj.(type) { - case *apiextv1.CustomResourceDefinition: - if crd, err = convertv1Tov1beta1CustomResourceDefinition(t); err != nil { - return nil, fmt.Errorf("error converting CustomResourceDefinition v1 to v1beta1: %v", err) - } - case *apiextv1beta1.CustomResourceDefinition: - // Only set spec.preserveUnknownFields if it was set before conversion. - preserveUnknownFields = t.Spec.PreserveUnknownFields - crd = t - default: - return nil, fmt.Errorf("unrecognized type in CustomResourceDefinition getter: %T", t) - } - - // If version is new, append it to spec.versions. - hasVersion := false - for _, version := range crd.Spec.Versions { - if version.Name == g.Resource.Version { - hasVersion = true - break - } - } - if !hasVersion { - // Let either the user or below logic determine whether this new - // version is stored or not. - crd.Spec.Versions = append(crd.Spec.Versions, apiextv1beta1.CustomResourceDefinitionVersion{ - Name: g.Resource.Version, - Storage: false, - Served: true, - }) - } - } - - sort.Sort(k8sutil.CRDVersions(crd.Spec.Versions)) - setCRDStorageVersion(crd) - if err := checkCRDVersions(crd); err != nil { - return nil, fmt.Errorf("invalid version in CRD %s: %w", crd.GetName(), err) - } - - var ( - b []byte - err error - ) - switch g.CRDVersion { - case "v1beta1": - // If converting from v1, this will be nil and handled by a validation key. - // Otherwise this is a no-op. - crd.Spec.PreserveUnknownFields = preserveUnknownFields - b, err = k8sutil.GetObjectBytes(crd, yaml.Marshal) - case "v1": - out, cerr := k8sutil.Convertv1beta1Tov1CustomResourceDefinition(crd) - if cerr != nil { - return nil, fmt.Errorf("error converting CustomResourceDefinition v1beta1 to v1: %v", err) - } - b, err = k8sutil.GetObjectBytes(out, yaml.Marshal) - } - if err != nil { - return nil, fmt.Errorf("error marshalling CRD %s: %w", crd.GetName(), err) - } - - fileMap[fileName] = b - return fileMap, nil -} - -func convertv1Tov1beta1CustomResourceDefinition(in *apiextv1.CustomResourceDefinition) (*apiextv1beta1.CustomResourceDefinition, error) { - var unversioned apiext.CustomResourceDefinition - if err := apiextv1.Convert_v1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(in, &unversioned, nil); err != nil { - return nil, err - } - - var out apiextv1beta1.CustomResourceDefinition - out.TypeMeta.APIVersion = apiextv1beta1.SchemeGroupVersion.String() - out.TypeMeta.Kind = "CustomResourceDefinition" - if err := apiextv1beta1.Convert_apiextensions_CustomResourceDefinition_To_v1beta1_CustomResourceDefinition(&unversioned, &out, nil); err != nil { - return nil, err - } - return &out, nil -} - -// newCRDForResource constructs a barebones CRD using data in resource. -func newCRDForResource(r scaffold.Resource) *apiextv1beta1.CustomResourceDefinition { - trueVal := true - return &apiextv1beta1.CustomResourceDefinition{ - TypeMeta: metav1.TypeMeta{ - APIVersion: apiextv1beta1.SchemeGroupVersion.String(), - Kind: "CustomResourceDefinition", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s.%s", r.Resource, r.FullGroup), - }, - Spec: apiextv1beta1.CustomResourceDefinitionSpec{ - Group: r.FullGroup, - Scope: apiextv1beta1.NamespaceScoped, - Versions: []apiextv1beta1.CustomResourceDefinitionVersion{ - {Name: r.Version, Served: true, Storage: true}, - }, - Names: apiextv1beta1.CustomResourceDefinitionNames{ - Kind: r.Kind, - ListKind: r.Kind + "List", - Plural: r.Resource, - Singular: r.LowerKind, - }, - Subresources: &apiextv1beta1.CustomResourceSubresources{ - Status: &apiextv1beta1.CustomResourceSubresourceStatus{}, - }, - Validation: &apiextv1beta1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextv1beta1.JSONSchemaProps{ - Type: "object", - XPreserveUnknownFields: &trueVal, - }, - }, - }, - } -} - -// setCRDStorageVersion sets exactly one version's storage field to true if -// one is not already set. -func setCRDStorageVersion(crd *apiextv1beta1.CustomResourceDefinition) { - if len(crd.Spec.Versions) == 0 { - return - } - for _, ver := range crd.Spec.Versions { - if ver.Storage { - return - } - } - // Set the first element in spec.versions to be the storage version. - log.Infof("Setting CRD %q storage version to %s", crd.GetName(), crd.Spec.Versions[0].Name) - crd.Spec.Versions[0].Storage = true -} - -// checkCRDVersions ensures version(s) generated for a CRD are in valid format. -// From the Kubernetes CRD docs: -// -// The version field is deprecated and optional, but if it is not empty, -// it must match the first item in the versions field. -func checkCRDVersions(crd *apiextv1beta1.CustomResourceDefinition) error { - singleVer := crd.Spec.Version != "" - multiVers := len(crd.Spec.Versions) > 0 - if singleVer { - if !multiVers { - log.Warnf("CRD %s: spec.version is deprecated and should be migrated to spec.versions", - crd.Spec.Names.Kind) - } else if crd.Spec.Version != crd.Spec.Versions[0].Name { - return fmt.Errorf("spec.version %s must be the first element in spec.versions for CRD %s", - crd.Spec.Version, crd.Spec.Names.Kind) - } - } - - var hasStorageVer bool - for _, ver := range crd.Spec.Versions { - // There must be exactly one version flagged as a storage version. - if ver.Storage { - if hasStorageVer { - return fmt.Errorf("%s CRD has more than one storage version", crd.GetName()) - } - hasStorageVer = true - } - } - if multiVers && !hasStorageVer { - return fmt.Errorf("%s CRD has no storage version", crd.GetName()) - } - return nil -} diff --git a/internal/generate/crd/crd_test.go b/internal/generate/crd/crd_test.go deleted file mode 100644 index 6639a5d684..0000000000 --- a/internal/generate/crd/crd_test.go +++ /dev/null @@ -1,393 +0,0 @@ -// 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 crd - -import ( - "encoding/base32" - "math/rand" - "os" - "path" - "path/filepath" - "strconv" - "testing" - "time" - - gen "github.com/operator-framework/operator-sdk/internal/generate/gen" - "github.com/operator-framework/operator-sdk/internal/scaffold" - - "github.com/stretchr/testify/assert" -) - -const ( - testGroup = "cache" - testFullGroup = testGroup + ".example.com" - testVersion = "v1alpha1" - testKind = "Memcached" -) - -var ( - testDataDir = filepath.Join("..", "testdata") - testGoDataDir = filepath.Join(testDataDir, "go") - testGoAPIDir = filepath.Join(testGoDataDir, "pkg", "apis", testGroup, testVersion) - testAPIVersion = path.Join(testFullGroup, testVersion) -) - -func init() { - rand.Seed(time.Now().UnixNano()) -} - -// randomString returns a base32-encoded random string, reasonably sized for -// directory creation. -func randomString() string { - rb := []byte(strconv.Itoa(rand.Int() % (2 << 20))) - return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(rb) -} - -func TestGenerate(t *testing.T) { - tmp := filepath.Join(os.TempDir(), randomString()) - if err := os.MkdirAll(tmp, 0755); err != nil { - t.Fatal(err) - } - defer func() { - if err := os.RemoveAll(tmp); err != nil { - t.Fatal(err) - } - }() - r, err := scaffold.NewResource(testAPIVersion, testKind) - if err != nil { - t.Fatal(err) - } - - cases := []struct { - description string - generator gen.Generator - wantErr bool - }{ - { - description: "Generate Go CRD", - generator: Generator{ - IsOperatorGo: true, - ApisDir: testGoAPIDir, - OutputDir: filepath.Join(tmp, randomString()), - CRDVersion: "v1beta1", - }, - wantErr: false, - }, - { - description: "Generate non-Go CRD", - generator: Generator{ - IsOperatorGo: false, - ApisDir: testGoAPIDir, - OutputDir: filepath.Join(tmp, randomString()), - CRDVersion: "v1beta1", - Resource: *r, - }, - wantErr: false, - }, - { - description: "invalid Go CRD version", - generator: Generator{ - IsOperatorGo: true, - ApisDir: testGoAPIDir, - OutputDir: filepath.Join(tmp, randomString()), - CRDVersion: "invalid", - }, - wantErr: true, - }, - { - description: "invalid non-Go CRD version", - generator: Generator{ - IsOperatorGo: false, - ApisDir: testGoAPIDir, - OutputDir: filepath.Join(tmp, randomString()), - CRDVersion: "invalid", - Resource: *r, - }, - wantErr: true, - }, - } - - for _, c := range cases { - t.Run(c.description, func(t *testing.T) { - err := c.generator.Generate() - if err != nil { - if c.wantErr { - return - } - t.Errorf("Wanted nil error, got: %v", err) - } - }) - } -} - -func TestCRDGo(t *testing.T) { - g := Generator{ - IsOperatorGo: true, - ApisDir: testGoAPIDir, - } - - r, err := scaffold.NewResource(testAPIVersion, testKind) - if err != nil { - t.Fatal(err) - } - - cases := []struct { - crdVersion string - expectedCRD string - }{ - {"v1beta1", crdCustomExpV1beta1}, - {"v1", crdCustomExpV1}, - } - - for _, c := range cases { - t.Run(c.crdVersion, func(t *testing.T) { - - g.CRDVersion = c.crdVersion - fileMap, err := g.generateGo() - if err != nil { - t.Fatalf("Failed to execute CRD generator: %v", err) - } - if b, ok := fileMap[getFileNameForResource(*r)]; !ok { - t.Errorf("Failed to generate CRDs for %s", r) - } else { - assert.Equal(t, c.expectedCRD, string(b)) - } - }) - } -} - -func TestCRDNonGo(t *testing.T) { - r, err := scaffold.NewResource(testAPIVersion, testKind) - if err != nil { - t.Fatal(err) - } - - cases := []struct { - description string - crdsDir string - crdVersion string - expCRD string - }{ - { - "non-existent v1beta1 CRD with default structural schema", - filepath.Join("not", "exist"), "v1beta1", crdNonGoDefaultExpV1beta1, - }, - { - "non-existent v1 CRD with default structural schema", - filepath.Join("not", "exist"), "v1", crdNonGoDefaultExpV1, - }, - { - "existing v1beta1 CRD with custom structural schema", - filepath.Join(testGoDataDir, scaffold.CRDsDir+"_v1beta1"), "v1beta1", crdCustomExpV1beta1, - }, - { - "existing v1 CRD with custom structural schema", - filepath.Join(testGoDataDir, scaffold.CRDsDir+"_v1"), "v1", crdCustomExpV1, - }, - { - "existing v1beta1 to v1 CRD with custom structural schema", - filepath.Join(testGoDataDir, scaffold.CRDsDir+"_v1beta1"), "v1", crdCustomExpV1, - }, - { - "existing v1 to v1beta1 CRD with custom structural schema", - filepath.Join(testGoDataDir, scaffold.CRDsDir+"_v1"), "v1beta1", crdCustomExpV1beta1, - }, - } - - for _, c := range cases { - t.Run(c.description, func(t *testing.T) { - g := Generator{ - CRDsDir: c.crdsDir, - Resource: *r, - CRDVersion: c.crdVersion, - IsOperatorGo: false, - } - fileMap, err := g.generateNonGo() - if err != nil { - t.Fatalf("Error executing CRD generator: %v", err) - } - if b, ok := fileMap[getFileNameForResource(*r)]; !ok { - t.Errorf("Failed to generate CRD for %s", r) - } else { - assert.Equal(t, c.expCRD, string(b)) - } - }) - } -} - -// crdNonGoDefaultExpV1beta1 is the default non-go v1beta1 CRD. Non-go projects don't have the -// luxury of kubebuilder annotations. -const crdNonGoDefaultExpV1beta1 = `apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - 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: - type: object - x-kubernetes-preserve-unknown-fields: true - versions: - - name: v1alpha1 - served: true - storage: true -` - -// crdNonGoDefaultExpV1 is the equivalent default non-go v1 CRD. Non-go projects don't have the -// luxury of kubebuilder annotations. -const crdNonGoDefaultExpV1 = `apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: memcacheds.cache.example.com -spec: - group: cache.example.com - names: - kind: Memcached - listKind: MemcachedList - plural: memcacheds - singular: memcached - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - type: object - x-kubernetes-preserve-unknown-fields: true - served: true - storage: true - subresources: - status: {} -` - -// crdCustomExpV1beta1 is a v1beta1 CRD with custom validation, either created manually or -// with Go API code annotations. -const crdCustomExpV1beta1 = `apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - 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: - description: Memcached is the Schema for the memcacheds API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: MemcachedSpec defines the desired state of Memcached - properties: - size: - description: Size is the size of the memcached deployment - format: int32 - type: integer - required: - - size - type: object - status: - description: MemcachedStatus defines the observed state of Memcached - properties: - nodes: - description: Nodes are the names of the memcached pods - items: - type: string - type: array - required: - - nodes - type: object - type: object - version: v1alpha1 - versions: - - name: v1alpha1 - served: true - storage: true -` - -// crdCustomExpV1 is the equivalent v1 CRD with custom validation, either created manually or -// with Go API code annotations. -const crdCustomExpV1 = `apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: memcacheds.cache.example.com -spec: - group: cache.example.com - names: - kind: Memcached - listKind: MemcachedList - plural: memcacheds - singular: memcached - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: Memcached is the Schema for the memcacheds API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: MemcachedSpec defines the desired state of Memcached - properties: - size: - description: Size is the size of the memcached deployment - format: int32 - type: integer - required: - - size - type: object - status: - description: MemcachedStatus defines the observed state of Memcached - properties: - nodes: - description: Nodes are the names of the memcached pods - items: - type: string - type: array - required: - - nodes - type: object - type: object - served: true - storage: true - subresources: - status: {} -` diff --git a/internal/generate/gen/generator.go b/internal/generate/gen/generator.go deleted file mode 100644 index 55089ce318..0000000000 --- a/internal/generate/gen/generator.go +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2019 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. - -// Code adapted from: -// https://github.com/kubernetes-sigs/controller-tools/blob/6eef398/cmd/controller-gen/main.go -package gen - -import ( - "fmt" - "sync" - - "github.com/spf13/afero" - crdgen "sigs.k8s.io/controller-tools/pkg/crd" - "sigs.k8s.io/controller-tools/pkg/genall" - "sigs.k8s.io/controller-tools/pkg/markers" -) - -// Generator can generate artifacts using data contained in the Generator. -type Generator interface { - // Generate invokes the Generator, usually writing a file to disk or memory - // depending on what output rules are set. - Generate() error -} - -// Runner runs a generator. -type Runner interface { - // AddOutputRule associates an OutputRule with a definition name in the - // Runner's generator. - AddOutputRule(string, genall.OutputRule) - // Run creates a generator runtime by passing in rawOpts, a raw set of option - // strings, and invokes the runtime. - Run([]string) error -} - -// cache contains all generated files from calls to cachedRunner.Run(). -// A global cache is necessary because individual output rules that use -// an in-memory filesystem set by the caller cannot propagate that filesystem -// instance to the underlying generator runtime. Only type information is -// propagated; all other fields are set by the runtime's parser. cache can -// be used by output rules defined here to access generated artifacts. -var cache afero.Fs - -func init() { - cache = afero.NewMemMapFs() -} - -// GetCache gets the singleton cache instance. -func GetCache() afero.Fs { - return cache -} - -// cachedRunner contains a set of pre-defined markers for generators and -// output rules from controller-tools used to configure a generator runtime. -// cachedRunner caches each file generated in a global cache, to both -// speed up generation for commands and work around the output rule system. -type cachedRunner struct { - // optionsRegistry contains all the marker definitions used to process - // option strings. - optionsRegistry *markers.Registry - // allGenerators maintains the list of all known generators. - allGenerators map[string]genall.Generator - // allOutputRules defines the list of all known output rules. - // Each output rule turns into two command line options: - // - output::
(per-generator output) - // - output: (default output) - allOutputRules map[string]genall.OutputRule - - once sync.Once -} - -// For lazy initialization. -func (g *cachedRunner) init() { - g.once.Do(func() { - for genName, gen := range g.allGenerators { - // make the generator options marker itself - defn := markers.Must(markers.MakeDefinition(genName, markers.DescribesPackage, gen)) - if err := g.optionsRegistry.Register(defn); err != nil { - panic(err) - } - // make per-generation output rule markers - for ruleName, rule := range g.allOutputRules { - ruleMarker := markers.Must(markers.MakeDefinition(fmt.Sprintf("output:%s:%s", genName, - ruleName), markers.DescribesPackage, rule)) - if err := g.optionsRegistry.Register(ruleMarker); err != nil { - panic(err) - } - } - } - // make "default output" output rule markers - for ruleName, rule := range g.allOutputRules { - ruleMarker := markers.Must(markers.MakeDefinition("output:"+ruleName, markers.DescribesPackage, rule)) - if err := g.optionsRegistry.Register(ruleMarker); err != nil { - panic(err) - } - } - // add in the common options markers - if err := genall.RegisterOptionsMarkers(g.optionsRegistry); err != nil { - panic(err) - } - }) -} - -// NewCachedRunner returns a cachedRunner with a set of default -// generators and output rules. The returned cachedRunner is lazily -// initialized. -func NewCachedRunner() Runner { - return &cachedRunner{ - optionsRegistry: &markers.Registry{}, - allGenerators: map[string]genall.Generator{ - "crd": crdgen.Generator{}, - }, - allOutputRules: map[string]genall.OutputRule{ - "dir": genall.OutputToDirectory(""), - }, - } -} - -// AddOutputRule adds a output rule definition to g's options registry. -func (g *cachedRunner) AddOutputRule(defName string, rule genall.OutputRule) { - ruleMarker := markers.Must(markers.MakeDefinition(defName, markers.DescribesPackage, rule)) - if err := g.optionsRegistry.Register(ruleMarker); err != nil { - panic(err) - } -} - -// Run creates a generator runtime by passing in rawOpts, a raw set of option -// strings, and invokes the runtime. rawOpts must contain CLI options that -// can be consumed by controller-gen. Note that only a subset of generators -// and rules that controller-gen supports are implemented. -func (g *cachedRunner) Run(rawOpts []string) error { - g.init() - rt, err := genall.FromOptions(g.optionsRegistry, rawOpts) - if err != nil { - return err - } - if len(rt.Generators) == 0 { - return fmt.Errorf("no generators specified") - } - if hadErrs := rt.Run(); hadErrs { - return fmt.Errorf("not all generators ran successfully") - } - return nil -} diff --git a/internal/generate/gen/rules.go b/internal/generate/gen/rules.go deleted file mode 100644 index 301f9b61e4..0000000000 --- a/internal/generate/gen/rules.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2019 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 gen - -import ( - "fmt" - "io" - "os" - "path/filepath" - - "sigs.k8s.io/controller-tools/pkg/genall" - "sigs.k8s.io/controller-tools/pkg/loader" -) - -// OutputToCachedDirectory configures a generator runtime to output files -// to a directory. The output option rule string is formatted as follows: -// -// - output:::dir (per-generator output) -// - output::dir (default output) -// -// where is the generator's registered string name and -// is the output rule's registered form string. See the CRD generator for -// an example of how this is used. -type OutputToCachedDirectory struct { - Dir string -} - -var _ genall.OutputRule = OutputToCachedDirectory{} - -// Open is used to generate a CRD manifest in cache at path. -func (o OutputToCachedDirectory) Open(_ *loader.Package, path string) (io.WriteCloser, error) { - if cache == nil { - return nil, fmt.Errorf("error opening %s in output rule: cache must be set", path) - } - if err := cache.MkdirAll(o.Dir, os.ModePerm); err != nil { - return nil, fmt.Errorf("error mkdir %v in output rule: %v", o.Dir, err) - } - dirPath := filepath.Join(o.Dir, path) - wc, err := cache.Create(dirPath) - if err != nil { - return nil, fmt.Errorf("error creating %v in output rule: %v", dirPath, err) - } - return wc, nil -} diff --git a/internal/genutil/crds.go b/internal/genutil/crds.go deleted file mode 100644 index 75c746c7f6..0000000000 --- a/internal/genutil/crds.go +++ /dev/null @@ -1,61 +0,0 @@ -// 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" - "path/filepath" - - gencrd "github.com/operator-framework/operator-sdk/internal/generate/crd" - "github.com/operator-framework/operator-sdk/internal/scaffold" - "github.com/operator-framework/operator-sdk/internal/util/projutil" - - log "github.com/sirupsen/logrus" -) - -// CRDGen generates CRDs for all APIs in pkg/apis. -func CRDGen(crdVersion string) error { - projutil.MustInProjectRoot() - - log.Info("Running CRD generator.") - - crd := gencrd.Generator{ - IsOperatorGo: true, - CRDVersion: crdVersion, - } - if err := crd.Generate(); err != nil { - return fmt.Errorf("error generating CRDs from APIs in %s: %w", scaffold.ApisDir, err) - } - - log.Info("CRD generation complete.") - return nil -} - -// GenerateCRDNonGo generates CRDs for Non-Go APIs(Eg., Ansible,Helm) -func GenerateCRDNonGo(projectName string, resource scaffold.Resource, crdVersion string) error { - crdsDir := filepath.Join(projectName, scaffold.CRDsDir) - crd := gencrd.Generator{ - CRDsDir: crdsDir, - OutputDir: crdsDir, - CRDVersion: crdVersion, - Resource: resource, - IsOperatorGo: false, - } - if err := crd.Generate(); err != nil { - return fmt.Errorf("error generating CRD for %s: %w", resource, err) - } - log.Info("Generated CustomResourceDefinition manifests.") - return nil -}