diff --git a/changelog/fragments/3088-addition.yaml b/changelog/fragments/3088-addition.yaml new file mode 100644 index 0000000000..29c653b268 --- /dev/null +++ b/changelog/fragments/3088-addition.yaml @@ -0,0 +1,6 @@ +entries: + - description: > + 'bundle generate' generates bundles for current project layouts; this + has the same behavior as 'generate csv --make-manifests=true' + + kind: addition diff --git a/cmd/operator-sdk/generate/bundle/bundle_legacy.go b/cmd/operator-sdk/generate/bundle/bundle_legacy.go new file mode 100644 index 0000000000..43893ebefd --- /dev/null +++ b/cmd/operator-sdk/generate/bundle/bundle_legacy.go @@ -0,0 +1,131 @@ +// 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" + "path/filepath" + + "github.com/operator-framework/operator-registry/pkg/lib/bundle" + log "github.com/sirupsen/logrus" + + 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" + "github.com/operator-framework/operator-sdk/internal/util/projutil" +) + +// setCommonDefaultsLegacy sets defaults useful to all modes of this subcommand. +func (c *bundleCmd) setCommonDefaultsLegacy() { + if c.operatorName == "" { + c.operatorName = filepath.Base(projutil.MustGetwd()) + } +} + +// validateManifestsLegacy validates c for bundle manifests generation for +// legacy project layouts. +func (c bundleCmd) validateManifestsLegacy() error { + if c.version != "" { + if err := genutil.ValidateVersion(c.version); err != nil { + return err + } + } + return nil +} + +// runManifestsLegacy generates bundle manifests for legacy project layouts. +func (c bundleCmd) runManifestsLegacy() (err error) { + + if !c.quiet { + if c.version == "" { + log.Info("Generating bundle manifests") + } else { + log.Info("Generating bundle manifests version", c.version) + } + } + + if c.apisDir == "" { + c.apisDir = filepath.Join("pkg", "apis") + } + if c.manifestRoot == "" { + c.manifestRoot = "deploy" + } + if c.crdsDir == "" { + c.crdsDir = filepath.Join(c.manifestRoot, "crds") + } + defaultBundleDir := filepath.Join(c.manifestRoot, "olm-catalog", c.operatorName) + if c.inputDir == "" { + c.inputDir = defaultBundleDir + } + if c.outputDir == "" { + c.outputDir = defaultBundleDir + } + + col := &collector.Manifests{} + if err := col.UpdateFromDirs(c.manifestRoot, c.crdsDir); err != nil { + return err + } + + csvGen := gencsv.Generator{ + OperatorName: c.operatorName, + OperatorType: projutil.GetOperatorType(), + Version: c.version, + Collector: col, + } + + opts := []gencsv.LegacyOption{ + gencsv.WithBundleBase(c.inputDir, c.apisDir, c.interactiveLevel), + gencsv.LegacyOption(gencsv.WithBundleWriter(c.outputDir)), + } + if err := csvGen.GenerateLegacy(opts...); err != nil { + return fmt.Errorf("error generating ClusterServiceVersion: %v", err) + } + + dir := filepath.Join(c.outputDir, bundle.ManifestsDir) + if err := genutil.WriteCRDFilesLegacy(dir, col.CustomResourceDefinitions...); err != nil { + return err + } + + if !c.quiet { + log.Infoln("Bundle manifests generated successfully in", c.outputDir) + } + + return nil +} + +// runMetadataLegacy generates a bundle.Dockerfile and bundle metadata for +// legacy project layouts. +func (c bundleCmd) runMetadataLegacy() error { + + directory := c.inputDir + if directory == "" { + // There may be no existing bundle at the default path, so assume manifests + // were generated in the output directs. + defaultDirectory := filepath.Join("deploy", "olm-catalog", c.operatorName, bundle.ManifestsDir) + if c.outputDir != "" && genutil.IsNotExist(defaultDirectory) { + directory = filepath.Join(c.outputDir, bundle.ManifestsDir) + } else { + directory = defaultDirectory + } + } else { + directory = filepath.Join(directory, bundle.ManifestsDir) + } + outputDir := c.outputDir + if filepath.Clean(outputDir) == filepath.Clean(directory) { + outputDir = "" + } + + return c.generateMetadata(directory, outputDir) +} diff --git a/cmd/operator-sdk/generate/bundle/cmd.go b/cmd/operator-sdk/generate/bundle/cmd.go index 823f59d0a0..8d09f7b51b 100644 --- a/cmd/operator-sdk/generate/bundle/cmd.go +++ b/cmd/operator-sdk/generate/bundle/cmd.go @@ -53,7 +53,7 @@ type bundleCmd struct { overwrite bool } -//nolint:lll +// NewCmd returns the 'bundle' command configured for the new project layout. func NewCmd() *cobra.Command { c := &bundleCmd{} cmd := &cobra.Command{ @@ -123,6 +123,64 @@ func NewCmd() *cobra.Command { return cmd } +// NewCmdLegacy returns the 'bundle' command configured for the legacy project layout. +func NewCmdLegacy() *cobra.Command { + c := &bundleCmd{} + cmd := &cobra.Command{ + Use: "bundle", + Short: "Generates bundle data for the operator", + RunE: func(cmd *cobra.Command, args []string) (err error) { + if len(args) != 0 { + return fmt.Errorf("command %s doesn't accept any arguments", cmd.CommandPath()) + } + + // Check if the user has any specific preference to enable/disable + // interactive prompts. Default behaviour is to disable the prompt + // unless a base bundle does not exist. + if cmd.Flags().Changed("interactive") { + if c.interactive { + c.interactiveLevel = projutil.InteractiveOnAll + } else { + c.interactiveLevel = projutil.InteractiveHardOff + } + } + + // Generate manifests and metadata by default if no flags are set so + // the default behavior is "do everything". + fs := cmd.Flags() + if !fs.Changed("metadata") && !fs.Changed("manifests") { + c.metadata = true + c.manifests = true + } + + c.setCommonDefaultsLegacy() + + if c.manifests { + if err = c.validateManifestsLegacy(); err != nil { + return fmt.Errorf("invalid command options: %v", err) + } + if err = c.runManifestsLegacy(); err != nil { + log.Fatalf("Error generating bundle manifests: %v", err) + } + } + if c.metadata { + if err = c.runMetadataLegacy(); err != nil { + log.Fatalf("Error generating bundle metadata: %v", err) + } + } + + return nil + }, + } + + cmd.Flags().BoolVar(&c.manifests, "manifests", false, "Generate bundle manifests") + cmd.Flags().BoolVar(&c.metadata, "metadata", false, "Generate bundle metadata and Dockerfile") + + 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") diff --git a/cmd/operator-sdk/generate/cmd.go b/cmd/operator-sdk/generate/cmd.go index 0b9740944f..694dae99e8 100644 --- a/cmd/operator-sdk/generate/cmd.go +++ b/cmd/operator-sdk/generate/cmd.go @@ -29,7 +29,7 @@ code or manifests.`, } } -// NewCmdLegacy returns the 'generate' command configured for the new project layout. +// NewCmd returns the 'generate' command configured for the new project layout. func NewCmd() *cobra.Command { cmd := newCmd() cmd.AddCommand( @@ -45,6 +45,7 @@ func NewCmdLegacy() *cobra.Command { newGenerateK8SCmd(), newGenerateCRDsCmd(), newGenerateCSVCmd(), + bundle.NewCmdLegacy(), ) return cmd } diff --git a/cmd/operator-sdk/generate/internal/genutil.go b/cmd/operator-sdk/generate/internal/genutil.go index 35a99db5eb..3b51bee299 100644 --- a/cmd/operator-sdk/generate/internal/genutil.go +++ b/cmd/operator-sdk/generate/internal/genutil.go @@ -83,17 +83,37 @@ func WriteCRDFiles(dir string, crds ...v1beta1.CustomResourceDefinition) error { return err } for _, crd := range crds { - if err := writeCRDFile(dir, crd); err != nil { + if err := writeCRDFile(dir, crd, makeCRDFileName(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) +func makeCRDFileName(crd v1beta1.CustomResourceDefinition) string { + return fmt.Sprintf("%s_%s.yaml", crd.Spec.Group, crd.Spec.Names.Plural) +} + +// WriteCRDFilesLegacy creates dir then writes each CustomResourceDefinition +// in crds to a file in legacy format in dir. +func WriteCRDFilesLegacy(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, makeCRDFileNameLegacy(crd)); err != nil { + return err + } + } + return nil +} + +func makeCRDFileNameLegacy(crd v1beta1.CustomResourceDefinition) string { + return fmt.Sprintf("%s_%s_crd.yaml", crd.Spec.Group, crd.Spec.Names.Plural) +} + +// writeCRDFile marshals crd to bytes and writes them to dir in file. +func writeCRDFile(dir string, crd v1beta1.CustomResourceDefinition, file string) error { f, err := os.Create(filepath.Join(dir, file)) if err != nil { return err diff --git a/hack/tests/subcommand-generate-csv.sh b/hack/tests/subcommand-generate-csv.sh index cc002b3a02..aacb57cbba 100755 --- a/hack/tests/subcommand-generate-csv.sh +++ b/hack/tests/subcommand-generate-csv.sh @@ -41,8 +41,11 @@ function check_crd_files() { } function generate_csv() { - echo "operator-sdk generate csv --operator-name $OPERATOR_NAME --interactive=false $@" - operator-sdk generate csv --operator-name $OPERATOR_NAME --interactive=false $@ + echo_run operator-sdk generate csv --operator-name $OPERATOR_NAME --interactive=false $@ +} + +function generate_bundle() { + echo_run operator-sdk generate bundle --operator-name $OPERATOR_NAME --interactive=false $@ } pushd "$TEST_DIR" > /dev/null @@ -96,3 +99,40 @@ check_crd_files "$TEST_NAME" "$OUTPUT_DIR/manifests" 1 cleanup_case header_text "All 'operator-sdk generate csv' subcommand tests passed." + +header_text "Running 'operator-sdk generate bundle' subcommand tests in $TEST_DIR." + +TEST_NAME="generate with version $OPERATOR_VERSION" +header_text "$TEST_NAME" +generate_bundle --version $OPERATOR_VERSION +check_dir "$TEST_NAME" "$DEFAULT_BUNDLE_DIR" 0 +check_dir "$TEST_NAME" "$OPERATOR_BUNDLE_ROOT_DIR/manifests" 1 +check_dir "$TEST_NAME" "$OPERATOR_BUNDLE_ROOT_DIR/metadata" 1 +check_csv_file "$TEST_NAME" "$OPERATOR_BUNDLE_ROOT_DIR/manifests" 1 +check_crd_files "$TEST_NAME" "$OPERATOR_BUNDLE_ROOT_DIR/manifests" 1 +check_file "$TEST_NAME" "bundle.Dockerfile" 1 +cleanup_case + +TEST_NAME="generate manifests only with version $OPERATOR_VERSION" +header_text "$TEST_NAME" +generate_bundle --version $OPERATOR_VERSION --manifests +check_dir "$TEST_NAME" "$DEFAULT_BUNDLE_DIR" 0 +check_dir "$TEST_NAME" "$OPERATOR_BUNDLE_ROOT_DIR/manifests" 1 +check_dir "$TEST_NAME" "$OPERATOR_BUNDLE_ROOT_DIR/metadata" 0 +check_csv_file "$TEST_NAME" "$OPERATOR_BUNDLE_ROOT_DIR/manifests" 1 +check_crd_files "$TEST_NAME" "$OPERATOR_BUNDLE_ROOT_DIR/manifests" 1 +check_file "$TEST_NAME" "bundle.Dockerfile" 0 +cleanup_case + +TEST_NAME="generate with version $OPERATOR_VERSION and output-dir" +header_text "$TEST_NAME" +generate_bundle --version $OPERATOR_VERSION --output-dir "$OUTPUT_DIR" +check_dir "$TEST_NAME" "$OPERATOR_BUNDLE_ROOT_DIR/manifests" 0 +check_dir "$TEST_NAME" "$OUTPUT_DIR/manifests" 1 +check_dir "$TEST_NAME" "$OUTPUT_DIR/metadata" 1 +check_csv_file "$TEST_NAME" "$OUTPUT_DIR/manifests" 1 +check_crd_files "$TEST_NAME" "$OUTPUT_DIR/manifests" 1 +check_file "$TEST_NAME" "bundle.Dockerfile" 1 +cleanup_case + +header_text "All 'operator-sdk generate bundle' subcommand tests passed." diff --git a/internal/generate/clusterserviceversion/clusterserviceversion.go b/internal/generate/clusterserviceversion/clusterserviceversion.go index 4582d87211..079ee4b507 100644 --- a/internal/generate/clusterserviceversion/clusterserviceversion.go +++ b/internal/generate/clusterserviceversion/clusterserviceversion.go @@ -149,6 +149,42 @@ func (g *Generator) Generate(cfg *config.Config, opts ...Option) (err error) { return genutil.WriteObject(w, csv) } +// LegacyOption is a function that modifies a Generator for legacy project layouts. +type LegacyOption Option + +// WithBundleBase sets a Generator's base CSV to a legacy-style bundle base. +func WithBundleBase(inputDir, apisDir string, ilvl projutil.InteractiveLevel) LegacyOption { + return func(g *Generator) error { + g.getBase = g.makeBundleBaseGetterLegacy(inputDir, apisDir, ilvl) + return nil + } +} + +// GenerateLegacy configures the generator with opts then runs it. Used for +// generating files for legacy project layouts. +func (g *Generator) GenerateLegacy(opts ...LegacyOption) (err error) { + 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 { @@ -211,6 +247,46 @@ func (g Generator) makeBaseGetter(basePath, apisDir string, interactive bool) ge } } +// makeBundleBaseGetterLegacy returns a function that gets a bundle base +// for legacy project layouts. +func (g Generator) makeBundleBaseGetterLegacy(inputDir, apisDir string, ilvl projutil.InteractiveLevel) getBaseFunc { + basePath := filepath.Join(inputDir, bundle.ManifestsDir, makeCSVFileName(g.OperatorName)) + if genutil.IsNotExist(basePath) { + basePath = "" + } + return g.makeBaseGetterLegacy(basePath, apisDir, requiresInteraction(basePath, ilvl)) +} + +// makeBaseGetterLegacy returns a function that gets a base from inputDir. +// apisDir is used by getBaseFunc to populate base fields. This method should +// be used when creating LegacyOptions. +func (g Generator) makeBaseGetterLegacy(basePath, apisDir string, interactive bool) getBaseFunc { + var gvks []schema.GroupVersionKind + if g.Collector != nil { + for _, crd := range g.Collector.CustomResourceDefinitions { + for _, version := range crd.Spec.Versions { + gvks = append(gvks, schema.GroupVersionKind{ + Group: crd.Spec.Group, + Version: version.Name, + Kind: crd.Spec.Names.Kind, + }) + } + } + } + + return func() (*operatorsv1alpha1.ClusterServiceVersion, error) { + b := bases.ClusterServiceVersion{ + OperatorName: g.OperatorName, + OperatorType: g.OperatorType, + BasePath: basePath, + APIsDir: apisDir, + GVKs: gvks, + Interactive: interactive, + } + return b.GetBase() + } +} + // requiresInteraction checks if the combination of ilvl and basePath existence // requires the generator prompt a user interactively. func requiresInteraction(basePath string, ilvl projutil.InteractiveLevel) bool { diff --git a/internal/generate/clusterserviceversion/clusterserviceversion_test.go b/internal/generate/clusterserviceversion/clusterserviceversion_test.go index 1fa59523f8..d08da052c1 100644 --- a/internal/generate/clusterserviceversion/clusterserviceversion_test.go +++ b/internal/generate/clusterserviceversion/clusterserviceversion_test.go @@ -153,6 +153,23 @@ var _ = Describe("Generating a ClusterServiceVersion", func() { Expect(outputFile).To(BeAnExistingFile()) Expect(string(readFileHelper(outputFile))).To(MatchYAML(newCSVStr)) }) + + It("should write a ClusterServiceVersion manifest to a legacy base/bundle file", func() { + g = Generator{ + OperatorName: operatorName, + OperatorType: operatorType, + Version: version, + Collector: col, + } + opts := []LegacyOption{ + WithBundleBase(csvBasesDir, goAPIsDir, projutil.InteractiveHardOff), + LegacyOption(WithBundleWriter(tmp)), + } + Expect(g.GenerateLegacy(opts...)).ToNot(HaveOccurred()) + outputFile := filepath.Join(tmp, bundle.ManifestsDir, makeCSVFileName(operatorName)) + Expect(outputFile).To(BeAnExistingFile()) + Expect(string(readFileHelper(outputFile))).To(MatchYAML(newCSVStr)) + }) }) Context("with incorrect Options", func() { @@ -182,6 +199,23 @@ var _ = Describe("Generating a ClusterServiceVersion", func() { } Expect(g.Generate(cfg, opts...)).To(MatchError(noGetBaseError)) }) + + It("should return an error without any LegacyOptions", func() { + opts := []LegacyOption{} + Expect(g.GenerateLegacy(opts...)).To(MatchError(noGetWriterError)) + }) + It("should return an error without a getWriter (legacy)", func() { + opts := []LegacyOption{ + WithBundleBase(csvBasesDir, goAPIsDir, projutil.InteractiveHardOff), + } + Expect(g.GenerateLegacy(opts...)).To(MatchError(noGetWriterError)) + }) + It("should return an error without a getBase (legacy)", func() { + opts := []LegacyOption{ + LegacyOption(WithWriter(&bytes.Buffer{})), + } + Expect(g.GenerateLegacy(opts...)).To(MatchError(noGetBaseError)) + }) }) Context("to create a new", func() { diff --git a/website/content/en/docs/cli/operator-sdk_generate.md b/website/content/en/docs/cli/operator-sdk_generate.md index 8f9b3b83b7..ca70ef6ae0 100644 --- a/website/content/en/docs/cli/operator-sdk_generate.md +++ b/website/content/en/docs/cli/operator-sdk_generate.md @@ -19,6 +19,7 @@ code or manifests. ### SEE ALSO * [operator-sdk](../operator-sdk) - An SDK for building operators with ease +* [operator-sdk generate bundle](../operator-sdk_generate_bundle) - Generates bundle data for the operator * [operator-sdk generate crds](../operator-sdk_generate_crds) - Generates CRDs for API's * [operator-sdk generate csv](../operator-sdk_generate_csv) - Generates a ClusterServiceVersion YAML file for the operator * [operator-sdk generate k8s](../operator-sdk_generate_k8s) - Generates Kubernetes code for custom resource diff --git a/website/content/en/docs/cli/operator-sdk_generate_bundle.md b/website/content/en/docs/cli/operator-sdk_generate_bundle.md new file mode 100644 index 0000000000..360b5b86ae --- /dev/null +++ b/website/content/en/docs/cli/operator-sdk_generate_bundle.md @@ -0,0 +1,38 @@ +--- +title: "operator-sdk generate bundle" +--- +## operator-sdk generate bundle + +Generates bundle data for the operator + +### Synopsis + +Generates bundle data for the operator + +``` +operator-sdk generate bundle [flags] +``` + +### Options + +``` + --apis-dir string Root directory for API type defintions + --channels string A comma-separated list of channels the bundle belongs to (default "alpha") + --crds-dir string Root directory for CustomResoureDefinition manifests + --default-channel string The default channel for the bundle + -h, --help help for bundle + --input-dir string Directory to read an existing bundle from. This directory is the parent of your bundle 'manifests' directory, and different from --manifest-root + --interactive When set or no bundle base exists, an interactive command prompt will be presented to accept bundle ClusterServiceVersion metadata + --manifests Generate bundle manifests + --metadata Generate bundle metadata and Dockerfile + --operator-name string Name of the bundle's operator + --output-dir string Directory to write the bundle to + --overwrite Overwrite the bundle's metadata and Dockerfile if they exist + -q, --quiet Run in quiet mode + -v, --version string Semantic version of the operator in the generated bundle. Only set if creating a new bundle or upgrading your operator +``` + +### SEE ALSO + +* [operator-sdk generate](../operator-sdk_generate) - Invokes a specific generator +