From 097233213428e2465b982101fb7acef89bcc5ef1 Mon Sep 17 00:00:00 2001 From: Eric Stroczynski Date: Mon, 18 May 2020 14:04:07 -0700 Subject: [PATCH 1/2] This commit adds 'generate bundle' which will generate a bundle format for a "new" project layout. cmd/operator-sdk/generate: bundle command; CustomResourceDefinition update behvior lives here internal/generate/clusterserviceversion: modular ClusterServiceVersion generator --- cmd/operator-sdk/cli/cli.go | 6 +- cmd/operator-sdk/cli/legacy.go | 2 +- cmd/operator-sdk/generate/bundle/bundle.go | 209 ++++++++++ cmd/operator-sdk/generate/bundle/cmd.go | 126 ++++++ cmd/operator-sdk/generate/cmd.go | 22 +- cmd/operator-sdk/generate/internal/genutil.go | 128 ++++++ .../clusterserviceversion.go | 248 ++++++++++++ .../clusterserviceversion_suite_test.go | 27 ++ .../clusterserviceversion_test.go | 379 ++++++++++++++++++ internal/generate/internal/genutil.go | 107 +++++ ...cached-operator.clusterserviceversion.yaml | 47 +++ ...ith-ui-metadata.clusterserviceversion.yaml | 58 +++ ...cached-operator.clusterserviceversion.yaml | 160 ++++++++ .../testdata/non-standard-layout/PROJECT | 10 + internal/util/kubebuilder/project.go | 16 + .../en/docs/cli/operator-sdk_generate.md | 2 +- 16 files changed, 1539 insertions(+), 8 deletions(-) create mode 100644 cmd/operator-sdk/generate/bundle/bundle.go create mode 100644 cmd/operator-sdk/generate/bundle/cmd.go create mode 100644 cmd/operator-sdk/generate/internal/genutil.go create mode 100644 internal/generate/clusterserviceversion/clusterserviceversion.go create mode 100644 internal/generate/clusterserviceversion/clusterserviceversion_suite_test.go create mode 100644 internal/generate/clusterserviceversion/clusterserviceversion_test.go create mode 100644 internal/generate/internal/genutil.go create mode 100644 internal/generate/testdata/clusterserviceversions/bases/memcached-operator.clusterserviceversion.yaml create mode 100644 internal/generate/testdata/clusterserviceversions/bases/with-ui-metadata.clusterserviceversion.yaml create mode 100644 internal/generate/testdata/clusterserviceversions/newlayout/manifests/memcached-operator.clusterserviceversion.yaml create mode 100644 internal/generate/testdata/non-standard-layout/PROJECT diff --git a/cmd/operator-sdk/cli/cli.go b/cmd/operator-sdk/cli/cli.go index 312cadb304..74f6a735c4 100644 --- a/cmd/operator-sdk/cli/cli.go +++ b/cmd/operator-sdk/cli/cli.go @@ -20,6 +20,7 @@ import ( "github.com/operator-framework/operator-sdk/cmd/operator-sdk/bundle" "github.com/operator-framework/operator-sdk/cmd/operator-sdk/cleanup" "github.com/operator-framework/operator-sdk/cmd/operator-sdk/completion" + "github.com/operator-framework/operator-sdk/cmd/operator-sdk/generate" "github.com/operator-framework/operator-sdk/cmd/operator-sdk/olm" "github.com/operator-framework/operator-sdk/cmd/operator-sdk/run" "github.com/operator-framework/operator-sdk/cmd/operator-sdk/version" @@ -39,15 +40,14 @@ var commands = []*cobra.Command{ // new.NewCmd() alpha.NewCmd(), + build.NewCmd(), bundle.NewCmd(), cleanup.NewCmd(), completion.NewCmd(), + generate.NewCmd(), olm.NewCmd(), run.NewCmd(), version.NewCmd(), - build.NewCmd(), - - // TODO(hasbro17): add generate csv command after aligning it for kubebuilder layout } func Run() error { diff --git a/cmd/operator-sdk/cli/legacy.go b/cmd/operator-sdk/cli/legacy.go index 9f008511a2..541575ce44 100644 --- a/cmd/operator-sdk/cli/legacy.go +++ b/cmd/operator-sdk/cli/legacy.go @@ -77,7 +77,7 @@ func GetCLIRoot() *cobra.Command { cleanup.NewCmd(), completion.NewCmd(), execentrypoint.NewCmd(), - generate.NewCmd(), + generate.NewCmdLegacy(), migrate.NewCmd(), new.NewCmd(), olm.NewCmd(), diff --git a/cmd/operator-sdk/generate/bundle/bundle.go b/cmd/operator-sdk/generate/bundle/bundle.go new file mode 100644 index 0000000000..a67d825529 --- /dev/null +++ b/cmd/operator-sdk/generate/bundle/bundle.go @@ -0,0 +1,209 @@ +// Copyright 2020 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 bundle + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/operator-framework/operator-registry/pkg/lib/bundle" + "sigs.k8s.io/kubebuilder/pkg/model/config" + + genutil "github.com/operator-framework/operator-sdk/cmd/operator-sdk/generate/internal" + gencsv "github.com/operator-framework/operator-sdk/internal/generate/clusterserviceversion" + "github.com/operator-framework/operator-sdk/internal/generate/collector" +) + +// setCommonDefaults sets defaults useful to all modes of this subcommand. +func (c *bundleCmd) setCommonDefaults(cfg *config.Config) { + if c.operatorName == "" { + c.operatorName = filepath.Base(cfg.Repo) + } +} + +// runKustomize generates kustomize bundle bases. +func (c bundleCmd) runKustomize(cfg *config.Config) error { + + if !c.quiet { + fmt.Println("Generating bundle manifest kustomize bases") + } + + defaultDir := filepath.Join("config", "bundle") + if c.inputDir == "" { + c.inputDir = defaultDir + } + if c.outputDir == "" { + c.outputDir = defaultDir + } + if c.apisDir == "" { + if cfg.MultiGroup { + c.apisDir = "apis" + } else { + c.apisDir = "api" + } + } + + csvGen := gencsv.Generator{ + OperatorName: c.operatorName, + OperatorType: genutil.PluginKeyToOperatorType(cfg.Layout), + } + opts := []gencsv.Option{ + gencsv.WithBase(c.inputDir, c.apisDir), + gencsv.WithBaseWriter(c.outputDir), + } + if err := csvGen.Generate(cfg, opts...); err != nil { + return fmt.Errorf("error generating ClusterServiceVersion: %v", err) + } + + if !c.quiet { + fmt.Println("Bases generated successfully in", c.outputDir) + } + + return nil +} + +// validateManifests validates c for bundle manifests generation. +func (c bundleCmd) validateManifests(*config.Config) (err error) { + if c.version != "" { + if err := genutil.ValidateVersion(c.version); err != nil { + return err + } + } + + if !genutil.IsPipeReader() { + if c.manifestRoot == "" { + return errors.New("--manifest-root must be set if not reading from stdin") + } + if c.crdsDir == "" { + return errors.New("--crds-dir must be set if not reading from stdin") + } + } + + if c.stdout { + if c.outputDir != "" { + return errors.New("--output-dir cannot be set if writing to stdout") + } + } + + return nil +} + +// runManifests generates bundle manifests. +func (c bundleCmd) runManifests(cfg *config.Config) (err error) { + + if !c.quiet && !c.stdout { + if c.version == "" { + fmt.Println("Generating bundle manifests") + } else { + fmt.Println("Generating bundle manifests version", c.version) + } + } + + defaultBundleDir := filepath.Join("config", "bundle") + if c.inputDir == "" { + c.inputDir = defaultBundleDir + } + if !c.stdout { + if c.outputDir == "" { + c.outputDir = defaultBundleDir + } + } + // Only regenerate API definitions once. + if c.apisDir == "" && !c.kustomize { + if cfg.MultiGroup { + c.apisDir = "apis" + } else { + c.apisDir = "api" + } + } + + col := &collector.Manifests{} + if genutil.IsPipeReader() { + if err := col.UpdateFromReader(os.Stdin); err != nil { + return err + } + } + if c.manifestRoot != "" { + if err := col.UpdateFromDirs(c.manifestRoot, c.crdsDir); err != nil { + return err + } + } + + csvGen := gencsv.Generator{ + OperatorName: c.operatorName, + OperatorType: genutil.PluginKeyToOperatorType(cfg.Layout), + Version: c.version, + Collector: col, + } + + stdout := genutil.NewMultiManifestWriter(os.Stdout) + opts := []gencsv.Option{ + gencsv.WithBase(c.inputDir, c.apisDir), + } + if c.stdout { + opts = append(opts, gencsv.WithWriter(stdout)) + } else { + opts = append(opts, gencsv.WithBundleWriter(c.outputDir)) + } + + if err := csvGen.Generate(cfg, opts...); err != nil { + return fmt.Errorf("error generating ClusterServiceVersion: %v", err) + } + + if c.stdout { + if err := genutil.WriteCRDs(stdout, col.CustomResourceDefinitions...); err != nil { + return err + } + } else { + dir := filepath.Join(c.outputDir, bundle.ManifestsDir) + if err := genutil.WriteCRDFiles(dir, col.CustomResourceDefinitions...); err != nil { + return err + } + } + + if !c.quiet && !c.stdout { + fmt.Println("Bundle manifests generated successfully in", c.outputDir) + } + + return nil +} + +// runMetadata generates a bundle.Dockerfile and bundle metadata. +func (c bundleCmd) runMetadata() error { + + directory := c.inputDir + if directory == "" { + directory = filepath.Join("config", "bundle", bundle.ManifestsDir) + } else { + directory = filepath.Join(directory, bundle.ManifestsDir) + } + outputDir := c.outputDir + if filepath.Clean(outputDir) == filepath.Clean(directory) { + outputDir = "" + } + + return c.generateMetadata(directory, outputDir) +} + +// generateMetadata wraps the operator-registry bundle Dockerfile/metadata generator. +func (c bundleCmd) generateMetadata(manifestsDir, outputDir string) error { + err := bundle.GenerateFunc(manifestsDir, outputDir, c.operatorName, c.channels, c.defaultChannel, c.overwrite) + if err != nil { + return fmt.Errorf("error generating bundle metadata: %v", err) + } + return nil +} diff --git a/cmd/operator-sdk/generate/bundle/cmd.go b/cmd/operator-sdk/generate/bundle/cmd.go new file mode 100644 index 0000000000..5ad550d19a --- /dev/null +++ b/cmd/operator-sdk/generate/bundle/cmd.go @@ -0,0 +1,126 @@ +// Copyright 2020 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 bundle + +import ( + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + kbutil "github.com/operator-framework/operator-sdk/internal/util/kubebuilder" +) + +//nolint:maligned +type bundleCmd struct { + // Options to turn on different parts of bundling. + kustomize bool + manifests bool + metadata bool + + // Common options. + operatorName string + version string + inputDir string + outputDir string + manifestRoot string + apisDir string + crdsDir string + stdout bool + quiet bool + + // Metadata options. + channels string + defaultChannel string + overwrite bool +} + +//nolint:lll +func NewCmd() *cobra.Command { + c := &bundleCmd{} + cmd := &cobra.Command{ + Use: "bundle", + Short: "Generates bundle data for the operator", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + return fmt.Errorf("command %s doesn't accept any arguments", cmd.CommandPath()) + } + + // Generate kustomize bases, manifests, and metadata by default if no + // flags are set so the default behavior is "do everything". + fs := cmd.Flags() + if !fs.Changed("kustomize") && !fs.Changed("metadata") && !fs.Changed("manifests") { + c.kustomize = true + c.manifests = true + c.metadata = true + } + + cfg, err := kbutil.ReadConfig() + if err != nil { + return fmt.Errorf("error reading configuration: %v", err) + } + c.setCommonDefaults(cfg) + + if c.kustomize { + if err = c.runKustomize(cfg); err != nil { + log.Fatalf("Error generating bundle bases: %v", err) + } + } + if c.manifests { + if err = c.validateManifests(cfg); err != nil { + return fmt.Errorf("invalid command options: %v", err) + } + if err = c.runManifests(cfg); err != nil { + log.Fatalf("Error generating bundle manifests: %v", err) + } + } + if c.metadata { + if err = c.runMetadata(); err != nil { + log.Fatalf("Error generating bundle metadata: %v", err) + } + } + + return nil + }, + } + + cmd.Flags().BoolVar(&c.kustomize, "kustomize", false, "Generate kustomize bases") + cmd.Flags().BoolVar(&c.manifests, "manifests", false, "Generate bundle manifests") + cmd.Flags().BoolVar(&c.metadata, "metadata", false, "Generate bundle metadata and Dockerfile") + cmd.Flags().BoolVar(&c.stdout, "stdout", false, "Write bundle manifest to stdout") + + c.addCommonFlagsTo(cmd.Flags()) + + return cmd +} + +// TODO(estroz): add flag to skip API metadata regeneration. +func (c *bundleCmd) addCommonFlagsTo(fs *pflag.FlagSet) { + fs.StringVar(&c.operatorName, "operator-name", "", "Name of the bundle's operator") + fs.StringVarP(&c.version, "version", "v", "", "Semantic version of the operator in the generated bundle. "+ + "Only set if creating a new bundle or upgrading your operator") + fs.StringVar(&c.inputDir, "input-dir", "", "Directory to read an existing bundle from. "+ + "This directory is the parent of your bundle 'manifests' directory, and different from --manifest-root") + fs.StringVar(&c.outputDir, "output-dir", "", "Directory to write the bundle to") + fs.StringVar(&c.manifestRoot, "manifest-root", "", "Root directory for operator manifests such as "+ + "Deployments and RBAC, ex. 'deploy' or 'config'. This directory is different from that passed to --input-dir") + fs.StringVar(&c.apisDir, "apis-dir", "", "Root directory for API type defintions") + fs.StringVar(&c.crdsDir, "crds-dir", "", "Root directory for CustomResoureDefinition manifests") + fs.StringVar(&c.channels, "channels", "alpha", "A comma-separated list of channels the bundle belongs to") + fs.StringVar(&c.defaultChannel, "default-channel", "", "The default channel for the bundle") + fs.BoolVar(&c.overwrite, "overwrite", false, "Overwrite the bundle's metadata and Dockerfile if they exist") + fs.BoolVarP(&c.quiet, "quiet", "q", false, "Run in quiet mode") +} diff --git a/cmd/operator-sdk/generate/cmd.go b/cmd/operator-sdk/generate/cmd.go index 1059ce582a..0b9740944f 100644 --- a/cmd/operator-sdk/generate/cmd.go +++ b/cmd/operator-sdk/generate/cmd.go @@ -16,15 +16,31 @@ package generate import ( "github.com/spf13/cobra" + + "github.com/operator-framework/operator-sdk/cmd/operator-sdk/generate/bundle" ) -func NewCmd() *cobra.Command { - cmd := &cobra.Command{ +func newCmd() *cobra.Command { + return &cobra.Command{ Use: "generate ", Short: "Invokes a specific generator", Long: `The 'operator-sdk generate' command invokes a specific generator to generate -code or manifests on disk.`, +code or manifests.`, } +} + +// NewCmdLegacy returns the 'generate' command configured for the new project layout. +func NewCmd() *cobra.Command { + cmd := newCmd() + cmd.AddCommand( + bundle.NewCmd(), + ) + return cmd +} + +// NewCmdLegacy returns the 'generate' command configured for the legacy project layout. +func NewCmdLegacy() *cobra.Command { + cmd := newCmd() cmd.AddCommand( newGenerateK8SCmd(), newGenerateCRDsCmd(), diff --git a/cmd/operator-sdk/generate/internal/genutil.go b/cmd/operator-sdk/generate/internal/genutil.go new file mode 100644 index 0000000000..cc6095e5e8 --- /dev/null +++ b/cmd/operator-sdk/generate/internal/genutil.go @@ -0,0 +1,128 @@ +// Copyright 2020 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 ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/blang/semver" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "sigs.k8s.io/yaml" + + "github.com/operator-framework/operator-sdk/internal/util/projutil" +) + +// ValidateVersion returns an error if version is not a strict semantic version. +func ValidateVersion(version string) error { + v, err := semver.Parse(version) + if err != nil { + return fmt.Errorf("%s is not a valid semantic version: %v", version, err) + } + // Ensures numerical values composing csvVersion don't contain leading 0's, + // ex. 01.01.01 + if v.String() != version { + return fmt.Errorf("provided CSV version %s contains bad values (parses to %s)", version, v) + } + return nil +} + +// IsPipeReader returns true if stdin is an open pipe, i.e. the caller can +// accept input from stdin. +func IsPipeReader() bool { + info, err := os.Stdin.Stat() + if err != nil { + return false + } + return info.Mode()&os.ModeNamedPipe != 0 +} + +// PluginKeyToOperatorType converts a plugin key string to an operator project +// type. +// TODO(estroz): this can probably be made more robust by checking known +// plugin keys directly. +func PluginKeyToOperatorType(pluginKey string) projutil.OperatorType { + switch { + case strings.HasPrefix(pluginKey, "go"): + return projutil.OperatorTypeGo + } + return "" +} + +// WriteCRDs writes each CustomResourceDefinition in crds to w. +func WriteCRDs(w io.Writer, crds ...v1beta1.CustomResourceDefinition) error { + for _, crd := range crds { + if err := writeCRD(w, crd); err != nil { + return err + } + } + return nil +} + +// WriteCRDFiles creates dir then writes each CustomResourceDefinition in crds +// to a file in dir. +func WriteCRDFiles(dir string, crds ...v1beta1.CustomResourceDefinition) error { + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } + for _, crd := range crds { + if err := writeCRDFile(dir, crd); err != nil { + return err + } + } + return nil +} + +// writeCRDFile marshals crd to bytes and writes them to dir in a file named +// _.yaml. +func writeCRDFile(dir string, crd v1beta1.CustomResourceDefinition) error { + file := fmt.Sprintf("%s_%s.yaml", crd.Spec.Group, crd.Spec.Names.Plural) + f, err := os.Create(filepath.Join(dir, file)) + if err != nil { + return err + } + defer f.Close() + return writeCRD(f, crd) +} + +// writeCRD marshals crd to bytes and writes them to w. +func writeCRD(w io.Writer, crd v1beta1.CustomResourceDefinition) error { + b, err := yaml.Marshal(crd) + if err != nil { + return err + } + _, err = w.Write(b) + return err +} + +// multiManifestWriter writes a multi-part manifest by prepending "---" +// to the argument of io.Writer.Write(). +type multiManifestWriter struct { + io.Writer +} + +func (w *multiManifestWriter) Write(b []byte) (int, error) { + return w.Writer.Write(append([]byte("\n---\n"), bytes.TrimSpace(b)...)) +} + +// NewMultiManifestWriter returns a multi-part manifest writer. Use this writer +// if writing a package or bundle to stdout or a single file. +func NewMultiManifestWriter(w io.Writer) io.Writer { + return &multiManifestWriter{w} +} diff --git a/internal/generate/clusterserviceversion/clusterserviceversion.go b/internal/generate/clusterserviceversion/clusterserviceversion.go new file mode 100644 index 0000000000..33b37c0bf7 --- /dev/null +++ b/internal/generate/clusterserviceversion/clusterserviceversion.go @@ -0,0 +1,248 @@ +// Copyright 2020 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 clusterserviceversion + +import ( + "fmt" + "io" + "path/filepath" + "strings" + + "github.com/blang/semver" + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/operator-framework/operator-registry/pkg/lib/bundle" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/kubebuilder/pkg/model/config" + + "github.com/operator-framework/operator-sdk/internal/generate/clusterserviceversion/bases" + "github.com/operator-framework/operator-sdk/internal/generate/collector" + genutil "github.com/operator-framework/operator-sdk/internal/generate/internal" + "github.com/operator-framework/operator-sdk/internal/util/projutil" +) + +const ( + // File extension for all ClusterServiceVersion manifests written by Generator. + csvYamlFileExt = ".clusterserviceversion.yaml" +) + +var ( + // Internal errors. + noGetBaseError = genutil.InternalError("getBase must be set") + noGetWriterError = genutil.InternalError("getWriter must be set") + baseVersionNotAllowedError = genutil.InternalError("cannot set version when generating a base") + baseCollectorNotAllowedError = genutil.InternalError("cannot set collector when generating a base") +) + +// ClusterServiceVersion configures ClusterServiceVersion manifest generation. +type Generator struct { + // OperatorName is the operator's name, ex. app-operator + OperatorName string + // OperatorType determines what code API types are written in for getBase. + OperatorType projutil.OperatorType + // Version is the CSV current version. + Version string + // Collector holds all manifests relevant to the Generator. + Collector *collector.Manifests + + // Project configuration. + config *config.Config + // Func that returns a base CSV. + getBase getBaseFunc + // Func that returns the writer the generated CSV's bytes are written to. + getWriter func() (io.Writer, error) + // If the CSV is destined for a bundle this will be the path of the updated + // CSV. Used to bring over data from an existing CSV that is not captured + // in a base. Not set if a non-file or base writer is returned by getWriter. + bundledPath string +} + +// Type of Generator.getBase. +type getBaseFunc func() (*operatorsv1alpha1.ClusterServiceVersion, error) + +// Option is a function that modifies a Generator. +type Option func(*Generator) error + +// WithBase sets a Generator's base CSV to a kustomize-style base. +func WithBase(inputDir, apisDir string) Option { + return func(g *Generator) error { + g.getBase = g.makeKustomizeBaseGetter(inputDir, apisDir) + return nil + } +} + +// WithWriter sets a Generator's writer to w. +func WithWriter(w io.Writer) Option { + return func(g *Generator) error { + g.getWriter = func() (io.Writer, error) { + return w, nil + } + return nil + } +} + +// WithBaseWriter sets a Generator's writer to a kustomize-style base file +// under /bases. +func WithBaseWriter(dir string) Option { + return func(g *Generator) error { + fileName := makeCSVFileName(g.OperatorName) + g.getWriter = func() (io.Writer, error) { + return genutil.Open(filepath.Join(dir, "bases"), fileName) + } + // Bases should not be updated with a version or manifests. + if g.Version != "" { + return baseVersionNotAllowedError + } + if g.Collector != nil { + return baseCollectorNotAllowedError + } + return nil + } +} + +// WithBundleWriter sets a Generator's writer to a bundle CSV file under +// /manifests. +func WithBundleWriter(dir string) Option { + return func(g *Generator) error { + fileName := makeCSVFileName(g.OperatorName) + g.bundledPath = filepath.Join(dir, bundle.ManifestsDir, fileName) + g.getWriter = func() (io.Writer, error) { + return genutil.Open(filepath.Join(dir, bundle.ManifestsDir), fileName) + } + return nil + } +} + +// Generate configures the generator with cfg and opts then runs it. +func (g *Generator) Generate(cfg *config.Config, opts ...Option) (err error) { + g.config = cfg + for _, opt := range opts { + if err = opt(g); err != nil { + return err + } + } + + if g.getWriter == nil { + return noGetWriterError + } + + csv, err := g.generate() + if err != nil { + return err + } + + w, err := g.getWriter() + if err != nil { + return err + } + return genutil.WriteObject(w, csv) +} + +// generate runs a configured Generator. +func (g *Generator) generate() (*operatorsv1alpha1.ClusterServiceVersion, error) { + if g.getBase == nil { + return nil, noGetBaseError + } + + base, err := g.getBase() + if err != nil { + return nil, fmt.Errorf("error getting ClusterServiceVersion base: %v", err) + } + + if err = g.updateVersions(base); err != nil { + return nil, err + } + + if g.Collector != nil { + if err := ApplyTo(g.Collector, base); err != nil { + return nil, err + } + } + + return base, nil +} + +// makeCSVFileName returns a CSV file name containing name. +func makeCSVFileName(name string) string { + return strings.ToLower(name) + csvYamlFileExt +} + +// makeKustomizeBaseGetter returns a function that gets a kustomize-style base. +func (g Generator) makeKustomizeBaseGetter(inputDir, apisDir string) getBaseFunc { + basePath := filepath.Join(inputDir, "bases", makeCSVFileName(g.OperatorName)) + if genutil.IsNotExist(basePath) { + basePath = "" + } + + return g.makeBaseGetter(basePath, apisDir) +} + +// makeBaseGetter returns a function that gets a base from inputDir. +// apisDir is used by getBaseFunc to populate base fields. +func (g Generator) makeBaseGetter(basePath, apisDir string) getBaseFunc { + gvks := make([]schema.GroupVersionKind, len(g.config.Resources)) + for i, gvk := range g.config.Resources { + gvks[i].Group = fmt.Sprintf("%s.%s", gvk.Group, g.config.Domain) + gvks[i].Version = gvk.Version + gvks[i].Kind = gvk.Kind + } + + return func() (*operatorsv1alpha1.ClusterServiceVersion, error) { + b := bases.ClusterServiceVersion{ + OperatorName: g.OperatorName, + OperatorType: g.OperatorType, + BasePath: basePath, + APIsDir: apisDir, + GVKs: gvks, + } + return b.GetBase() + } +} + +// updateVersions updates csv's version and data involving the version, +// ex. ObjectMeta.Name, and place the old version in the `replaces` object, +// if there is an old version to replace. +func (g Generator) updateVersions(csv *operatorsv1alpha1.ClusterServiceVersion) (err error) { + + oldVer, newVer := csv.Spec.Version.String(), g.Version + newName := genutil.MakeCSVName(g.OperatorName, newVer) + oldName := csv.GetName() + + // A bundled CSV may not have a base containing the previous version to use, + // so use the current bundled CSV for version information. + if genutil.IsExist(g.bundledPath) { + existing, err := (bases.ClusterServiceVersion{BasePath: g.bundledPath}).GetBase() + if err != nil { + return fmt.Errorf("error reading existing ClusterServiceVersion: %v", err) + } + oldVer = existing.Spec.Version.String() + oldName = existing.GetName() + } + + // If the new version is empty, either because a CSV is only being updated or + // a base was generated, no update is needed. + if newVer == "0.0.0" || newVer == "" { + return nil + } + + // Set replaces by default. + // TODO: consider all possible CSV versioning schemes supported by OLM. + if oldVer != "0.0.0" && newVer != oldVer { + csv.Spec.Replaces = oldName + } + + csv.SetName(newName) + csv.Spec.Version.Version, err = semver.Parse(newVer) + return err +} diff --git a/internal/generate/clusterserviceversion/clusterserviceversion_suite_test.go b/internal/generate/clusterserviceversion/clusterserviceversion_suite_test.go new file mode 100644 index 0000000000..7f841b1bba --- /dev/null +++ b/internal/generate/clusterserviceversion/clusterserviceversion_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2020 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 clusterserviceversion + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestGenerator(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Generator Suite") +} diff --git a/internal/generate/clusterserviceversion/clusterserviceversion_test.go b/internal/generate/clusterserviceversion/clusterserviceversion_test.go new file mode 100644 index 0000000000..db116e241b --- /dev/null +++ b/internal/generate/clusterserviceversion/clusterserviceversion_test.go @@ -0,0 +1,379 @@ +// Copyright 2020 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 clusterserviceversion + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/format" + + "github.com/blang/semver" + operatorversion "github.com/operator-framework/api/pkg/lib/version" + "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/operator-framework/operator-registry/pkg/lib/bundle" + appsv1 "k8s.io/api/apps/v1" + "sigs.k8s.io/kubebuilder/pkg/model/config" + "sigs.k8s.io/yaml" + + "github.com/operator-framework/operator-sdk/internal/generate/collector" + genutil "github.com/operator-framework/operator-sdk/internal/generate/internal" + kbutil "github.com/operator-framework/operator-sdk/internal/util/kubebuilder" + "github.com/operator-framework/operator-sdk/internal/util/projutil" +) + +var ( + testDataDir = filepath.Join("..", "testdata") + csvDir = filepath.Join(testDataDir, "clusterserviceversions") + csvBasesDir = filepath.Join(csvDir, "bases") + csvNewLayoutBundleDir = filepath.Join(csvDir, "newlayout", "manifests") + + // TODO: create a new testdata dir (top level?) that has both a "config" + // dir and a "deploy" dir that contains `kustomize build config/default` + // output to simulate actual manifest collection behavior. Using "config" + // directly is not standard behavior. + goTestDataDir = filepath.Join(testDataDir, "non-standard-layout") + goAPIsDir = filepath.Join(goTestDataDir, "api") + goManifestRootDir = filepath.Join(goTestDataDir, "config") + goCRDsDir = filepath.Join(goManifestRootDir, "crds") +) + +var ( + col *collector.Manifests + cfg *config.Config +) + +var ( + baseCSV, baseCSVUIMeta, newCSV *v1alpha1.ClusterServiceVersion + baseCSVStr, baseCSVUIMetaStr, newCSVStr string +) + +var _ = BeforeSuite(func() { + col = &collector.Manifests{} + Expect(col.UpdateFromDirs(goManifestRootDir, goCRDsDir)).ToNot(HaveOccurred()) + + cfg = readConfigHelper(goTestDataDir) + + initTestCSVsHelper() +}) + +var _ = Describe("Generating a ClusterServiceVersion", func() { + format.TruncatedDiff = true + format.UseStringerRepresentation = true + + var ( + g Generator + buf *bytes.Buffer + operatorName = "memcached-operator" + operatorType = projutil.OperatorTypeGo + version = "0.0.1" + ) + + BeforeEach(func() { + buf = &bytes.Buffer{} + }) + + Describe("for the new Go project layout", func() { + + Context("with correct Options", func() { + + var ( + tmp string + err error + ) + + BeforeEach(func() { + tmp, err = ioutil.TempDir(".", "") + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + if tmp != "" { + os.RemoveAll(tmp) + } + }) + + It("should write a ClusterServiceVersion manifest to an io.Writer", func() { + g = Generator{ + OperatorName: operatorName, + OperatorType: operatorType, + Version: version, + Collector: col, + } + opts := []Option{ + WithBase(csvBasesDir, goAPIsDir), + WithWriter(buf), + } + Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred()) + Expect(buf.String()).To(MatchYAML(newCSVStr)) + }) + It("should write a ClusterServiceVersion manifest to a base file", func() { + g = Generator{ + OperatorName: operatorName, + OperatorType: operatorType, + } + opts := []Option{ + WithBase(csvBasesDir, goAPIsDir), + WithBaseWriter(tmp), + } + Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred()) + outputFile := filepath.Join(tmp, "bases", makeCSVFileName("memcached-operator")) + Expect(outputFile).To(BeAnExistingFile()) + Expect(string(readFileHelper(outputFile))).To(MatchYAML(baseCSVUIMetaStr)) + }) + It("should write a ClusterServiceVersion manifest to a bundle file", func() { + g = Generator{ + OperatorName: operatorName, + OperatorType: operatorType, + Version: version, + Collector: col, + } + opts := []Option{ + WithBase(csvBasesDir, goAPIsDir), + WithBundleWriter(tmp), + } + Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred()) + outputFile := filepath.Join(tmp, bundle.ManifestsDir, makeCSVFileName("memcached-operator")) + Expect(outputFile).To(BeAnExistingFile()) + Expect(string(readFileHelper(outputFile))).To(MatchYAML(newCSVStr)) + }) + }) + + Context("with incorrect Options", func() { + + BeforeEach(func() { + g = Generator{ + OperatorName: operatorName, + OperatorType: operatorType, + Version: version, + Collector: col, + } + }) + + It("should return an error without any Options", func() { + opts := []Option{} + Expect(g.Generate(cfg, opts...)).To(MatchError(noGetWriterError)) + }) + It("should return an error without a getWriter", func() { + opts := []Option{ + WithBase(csvBasesDir, goAPIsDir), + } + Expect(g.Generate(cfg, opts...)).To(MatchError(noGetWriterError)) + }) + It("should return an error without a getBase", func() { + opts := []Option{ + WithWriter(&bytes.Buffer{}), + } + Expect(g.Generate(cfg, opts...)).To(MatchError(noGetBaseError)) + }) + }) + + Context("to create a new", func() { + + Context("bundle base", func() { + It("should return the default base object", func() { + g = Generator{ + OperatorName: operatorName, + OperatorType: operatorType, + config: cfg, + getBase: makeBaseGetter(baseCSV), + } + csv, err := g.generate() + Expect(err).ToNot(HaveOccurred()) + Expect(csv).To(Equal(baseCSV)) + }) + It("should return a base object with customresourcedefinitions", func() { + g = Generator{ + OperatorName: operatorName, + OperatorType: operatorType, + config: cfg, + getBase: makeBaseGetter(baseCSVUIMeta), + } + csv, err := g.generate() + Expect(err).ToNot(HaveOccurred()) + Expect(csv).To(Equal(baseCSVUIMeta)) + }) + }) + + Context("bundle", func() { + It("should return the expected object", func() { + g = Generator{ + OperatorName: operatorName, + OperatorType: operatorType, + Version: version, + Collector: col, + config: cfg, + getBase: makeBaseGetter(baseCSVUIMeta), + } + csv, err := g.generate() + Expect(err).ToNot(HaveOccurred()) + Expect(csv).To(Equal(newCSV)) + }) + }) + + }) + + Context("to update an existing", func() { + Context("bundle", func() { + It("should return the expected object", func() { + g = Generator{ + OperatorName: operatorName, + OperatorType: operatorType, + Version: version, + Collector: &collector.Manifests{}, + config: cfg, + getBase: makeBaseGetter(newCSV), + } + // Update the input's and expected CSV's Deployment image. + Expect(g.Collector.UpdateFromDirs(goManifestRootDir, goCRDsDir)).ToNot(HaveOccurred()) + Expect(len(g.Collector.Deployments)).To(BeNumerically(">=", 1)) + imageTag := "controller:v" + g.Version + modifyDepImageHelper(&g.Collector.Deployments[0].Spec, imageTag) + updatedCSV := updateCSV(newCSV, modifyCSVDepImageHelper(imageTag)) + + csv, err := g.generate() + Expect(err).ToNot(HaveOccurred()) + Expect(csv).To(Equal(updatedCSV)) + }) + }) + + }) + + Context("to upgrade an existing", func() { + + Context("bundle", func() { + It("should return the expected manifest", func() { + g = Generator{ + OperatorName: operatorName, + OperatorType: operatorType, + Version: "0.0.2", + Collector: col, + config: cfg, + getBase: makeBaseGetter(newCSV), + // Bundles need a path, usually set by an Option, to an existing + // CSV manifest so "replaces" can be set correctly. + bundledPath: filepath.Join(csvNewLayoutBundleDir, "memcached-operator.clusterserviceversion.yaml"), + } + csv, err := g.generate() + Expect(err).ToNot(HaveOccurred()) + Expect(csv).To(Equal(upgradeCSV(newCSV, g.OperatorName, g.Version))) + }) + }) + + }) + + }) + +}) + +func readConfigHelper(dir string) *config.Config { + wd, err := os.Getwd() + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + ExpectWithOffset(1, os.Chdir(dir)).ToNot(HaveOccurred()) + cfg, err := kbutil.ReadConfig() + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + ExpectWithOffset(1, os.Chdir(wd)).ToNot(HaveOccurred()) + return cfg +} + +func initTestCSVsHelper() { + var err error + path := filepath.Join(csvBasesDir, "memcached-operator.clusterserviceversion.yaml") + baseCSV, baseCSVStr, err = getCSVFromFile(path) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + path = filepath.Join(csvBasesDir, "with-ui-metadata.clusterserviceversion.yaml") + baseCSVUIMeta, baseCSVUIMetaStr, err = getCSVFromFile(path) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + path = filepath.Join(csvNewLayoutBundleDir, "memcached-operator.clusterserviceversion.yaml") + newCSV, newCSVStr, err = getCSVFromFile(path) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) +} + +func readFileHelper(path string) []byte { + b, err := ioutil.ReadFile(path) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + return b +} + +func modifyCSVDepImageHelper(tag string) func(csv *v1alpha1.ClusterServiceVersion) { + return func(csv *v1alpha1.ClusterServiceVersion) { + depSpecs := csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs + ExpectWithOffset(2, len(depSpecs)).To(BeNumerically(">=", 1)) + modifyDepImageHelper(&depSpecs[0].Spec, tag) + } +} + +func modifyDepImageHelper(depSpec *appsv1.DeploymentSpec, tag string) { + containers := depSpec.Template.Spec.Containers + ExpectWithOffset(1, len(containers)).To(BeNumerically(">=", 1)) + containers[0].Image = tag +} + +func makeBaseGetter(csv *v1alpha1.ClusterServiceVersion) getBaseFunc { + return func() (*v1alpha1.ClusterServiceVersion, error) { + return csv.DeepCopy(), nil + } +} + +func getCSVFromFile(path string) (*v1alpha1.ClusterServiceVersion, string, error) { + b, err := ioutil.ReadFile(path) + if err != nil { + return nil, "", err + } + csv := &v1alpha1.ClusterServiceVersion{} + if err = yaml.Unmarshal(b, csv); err == nil { + // Any updates applied to a CSV object will create non-nil slice type fields, + // which cause comparison issues if their counterpart was only unmarshaled. + if csv.Spec.InstallStrategy.StrategySpec.Permissions == nil { + csv.Spec.InstallStrategy.StrategySpec.Permissions = []v1alpha1.StrategyDeploymentPermissions{} + } + if csv.Spec.InstallStrategy.StrategySpec.ClusterPermissions == nil { + csv.Spec.InstallStrategy.StrategySpec.ClusterPermissions = []v1alpha1.StrategyDeploymentPermissions{} + } + if csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs == nil { + csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs = []v1alpha1.StrategyDeploymentSpec{} + } + if csv.Spec.WebhookDefinitions == nil { + csv.Spec.WebhookDefinitions = []v1alpha1.WebhookDescription{} + } + } + return csv, string(b), err +} + +func updateCSV(csv *v1alpha1.ClusterServiceVersion, + opts ...func(*v1alpha1.ClusterServiceVersion)) *v1alpha1.ClusterServiceVersion { + + updated := csv.DeepCopy() + for _, opt := range opts { + opt(updated) + } + return updated +} + +func upgradeCSV(csv *v1alpha1.ClusterServiceVersion, name, version string) *v1alpha1.ClusterServiceVersion { + upgraded := csv.DeepCopy() + + // Update CSV name and upgrade version, then add "replaces" for the old CSV name. + oldName := upgraded.GetName() + upgraded.SetName(genutil.MakeCSVName(name, version)) + upgraded.Spec.Version = operatorversion.OperatorVersion{Version: semver.MustParse(version)} + upgraded.Spec.Replaces = oldName + + return upgraded +} diff --git a/internal/generate/internal/genutil.go b/internal/generate/internal/genutil.go new file mode 100644 index 0000000000..f4a6b4670c --- /dev/null +++ b/internal/generate/internal/genutil.go @@ -0,0 +1,107 @@ +// Copyright 2020 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 ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + + "sigs.k8s.io/yaml" + + "github.com/operator-framework/operator-sdk/internal/util/k8sutil" +) + +// InternalError wraps errors that are development issues and unrelated to user +// input. +type InternalError string + +func (e InternalError) Error() string { + return fmt.Sprintf("internal error: %s", string(e)) +} + +// MakeCSVName returns a ClusterServiceVersion's name. +func MakeCSVName(name, version string) string { + return fmt.Sprintf("%s.v%s", name, version) +} + +// File wraps os.File. Use this type when generating files that may already +// exist on disk and should be overwritten. +type File struct { + *os.File +} + +// Open first creates dir then opens / for reading and writing, +// creating the file if it does not exist. +func Open(dir, fileName string) (*File, error) { + if err := os.MkdirAll(dir, 0700); err != nil { + return nil, err + } + f, err := os.OpenFile(filepath.Join(dir, fileName), os.O_RDWR|os.O_CREATE, 0666) + return &File{f}, err +} + +// WriteObject writes a k8s object to w. +func WriteObject(w io.Writer, obj interface{}) error { + b, err := k8sutil.GetObjectBytes(obj, yaml.Marshal) + if err != nil { + return err + } + return write(w, b) +} + +// WriteObject writes any object to w. +func WriteYAML(w io.Writer, obj interface{}) error { + b, err := yaml.Marshal(obj) + if err != nil { + return err + } + return write(w, b) +} + +// write writes b to w. If w is a File, its contents will be cleared and w +// will be closed following the write. +func write(w io.Writer, b []byte) error { + if f, isFile := w.(*File); isFile { + if err := f.Truncate(0); err != nil { + return err + } + defer func() { + _ = f.Close() + }() + } + _, err := w.Write(b) + return err +} + +// IsExist returns true if path exists on disk. +func IsExist(path string) bool { + if path == "" { + return false + } + _, err := os.Stat(path) + return err == nil || errors.Is(err, os.ErrExist) +} + +// IsNotExist returns true if path does not exist on disk. +func IsNotExist(path string) bool { + if path == "" { + return true + } + _, err := os.Stat(path) + return err != nil && errors.Is(err, os.ErrNotExist) +} diff --git a/internal/generate/testdata/clusterserviceversions/bases/memcached-operator.clusterserviceversion.yaml b/internal/generate/testdata/clusterserviceversions/bases/memcached-operator.clusterserviceversion.yaml new file mode 100644 index 0000000000..63dabe42f3 --- /dev/null +++ b/internal/generate/testdata/clusterserviceversions/bases/memcached-operator.clusterserviceversion.yaml @@ -0,0 +1,47 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: '[]' + capabilities: Basic Install + name: memcached-operator.vX.Y.Z + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - displayName: Memcached App + kind: Memcached + name: memcacheds.cache.example.com + version: v1alpha1 + description: Memcached Operator description. TODO. + displayName: Memcached Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + deployments: null + strategy: "" + installModes: + - supported: true + type: OwnNamespace + - supported: true + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - memcached-operator + links: + - name: Memcached Operator + url: https://memcached-operator.domain + maintainers: + - email: your@email.com + name: Maintainer Name + maturity: alpha + provider: + name: Provider Name + url: https://your.domain + version: 0.0.0 diff --git a/internal/generate/testdata/clusterserviceversions/bases/with-ui-metadata.clusterserviceversion.yaml b/internal/generate/testdata/clusterserviceversions/bases/with-ui-metadata.clusterserviceversion.yaml new file mode 100644 index 0000000000..281662bafa --- /dev/null +++ b/internal/generate/testdata/clusterserviceversions/bases/with-ui-metadata.clusterserviceversion.yaml @@ -0,0 +1,58 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: '[]' + capabilities: Basic Install + name: memcached-operator.vX.Y.Z + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: Memcached is the Schema for the memcacheds API + displayName: Memcached App Display Name + kind: Memcached + name: memcacheds.cache.example.com + specDescriptors: + - description: Size is the size of the memcached deployment + displayName: Size + path: size + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:podCount + statusDescriptors: + - description: Nodes are the names of the memcached pods + displayName: Nodes + path: nodes + version: v1alpha1 + description: Memcached Operator description. TODO. + displayName: Memcached Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + deployments: null + strategy: "" + installModes: + - supported: true + type: OwnNamespace + - supported: true + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - memcached-operator + links: + - name: Memcached Operator + url: https://memcached-operator.domain + maintainers: + - email: your@email.com + name: Maintainer Name + maturity: alpha + provider: + name: Provider Name + url: https://your.domain + version: 0.0.0 diff --git a/internal/generate/testdata/clusterserviceversions/newlayout/manifests/memcached-operator.clusterserviceversion.yaml b/internal/generate/testdata/clusterserviceversions/newlayout/manifests/memcached-operator.clusterserviceversion.yaml new file mode 100644 index 0000000000..e2c3857deb --- /dev/null +++ b/internal/generate/testdata/clusterserviceversions/newlayout/manifests/memcached-operator.clusterserviceversion.yaml @@ -0,0 +1,160 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "cache.example.com/v1alpha1", + "kind": "Memcached", + "metadata": { + "name": "example-memcached" + }, + "spec": { + "size": 3 + } + } + ] + capabilities: Basic Install + name: memcached-operator.v0.0.1 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: Memcached is the Schema for the memcacheds API + displayName: Memcached App Display Name + kind: Memcached + name: memcacheds.cache.example.com + specDescriptors: + - description: Size is the size of the memcached deployment + displayName: Size + path: size + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:podCount + statusDescriptors: + - description: Nodes are the names of the memcached pods + displayName: Nodes + path: nodes + version: v1alpha1 + description: Memcached Operator description. TODO. + displayName: Memcached Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + deployments: + - name: memcached-operator + spec: + replicas: 1 + selector: + matchLabels: + name: memcached-operator + strategy: {} + template: + metadata: + labels: + name: memcached-operator + spec: + containers: + - command: + - memcached-operator + env: + - name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: OPERATOR_NAME + value: memcached-operator + image: quay.io/example/memcached-operator:v0.0.3 + imagePullPolicy: Never + name: memcached-operator + resources: {} + serviceAccountName: memcached-operator + permissions: + - rules: + - apiGroups: + - "" + resources: + - pods + - services + - services/finalizers + - endpoints + - persistentvolumeclaims + - events + - configmaps + - secrets + verbs: + - '*' + - apiGroups: + - apps + resources: + - deployments + - daemonsets + - replicasets + - statefulsets + verbs: + - '*' + - apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - get + - create + - apiGroups: + - apps + resourceNames: + - memcached-operator + resources: + - deployments/finalizers + verbs: + - update + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - apiGroups: + - apps + resources: + - replicasets + - deployments + verbs: + - get + - apiGroups: + - cache.example.com + resources: + - '*' + verbs: + - '*' + serviceAccountName: memcached-operator + strategy: deployment + installModes: + - supported: true + type: OwnNamespace + - supported: true + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - memcached-operator + links: + - name: Memcached Operator + url: https://memcached-operator.domain + maintainers: + - email: your@email.com + name: Maintainer Name + maturity: alpha + provider: + name: Provider Name + url: https://your.domain + version: 0.0.1 diff --git a/internal/generate/testdata/non-standard-layout/PROJECT b/internal/generate/testdata/non-standard-layout/PROJECT new file mode 100644 index 0000000000..3a158eb8a2 --- /dev/null +++ b/internal/generate/testdata/non-standard-layout/PROJECT @@ -0,0 +1,10 @@ +domain: example.com +layout: go.kubebuilder.io/v2.0.0 +repo: github.com/example/memcached-operator +resources: +- group: cache + kind: Memcached + version: v1alpha1 +version: 3-alpha +plugins: + go.operator-sdk.io/v2.0.0: {} diff --git a/internal/util/kubebuilder/project.go b/internal/util/kubebuilder/project.go index bece9b5495..305d910544 100644 --- a/internal/util/kubebuilder/project.go +++ b/internal/util/kubebuilder/project.go @@ -15,9 +15,11 @@ package kbutil import ( + "io/ioutil" "os" log "github.com/sirupsen/logrus" + "sigs.k8s.io/kubebuilder/pkg/model/config" ) const configFile = "PROJECT" @@ -34,3 +36,17 @@ func HasProjectFile() bool { } return true } + +// ReadConfig returns a configuration if a file containing one exists at the +// default path (project root). +func ReadConfig() (*config.Config, error) { + b, err := ioutil.ReadFile(configFile) + if err != nil { + return nil, err + } + c := &config.Config{} + if err = c.Unmarshal(b); err != nil { + return nil, err + } + return c, nil +} diff --git a/website/content/en/docs/cli/operator-sdk_generate.md b/website/content/en/docs/cli/operator-sdk_generate.md index f2478bc5bc..8f9b3b83b7 100644 --- a/website/content/en/docs/cli/operator-sdk_generate.md +++ b/website/content/en/docs/cli/operator-sdk_generate.md @@ -8,7 +8,7 @@ Invokes a specific generator ### Synopsis The 'operator-sdk generate' command invokes a specific generator to generate -code or manifests on disk. +code or manifests. ### Options From ace0487af97fa5a921f95f3949a1600ac527c132 Mon Sep 17 00:00:00 2001 From: Eric Stroczynski Date: Thu, 21 May 2020 16:39:24 -0700 Subject: [PATCH 2/2] generate bundle: hide --manifest-root until name is decided --- cmd/operator-sdk/generate/bundle/cmd.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmd/operator-sdk/generate/bundle/cmd.go b/cmd/operator-sdk/generate/bundle/cmd.go index 5ad550d19a..d18fd4e5e2 100644 --- a/cmd/operator-sdk/generate/bundle/cmd.go +++ b/cmd/operator-sdk/generate/bundle/cmd.go @@ -115,8 +115,16 @@ func (c *bundleCmd) addCommonFlagsTo(fs *pflag.FlagSet) { fs.StringVar(&c.inputDir, "input-dir", "", "Directory to read an existing bundle from. "+ "This directory is the parent of your bundle 'manifests' directory, and different from --manifest-root") fs.StringVar(&c.outputDir, "output-dir", "", "Directory to write the bundle to") + fs.StringVar(&c.manifestRoot, "manifest-root", "", "Root directory for operator manifests such as "+ "Deployments and RBAC, ex. 'deploy' or 'config'. This directory is different from that passed to --input-dir") + // NB(estroz): still debating the name of this flag. For now, hide it as an + // "alpha" flag so we do not have to deprecate it if we change this name. + // TODO(estroz): decide on this flag's name before making 'init' default. + if err := fs.MarkHidden("manifest-root"); err != nil { + panic(err) + } + fs.StringVar(&c.apisDir, "apis-dir", "", "Root directory for API type defintions") fs.StringVar(&c.crdsDir, "crds-dir", "", "Root directory for CustomResoureDefinition manifests") fs.StringVar(&c.channels, "channels", "alpha", "A comma-separated list of channels the bundle belongs to")