diff --git a/cmd/operator-sdk/generate/bundle/bundle.go b/cmd/operator-sdk/generate/bundle/bundle.go index a67d825529..07cc6f8a19 100644 --- a/cmd/operator-sdk/generate/bundle/bundle.go +++ b/cmd/operator-sdk/generate/bundle/bundle.go @@ -62,7 +62,7 @@ func (c bundleCmd) runKustomize(cfg *config.Config) error { OperatorType: genutil.PluginKeyToOperatorType(cfg.Layout), } opts := []gencsv.Option{ - gencsv.WithBase(c.inputDir, c.apisDir), + gencsv.WithBase(c.inputDir, c.apisDir, c.interactiveLevel), gencsv.WithBaseWriter(c.outputDir), } if err := csvGen.Generate(cfg, opts...); err != nil { @@ -152,7 +152,7 @@ func (c bundleCmd) runManifests(cfg *config.Config) (err error) { stdout := genutil.NewMultiManifestWriter(os.Stdout) opts := []gencsv.Option{ - gencsv.WithBase(c.inputDir, c.apisDir), + gencsv.WithBase(c.inputDir, c.apisDir, c.interactiveLevel), } if c.stdout { opts = append(opts, gencsv.WithWriter(stdout)) @@ -187,7 +187,14 @@ func (c bundleCmd) runMetadata() error { directory := c.inputDir if directory == "" { - directory = filepath.Join("config", "bundle", bundle.ManifestsDir) + // There may be no existing bundle at the default path, so assume manifests + // only exist in the output directory. + defaultDirectory := filepath.Join("config", "bundle", 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) } diff --git a/cmd/operator-sdk/generate/bundle/cmd.go b/cmd/operator-sdk/generate/bundle/cmd.go index d18fd4e5e2..823f59d0a0 100644 --- a/cmd/operator-sdk/generate/bundle/cmd.go +++ b/cmd/operator-sdk/generate/bundle/cmd.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/pflag" kbutil "github.com/operator-framework/operator-sdk/internal/util/kubebuilder" + "github.com/operator-framework/operator-sdk/internal/util/projutil" ) //nolint:maligned @@ -42,6 +43,10 @@ type bundleCmd struct { stdout bool quiet bool + // Interactive options. + interactiveLevel projutil.InteractiveLevel + interactive bool + // Metadata options. channels string defaultChannel string @@ -59,6 +64,17 @@ func NewCmd() *cobra.Command { 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 kustomize bases, manifests, and metadata by default if no // flags are set so the default behavior is "do everything". fs := cmd.Flags() @@ -131,4 +147,6 @@ func (c *bundleCmd) addCommonFlagsTo(fs *pflag.FlagSet) { 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") + fs.BoolVar(&c.interactive, "interactive", false, "When set or no bundle base exists, an interactive "+ + "command prompt will be presented to accept bundle ClusterServiceVersion metadata") } diff --git a/cmd/operator-sdk/generate/internal/genutil.go b/cmd/operator-sdk/generate/internal/genutil.go index cc6095e5e8..35a99db5eb 100644 --- a/cmd/operator-sdk/generate/internal/genutil.go +++ b/cmd/operator-sdk/generate/internal/genutil.go @@ -16,6 +16,7 @@ package genutil import ( "bytes" + "errors" "fmt" "io" "os" @@ -126,3 +127,12 @@ func (w *multiManifestWriter) Write(b []byte) (int, error) { func NewMultiManifestWriter(w io.Writer) io.Writer { return &multiManifestWriter{w} } + +// 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/clusterserviceversion/clusterserviceversion.go b/internal/generate/clusterserviceversion/clusterserviceversion.go index 33b37c0bf7..4582d87211 100644 --- a/internal/generate/clusterserviceversion/clusterserviceversion.go +++ b/internal/generate/clusterserviceversion/clusterserviceversion.go @@ -75,9 +75,9 @@ type getBaseFunc func() (*operatorsv1alpha1.ClusterServiceVersion, error) type Option func(*Generator) error // WithBase sets a Generator's base CSV to a kustomize-style base. -func WithBase(inputDir, apisDir string) Option { +func WithBase(inputDir, apisDir string, ilvl projutil.InteractiveLevel) Option { return func(g *Generator) error { - g.getBase = g.makeKustomizeBaseGetter(inputDir, apisDir) + g.getBase = g.makeKustomizeBaseGetter(inputDir, apisDir, ilvl) return nil } } @@ -179,18 +179,18 @@ func makeCSVFileName(name string) string { } // makeKustomizeBaseGetter returns a function that gets a kustomize-style base. -func (g Generator) makeKustomizeBaseGetter(inputDir, apisDir string) getBaseFunc { +func (g Generator) makeKustomizeBaseGetter(inputDir, apisDir string, ilvl projutil.InteractiveLevel) getBaseFunc { basePath := filepath.Join(inputDir, "bases", makeCSVFileName(g.OperatorName)) if genutil.IsNotExist(basePath) { basePath = "" } - return g.makeBaseGetter(basePath, apisDir) + return g.makeBaseGetter(basePath, apisDir, requiresInteraction(basePath, ilvl)) } // 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 { +func (g Generator) makeBaseGetter(basePath, apisDir string, interactive bool) 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) @@ -205,11 +205,18 @@ func (g Generator) makeBaseGetter(basePath, apisDir string) getBaseFunc { 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 { + return (ilvl == projutil.InteractiveSoftOff && genutil.IsNotExist(basePath)) || ilvl == projutil.InteractiveOnAll +} + // 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. diff --git a/internal/generate/clusterserviceversion/clusterserviceversion_test.go b/internal/generate/clusterserviceversion/clusterserviceversion_test.go index db116e241b..1fa59523f8 100644 --- a/internal/generate/clusterserviceversion/clusterserviceversion_test.go +++ b/internal/generate/clusterserviceversion/clusterserviceversion_test.go @@ -117,7 +117,7 @@ var _ = Describe("Generating a ClusterServiceVersion", func() { Collector: col, } opts := []Option{ - WithBase(csvBasesDir, goAPIsDir), + WithBase(csvBasesDir, goAPIsDir, projutil.InteractiveHardOff), WithWriter(buf), } Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred()) @@ -129,11 +129,11 @@ var _ = Describe("Generating a ClusterServiceVersion", func() { OperatorType: operatorType, } opts := []Option{ - WithBase(csvBasesDir, goAPIsDir), + WithBase(csvBasesDir, goAPIsDir, projutil.InteractiveHardOff), WithBaseWriter(tmp), } Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred()) - outputFile := filepath.Join(tmp, "bases", makeCSVFileName("memcached-operator")) + outputFile := filepath.Join(tmp, "bases", makeCSVFileName(operatorName)) Expect(outputFile).To(BeAnExistingFile()) Expect(string(readFileHelper(outputFile))).To(MatchYAML(baseCSVUIMetaStr)) }) @@ -145,11 +145,11 @@ var _ = Describe("Generating a ClusterServiceVersion", func() { Collector: col, } opts := []Option{ - WithBase(csvBasesDir, goAPIsDir), + WithBase(csvBasesDir, goAPIsDir, projutil.InteractiveHardOff), WithBundleWriter(tmp), } Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred()) - outputFile := filepath.Join(tmp, bundle.ManifestsDir, makeCSVFileName("memcached-operator")) + outputFile := filepath.Join(tmp, bundle.ManifestsDir, makeCSVFileName(operatorName)) Expect(outputFile).To(BeAnExistingFile()) Expect(string(readFileHelper(outputFile))).To(MatchYAML(newCSVStr)) }) @@ -172,7 +172,7 @@ var _ = Describe("Generating a ClusterServiceVersion", func() { }) It("should return an error without a getWriter", func() { opts := []Option{ - WithBase(csvBasesDir, goAPIsDir), + WithBase(csvBasesDir, goAPIsDir, projutil.InteractiveHardOff), } Expect(g.Generate(cfg, opts...)).To(MatchError(noGetWriterError)) }) @@ -282,6 +282,43 @@ var _ = Describe("Generating a ClusterServiceVersion", func() { }) +var _ = Describe("Generation requires interaction", func() { + var ( + testExistingPath = filepath.Join(csvBasesDir, "memcached-operator.clusterserviceversion.yaml") + testNotExistingPath = filepath.Join(csvBasesDir, "notexist.clusterserviceversion.yaml") + ) + + Context("when base path does not exist", func() { + By("turning interaction off explicitly") + It("returns false", func() { + Expect(requiresInteraction(testNotExistingPath, projutil.InteractiveHardOff)).To(BeFalse()) + }) + By("turning interaction off implicitly") + It("returns true", func() { + Expect(requiresInteraction(testNotExistingPath, projutil.InteractiveSoftOff)).To(BeTrue()) + }) + By("turning interaction on explicitly") + It("returns true", func() { + Expect(requiresInteraction(testNotExistingPath, projutil.InteractiveOnAll)).To(BeTrue()) + }) + }) + + Context("when base path does exist", func() { + By("turning interaction off explicitly") + It("returns false", func() { + Expect(requiresInteraction(testExistingPath, projutil.InteractiveHardOff)).To(BeFalse()) + }) + By("turning interaction off implicitly") + It("returns false", func() { + Expect(requiresInteraction(testExistingPath, projutil.InteractiveSoftOff)).To(BeFalse()) + }) + By("turning interaction on explicitly") + It("returns true", func() { + Expect(requiresInteraction(testExistingPath, projutil.InteractiveOnAll)).To(BeTrue()) + }) + }) +}) + func readConfigHelper(dir string) *config.Config { wd, err := os.Getwd() ExpectWithOffset(1, err).ToNot(HaveOccurred()) diff --git a/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go b/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go index 92ea84c5ad..e3180999de 100644 --- a/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go +++ b/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go @@ -212,7 +212,6 @@ func mutatingToWebhookDescription(webhook admissionregv1.MutatingWebhook) operat func applyCustomResources(c *collector.Manifests, csv *operatorsv1alpha1.ClusterServiceVersion) error { examples := []json.RawMessage{} for _, cr := range c.CustomResources { - fmt.Println("applying", cr.GetName()) crBytes, err := cr.MarshalJSON() if err != nil { return err @@ -228,7 +227,6 @@ func applyCustomResources(c *collector.Manifests, csv *operatorsv1alpha1.Cluster csv.SetAnnotations(make(map[string]string)) } csv.GetAnnotations()["alm-examples"] = string(examplesJSON) - fmt.Println("applied") return nil }