diff --git a/cmd/operator-sdk/bundle/cmd.go b/cmd/operator-sdk/bundle/cmd.go index f5368dd48f..fb6b8eb151 100644 --- a/cmd/operator-sdk/bundle/cmd.go +++ b/cmd/operator-sdk/bundle/cmd.go @@ -29,7 +29,7 @@ type bundleCmd struct { generateOnly bool } -func NewCmd() *cobra.Command { +func newCmd() *cobra.Command { cmd := &cobra.Command{ Use: "bundle", Short: "Manage operator bundle metadata", @@ -44,10 +44,22 @@ More information about the integration with OLM via SDK: https://sdk.operatorframework.io/docs/olm-integration/ `, } + return cmd +} +func NewCmdLegacy() *cobra.Command { + cmd := newCmd() cmd.AddCommand( newCreateCmd(), newValidateCmd(), ) return cmd } + +func NewCmd() *cobra.Command { + cmd := newCmd() + cmd.AddCommand( + newValidateCmd(), + ) + return cmd +} diff --git a/cmd/operator-sdk/cli/legacy.go b/cmd/operator-sdk/cli/legacy.go index 541575ce44..14f2b13eb0 100644 --- a/cmd/operator-sdk/cli/legacy.go +++ b/cmd/operator-sdk/cli/legacy.go @@ -73,7 +73,7 @@ func GetCLIRoot() *cobra.Command { add.NewCmd(), alpha.NewCmd(), build.NewCmd(), - bundle.NewCmd(), + bundle.NewCmdLegacy(), cleanup.NewCmd(), completion.NewCmd(), execentrypoint.NewCmd(), diff --git a/cmd/operator-sdk/generate/bundle/bundle.go b/cmd/operator-sdk/generate/bundle/bundle.go index f4a59c5f84..4737445b0d 100644 --- a/cmd/operator-sdk/generate/bundle/bundle.go +++ b/cmd/operator-sdk/generate/bundle/bundle.go @@ -29,6 +29,78 @@ import ( "github.com/operator-framework/operator-sdk/internal/generate/collector" ) +const ( + //nolint:lll + examples = ` + # Using the example 'memcached-operator' and assuming a directory structure + # similar to the following exists: + $ tree api/ config/ + api/ + └── v1alpha1 + ├── groupversion_info.go + ├── memcached_types.go + └── zz_generated.deepcopy.go + config/ + ├── bundle + │   └── kustomization.yaml + ├── crd + │   ├── bases + │   │   └── cache.my.domain_memcacheds.yaml + │   ├── kustomization.yaml + │   ├── kustomizeconfig.yaml + │   ... + ├── default + │   ├── kustomization.yaml + │   ... + ├── manager + │   ├── kustomization.yaml + │   └── manager.yaml + ... + + # Generate bundle files and build your bundle image with these 'make' recipes: + $ make bundle + $ export USERNAME= + $ export BUNDLE_IMG=quay.io/$USERNAME/memcached-operator-bundle:v0.0.1 + $ make bundle-build BUNDLE_IMG=$BUNDLE_IMG + + # The above recipe runs the following commands manually. First it creates bundle + # manifests, metadata, and a bundle.Dockerfile: + $ make manifests + /home/user/go/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $ operator-sdk generate bundle -q --kustomize + + Display name for the operator (required): + > memcached-operator + ... + + $ kustomize build config/bundle | operator-sdk generate bundle --manifests --metadata --overwrite --version 0.0.1 + Generating bundle manifest version 0.0.1 + ... + + # After running the above commands, you should see: + $ tree config/bundle + config/bundle + ├── bases + │   └── memcached-operator.clusterserviceversion.yaml + ├── kustomization.yaml + ├── manifests + │   ├── cache.my.domain_memcacheds.yaml + │   └── memcached-operator.clusterserviceversion.yaml + └── metadata + └── annotations.yaml + + # Then it validates your bundle files and builds your bundle image: + $ operator-sdk bundle validate config/bundle + $ docker build -f bundle.Dockerfile -t $BUNDLE_IMG . + Sending build context to Docker daemon 42.33MB + Step 1/9 : FROM scratch + ... + + # You can then push your bundle image: + $ make docker-push IMG=$BUNDLE_IMG +` +) + // setCommonDefaults sets defaults useful to all modes of this subcommand. func (c *bundleCmd) setCommonDefaults(cfg *config.Config) { if c.operatorName == "" { diff --git a/cmd/operator-sdk/generate/bundle/bundle_legacy.go b/cmd/operator-sdk/generate/bundle/bundle_legacy.go index 8242f8c90c..7dfad20ffb 100644 --- a/cmd/operator-sdk/generate/bundle/bundle_legacy.go +++ b/cmd/operator-sdk/generate/bundle/bundle_legacy.go @@ -28,6 +28,56 @@ import ( "github.com/operator-framework/operator-sdk/internal/util/projutil" ) +const ( + examplesLegacy = ` + # Using the example 'memcached-operator' and assuming a directory structure + # similar to the following exists: + $ tree pkg/apis/ deploy/ + pkg/apis/ + ├── ... + └── cache + ├── group.go + └── v1alpha1 + ├── ... + └── memcached_types.go + deploy/ + ├── crds + │   ├── cache.example.com_memcacheds_crd.yaml + │   └── cache.example.com_v1alpha1_memcached_cr.yaml + ├── operator.yaml + ├── role.yaml + ├── role_binding.yaml + └── service_account.yaml + + # Create bundle manifests, metadata, and a bundle.Dockerfile: + $ operator-sdk generate bundle --version 0.0.1 + INFO[0000] Generating bundle manifest version 0.0.1 + + Display name for the operator (required): + > memcached-operator + ... + + # After running the above commands, you should see: + $ tree deploy/olm-catalog + deploy/olm-catalog + └── memcached-operator + ├── manifests + │ ├── cache.example.com_memcacheds_crd.yaml + │ └── memcached-operator.clusterserviceversion.yaml + └── metadata + └── annotations.yaml + + # Then build and push your bundle image: + $ export USERNAME= + $ export BUNDLE_IMG=quay.io/$USERNAME/memcached-operator-bundle:v0.0.1 + $ docker build -f bundle.Dockerfile -t $BUNDLE_IMG . + Sending build context to Docker daemon 42.33MB + Step 1/9 : FROM scratch + ... + $ docker push $BUNDLE_IMG +` +) + // setCommonDefaultsLegacy sets defaults useful to all modes of this subcommand. func (c *bundleCmd) setCommonDefaultsLegacy() { if c.operatorName == "" { @@ -58,7 +108,7 @@ func (c bundleCmd) runManifestsLegacy() (err error) { if c.version == "" { log.Info("Generating bundle manifests") } else { - log.Info("Generating bundle manifests version", c.version) + log.Infoln("Generating bundle manifests version", c.version) } } diff --git a/cmd/operator-sdk/generate/bundle/cmd.go b/cmd/operator-sdk/generate/bundle/cmd.go index 424963cd71..fdc4e1d72c 100644 --- a/cmd/operator-sdk/generate/bundle/cmd.go +++ b/cmd/operator-sdk/generate/bundle/cmd.go @@ -25,6 +25,25 @@ import ( "github.com/operator-framework/operator-sdk/internal/util/projutil" ) +const ( + longHelp = ` + Running 'generate bundle' is the first step to publishing your operator to a catalog + and/or deploying it with OLM. This command generates a set of bundle manifests, + metadata, and a bundle.Dockerfile for your operator, and will interactively ask + for UI metadata, an important component of publishing your operator, by default unless + a bundle for your operator exists or you set '--interactive=false'. + + Set '--version' to supply a semantic version for your bundle if you are creating one + for the first time or upgrading an existing one. + + If '--output-dir' is set and you wish to build bundle images from that directory, + either manually update your bundle.Dockerfile or set '--overwrite'. + + More information on bundles: + https://github.com/operator-framework/operator-registry/#manifest-format +` +) + //nolint:maligned type bundleCmd struct { // Options to turn on different parts of bundling. @@ -57,8 +76,10 @@ type bundleCmd struct { func NewCmd() *cobra.Command { c := &bundleCmd{} cmd := &cobra.Command{ - Use: "bundle", - Short: "Generates bundle data for the operator", + Use: "bundle", + Short: "Generates bundle data for the operator", + Long: longHelp, + Example: examples, RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 0 { return fmt.Errorf("command %s doesn't accept any arguments", cmd.CommandPath()) @@ -138,8 +159,10 @@ func NewCmd() *cobra.Command { func NewCmdLegacy() *cobra.Command { c := &bundleCmd{} cmd := &cobra.Command{ - Use: "bundle", - Short: "Generates bundle data for the operator", + Use: "bundle", + Short: "Generates bundle data for the operator", + Long: longHelp, + Example: examplesLegacy, 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()) diff --git a/internal/generate/clusterserviceversion/bases/clusterserviceversion.go b/internal/generate/clusterserviceversion/bases/clusterserviceversion.go index 3c69700a69..970ca7528a 100644 --- a/internal/generate/clusterserviceversion/bases/clusterserviceversion.go +++ b/internal/generate/clusterserviceversion/bases/clusterserviceversion.go @@ -186,7 +186,7 @@ func readClusterServiceVersionBase(path string) (*v1alpha1.ClusterServiceVersion if typeMeta.Kind == v1alpha1.ClusterServiceVersionKind { csv := &v1alpha1.ClusterServiceVersion{} if err := yaml.Unmarshal(manifest, csv); err != nil { - return nil, fmt.Errorf("error unmarshalling ClusterServiceVersion from manifest %s: %v", path, err) + return nil, fmt.Errorf("error unmarshaling ClusterServiceVersion from manifest %s: %v", path, err) } return csv, nil } diff --git a/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go b/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go index 2813a61d40..3cd193b22e 100644 --- a/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go +++ b/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go @@ -314,6 +314,8 @@ func validate(csv *operatorsv1alpha1.ClusterServiceVersion) error { if csv == nil { return errors.New("empty ClusterServiceVersion") } + + hasErrors := false results := validation.ClusterServiceVersionValidator.Validate(csv) for _, r := range results { for _, w := range r.Warnings { @@ -323,8 +325,13 @@ func validate(csv *operatorsv1alpha1.ClusterServiceVersion) error { log.Errorf("ClusterServiceVersion validation: [%s] %s", e.Type, e.Detail) } if r.HasError() { - return errors.New("got ClusterServiceVersion validation errors") + hasErrors = true } } + + if hasErrors { + return errors.New("generated ClusterServiceVersion is invalid") + } + return nil } diff --git a/internal/generate/olm-catalog/csv.go b/internal/generate/olm-catalog/csv.go index 16201694ee..1525a2213d 100644 --- a/internal/generate/olm-catalog/csv.go +++ b/internal/generate/olm-catalog/csv.go @@ -39,9 +39,6 @@ import ( "sigs.k8s.io/yaml" ) -// KB_INTEGRATION_TODO(estroz): generate these using kustomize and pass -// from stdin, like 'make deploy'. - const ( OLMCatalogChildDir = "olm-catalog" // OLMCatalogDir is the default location for OLM catalog directory. diff --git a/internal/plugins/golang/api.go b/internal/plugins/golang/api.go index c844d44a91..1ce547aac3 100644 --- a/internal/plugins/golang/api.go +++ b/internal/plugins/golang/api.go @@ -15,11 +15,22 @@ package golang import ( + "fmt" + "path/filepath" + "strings" + + "github.com/operator-framework/operator-sdk/internal/scaffold/kustomize" + "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/pkg/model/config" "sigs.k8s.io/kubebuilder/pkg/plugin" ) +// sampleKustomizationFragment is a template for samples/kustomization.yaml. +const sampleKustomizationFragment = `## This file is auto-generated, do not modify ## +resources: +` + type createAPIPlugin struct { plugin.CreateAPI @@ -52,5 +63,23 @@ func (p *createAPIPlugin) Run() error { // SDK plugin-specific scaffolds. func (p *createAPIPlugin) run() error { + + // Write CR paths to the samples' kustomization file. This file has a + // "do not modify" comment so it is safe to overwrite. + samplesKustomization := sampleKustomizationFragment + for _, gvk := range p.config.Resources { + samplesKustomization += fmt.Sprintf("- %s\n", makeCRFileName(gvk)) + } + kpath := filepath.Join("config", "samples") + if err := kustomize.Write(kpath, samplesKustomization); err != nil { + return err + } + return nil } + +// makeCRFileName returns a Custom Resource example file name in the same format +// as kubebuilder's CreateAPI plugin for a gvk. +func makeCRFileName(gvk config.GVK) string { + return fmt.Sprintf("%s_%s_%s.yaml", gvk.Group, gvk.Version, strings.ToLower(gvk.Kind)) +} diff --git a/internal/plugins/golang/init.go b/internal/plugins/golang/init.go index 6024351034..e0b0b35e53 100644 --- a/internal/plugins/golang/init.go +++ b/internal/plugins/golang/init.go @@ -16,10 +16,14 @@ package golang import ( "fmt" + "io/ioutil" + "path/filepath" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/pkg/model/config" "sigs.k8s.io/kubebuilder/pkg/plugin" + + "github.com/operator-framework/operator-sdk/internal/scaffold/kustomize" ) type initPlugin struct { @@ -43,11 +47,76 @@ func (p *initPlugin) Run() error { return err } + // Update the scaffolded Makefile with operator-sdk recipes. + // TODO: rewrite this when plugins phase 2 is implemented. + if err := initUpdateMakefile("Makefile"); err != nil { + return fmt.Errorf("error updating Makefile: %v", err) + } + // Update plugin config section with this plugin's configuration. cfg := Config{} if err := p.config.EncodePluginConfig(pluginConfigKey, cfg); err != nil { return fmt.Errorf("error writing plugin config for %s: %v", pluginConfigKey, err) } + // Write a kustomization.yaml to the config directory. + if err := kustomize.Write(filepath.Join("config", "bundle"), bundleKustomization); err != nil { + return err + } + return nil } + +// initUpdateMakefile updates a vanilla kubebuilder Makefile with operator-sdk recipes. +func initUpdateMakefile(filePath string) error { + makefileBytes, err := ioutil.ReadFile(filePath) + if err != nil { + return err + } + + // Prepend bundle variables. + makefileBytes = append([]byte(makefileBundleVarFragment), makefileBytes...) + // Append bundle recipes. + makefileBytes = append(makefileBytes, []byte(makefileBundleFragment)...) + makefileBytes = append(makefileBytes, []byte(makefileBundleBuildFragment)...) + + return ioutil.WriteFile(filePath, makefileBytes, 0644) +} + +// kustomization for bundles. +const bundleKustomization = `resources: +- ../default +- ../samples +` + +// Makefile fragments to add to the base Makefile. +const ( + makefileBundleVarFragment = `# Current Operator version +VERSION ?= 0.0.1 +# Default bundle image tag +BUNDLE_IMG ?= controller-bundle:$(VERSION) +# Options for 'bundle-build' +ifneq ($(origin CHANNELS), undefined) +BUNDLE_CHANNELS := --channels=$(CHANNELS) +endif +ifneq ($(origin DEFAULT_CHANNEL), undefined) +BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) +endif +BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) +` + + //nolint:lll + makefileBundleFragment = ` +# Generate bundle manifests and metadata, then validate generated files. +bundle: manifests + operator-sdk generate bundle -q --kustomize + kustomize build config/bundle | operator-sdk generate bundle -q --manifests --metadata --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) + operator-sdk bundle validate config/bundle +` + + makefileBundleBuildFragment = ` +# Build the bundle image. +bundle-build: + docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . +` +) diff --git a/internal/scaffold/kustomize/kustomize.go b/internal/scaffold/kustomize/kustomize.go new file mode 100644 index 0000000000..b96dd1c7e5 --- /dev/null +++ b/internal/scaffold/kustomize/kustomize.go @@ -0,0 +1,45 @@ +// 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 kustomize + +import ( + "errors" + "io/ioutil" + "os" + "path/filepath" +) + +// File is the standard kustomization file name. kustomize will look for a file +// with this name in a kustomize-able directory. +const File = "kustomization.yaml" + +// Write writes a kustomization.yaml to dir. +func Write(dir, content string) error { + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } + path := filepath.Join(dir, File) + return ioutil.WriteFile(path, []byte(content), 0666) +} + +// WriteIfNotExist writes a kustomization.yaml to dir if the file does not +// already exist. If it does, this function is a no-op. +func WriteIfNotExist(dir, content string) error { + _, err := os.Stat(filepath.Join(dir, File)) + if err != nil && errors.Is(err, os.ErrNotExist) { + return Write(dir, content) + } + return nil +} diff --git a/test/e2e-new/e2e_suite.go b/test/e2e-new/e2e_suite.go index f15e59b269..23280722b7 100644 --- a/test/e2e-new/e2e_suite.go +++ b/test/e2e-new/e2e_suite.go @@ -18,6 +18,9 @@ package e2e import ( "fmt" + "io/ioutil" + "os" + "path" "path/filepath" "strings" "time" @@ -134,6 +137,34 @@ var _ = Describe("operator-sdk", func() { return logOutput } Eventually(managerContainerLogs, time.Minute, time.Second).Should(ContainSubstring("Successfully Reconciled")) + + By("generating the operator bundle") + // Turn off interactive prompts for all generation tasks. + replace := "operator-sdk generate bundle" + replaceInFile(filepath.Join(tc.Dir, "Makefile"), replace, replace+" --interactive=false") + err = tc.Make("bundle") + Expect(err).Should(Succeed()) + + By("building the operator bundle image") + // Use the existing image tag but with a "-bundle" suffix. + imageSplit := strings.SplitN(tc.ImageName, ":", 2) + bundleImage := path.Join("quay.io", imageSplit[0]+"-bundle") + if len(imageSplit) == 2 { + bundleImage += ":" + imageSplit[1] + } + err = tc.Make("bundle-build", "BUNDLE_IMG="+bundleImage) + Expect(err).Should(Succeed()) }) }) }) + +// replaceInFile replaces all instances of old with new in the file at path. +func replaceInFile(path, old, new string) { + info, err := os.Stat(path) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + b, err := ioutil.ReadFile(path) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + s := strings.Replace(string(b), old, new, -1) + err = ioutil.WriteFile(path, []byte(s), info.Mode()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) +} diff --git a/website/content/en/docs/cli/operator-sdk_generate_bundle.md b/website/content/en/docs/cli/operator-sdk_generate_bundle.md index 360b5b86ae..e43c31f672 100644 --- a/website/content/en/docs/cli/operator-sdk_generate_bundle.md +++ b/website/content/en/docs/cli/operator-sdk_generate_bundle.md @@ -7,12 +7,79 @@ Generates bundle data for the operator ### Synopsis -Generates bundle data for the operator + + Running 'generate bundle' is the first step to publishing your operator to a catalog + and/or deploying it with OLM. This command generates a set of bundle manifests, + metadata, and a bundle.Dockerfile for your operator, and will interactively ask + for UI metadata, an important component of publishing your operator, by default unless + a bundle for your operator exists or you set '--interactive=false'. + + Set '--version' to supply a semantic version for your bundle if you are creating one + for the first time or upgrading an existing one. + + If '--output-dir' is set and you wish to build bundle images from that directory, + either manually update your bundle.Dockerfile or set '--overwrite'. + + More information on bundles: + https://github.com/operator-framework/operator-registry/#manifest-format + ``` operator-sdk generate bundle [flags] ``` +### Examples + +``` + + # Using the example 'memcached-operator' and assuming a directory structure + # similar to the following exists: + $ tree pkg/apis/ deploy/ + pkg/apis/ + ├── ... + └── cache + ├── group.go + └── v1alpha1 + ├── ... + └── memcached_types.go + deploy/ + ├── crds + │   ├── cache.example.com_memcacheds_crd.yaml + │   └── cache.example.com_v1alpha1_memcached_cr.yaml + ├── operator.yaml + ├── role.yaml + ├── role_binding.yaml + └── service_account.yaml + + # Create bundle manifests, metadata, and a bundle.Dockerfile: + $ operator-sdk generate bundle --version 0.0.1 + INFO[0000] Generating bundle manifest version 0.0.1 + + Display name for the operator (required): + > memcached-operator + ... + + # After running the above commands, you should see: + $ tree deploy/olm-catalog + deploy/olm-catalog + └── memcached-operator + ├── manifests + │ ├── cache.example.com_memcacheds_crd.yaml + │ └── memcached-operator.clusterserviceversion.yaml + └── metadata + └── annotations.yaml + + # Then build and push your bundle image: + $ export USERNAME= + $ export BUNDLE_IMG=quay.io/$USERNAME/memcached-operator-bundle:v0.0.1 + $ docker build -f bundle.Dockerfile -t $BUNDLE_IMG . + Sending build context to Docker daemon 42.33MB + Step 1/9 : FROM scratch + ... + $ docker push $BUNDLE_IMG + +``` + ### Options ```