From 21c6e83e1308a0a9b72157a6c6c1a59e0948ac7f Mon Sep 17 00:00:00 2001 From: Vu Dinh Date: Fri, 27 Sep 2019 13:21:44 -0400 Subject: [PATCH 1/7] Add CLI tool to build Operator Bundle image 1. Add top level CLI command `operator-cli` 2. Add bundle command to generate annotations.yaml and Dockerfile 3. Add bundle command to build Bundle image * The build command will generate annotations.yaml and Dockerfile as well 4. Add unit test cases for operator bundle commands 5. Add operator bundle documentation Signed-off-by: Vu Dinh --- cmd/operator-cli/bundle/build.go | 108 +++++++++++++++ cmd/operator-cli/bundle/build_test.go | 60 ++++++++ cmd/operator-cli/bundle/cmd.go | 17 +++ cmd/operator-cli/bundle/generate.go | 168 +++++++++++++++++++++++ cmd/operator-cli/bundle/generate_test.go | 85 ++++++++++++ cmd/operator-cli/bundle/utils_test.go | 62 +++++++++ cmd/operator-cli/main.go | 35 +++++ doc/design/operator-bundle.md | 144 +++++++++++++++++++ 8 files changed, 679 insertions(+) create mode 100644 cmd/operator-cli/bundle/build.go create mode 100644 cmd/operator-cli/bundle/build_test.go create mode 100644 cmd/operator-cli/bundle/cmd.go create mode 100644 cmd/operator-cli/bundle/generate.go create mode 100644 cmd/operator-cli/bundle/generate_test.go create mode 100644 cmd/operator-cli/bundle/utils_test.go create mode 100644 cmd/operator-cli/main.go create mode 100644 doc/design/operator-bundle.md diff --git a/cmd/operator-cli/bundle/build.go b/cmd/operator-cli/bundle/build.go new file mode 100644 index 000000000..b5a100497 --- /dev/null +++ b/cmd/operator-cli/bundle/build.go @@ -0,0 +1,108 @@ +package bundle + +import ( + "fmt" + "os" + "os/exec" + "path" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + dirBuildArgs string + tagBuildArgs string + imageBuilderArgs string +) + +// newBundleBuildCmd returns a command that will build operator bundle image. +func newBundleBuildCmd() *cobra.Command { + bundleBuildCmd := &cobra.Command{ + Use: "build", + Short: "Build operator bundle image", + Long: `The operator-cli bundle build command will generate operator + bundle metadata if needed and build bundle image with operator manifest + and metadata. + + For example: The command will generate annotations.yaml metadata plus + Dockerfile for bundle image and then build a container image from + provided operator bundle manifests generated metadata + e.g. "quay.io/example/operator:v0.0.1". + + After the build process is completed, a container image would be built + locally in docker and available to push to a container registry. + + $ operator-cli bundle build -dir /test/0.0.1/ -t quay.io/example/operator:v0.0.1 + + Note: Bundle image is not runnable. + `, + RunE: buildFunc, + } + + bundleBuildCmd.Flags().StringVarP(&dirBuildArgs, "directory", "d", "", "The directory where bundle manifests are located.") + if err := bundleBuildCmd.MarkFlagRequired("directory"); err != nil { + log.Fatalf("Failed to mark `directory` flag for `build` subcommand as required") + } + + bundleBuildCmd.Flags().StringVarP(&tagBuildArgs, "tag", "t", "", "The name of the bundle image will be built.") + if err := bundleBuildCmd.MarkFlagRequired("tag"); err != nil { + log.Fatalf("Failed to mark `tag` flag for `build` subcommand as required") + } + + bundleBuildCmd.Flags().StringVarP(&imageBuilderArgs, "image-builder", "b", "docker", "Tool to build container images. One of: [docker, podman, buildah]") + + return bundleBuildCmd +} + +// Create build command to build bundle manifests image +func buildBundleImage(directory, imageTag, imageBuilder string) (*exec.Cmd, error) { + var args []string + + dockerfilePath := path.Join(directory, dockerFile) + + switch imageBuilder { + case "docker", "podman": + args = append(args, "build", "-f", dockerfilePath, "-t", imageTag, ".") + case "buildah": + args = append(args, "bud", "--format=docker", "-f", dockerfilePath, "-t", imageTag, ".") + default: + return nil, fmt.Errorf("%s is not supported image builder", imageBuilder) + } + + return exec.Command(imageBuilder, args...), nil +} + +func executeCommand(cmd *exec.Cmd) error { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + log.Debugf("Running %#v", cmd.Args) + + if err := cmd.Run(); err != nil { + return fmt.Errorf("Failed to exec %#v: %v", cmd.Args, err) + } + + return nil +} + +func buildFunc(cmd *cobra.Command, args []string) error { + // Generate annotations.yaml and Dockerfile + err := generateFunc(cmd, args) + if err != nil { + return err + } + + // Build bundle image + log.Info("Building bundle image") + buildCmd, err := buildBundleImage(path.Dir(path.Clean(dirBuildArgs)), tagBuildArgs, imageBuilderArgs) + if err != nil { + return err + } + + if err := executeCommand(buildCmd); err != nil { + return err + } + + return nil +} diff --git a/cmd/operator-cli/bundle/build_test.go b/cmd/operator-cli/bundle/build_test.go new file mode 100644 index 000000000..dd9611e36 --- /dev/null +++ b/cmd/operator-cli/bundle/build_test.go @@ -0,0 +1,60 @@ +package bundle + +import ( + "os/exec" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBuildBundleImage(t *testing.T) { + setup("") + defer cleanup() + + tests := []struct { + directory string + imageTag string + imageBuilder string + commandStr string + errorMsg string + }{ + { + operatorDir, + "test", + "docker", + "docker build -f test-operator/Dockerfile -t test .", + "", + }, + { + operatorDir, + "test", + "podman", + "podman bud --format=docker -f test-operator/Dockerfile -t test .", + "", + }, + { + operatorDir, + "test", + "buildah", + "buildah build -f test-operator/Dockerfile -t test .", + "", + }, + { + operatorDir, + "test", + "hello", + "", + "hello is not supported image builder", + }, + } + + for _, item := range tests { + var cmd *exec.Cmd + cmd, err := buildBundleImage(item.directory, item.imageTag, item.imageBuilder) + if item.errorMsg == "" { + require.Equal(t, item.commandStr, cmd.String()) + } else { + require.Equal(t, item.errorMsg, err.Error()) + } + } +} diff --git a/cmd/operator-cli/bundle/cmd.go b/cmd/operator-cli/bundle/cmd.go new file mode 100644 index 000000000..6abccd54e --- /dev/null +++ b/cmd/operator-cli/bundle/cmd.go @@ -0,0 +1,17 @@ +package bundle + +import ( + "github.com/spf13/cobra" +) + +func NewCmd() *cobra.Command { + runCmd := &cobra.Command{ + Use: "bundle", + Short: "Operator bundle commands", + Long: `Generate operator bundle metadata and build bundle image.`, + } + + runCmd.AddCommand(newBundleGenerateCmd()) + runCmd.AddCommand(newBundleBuildCmd()) + return runCmd +} diff --git a/cmd/operator-cli/bundle/generate.go b/cmd/operator-cli/bundle/generate.go new file mode 100644 index 000000000..796cda9a0 --- /dev/null +++ b/cmd/operator-cli/bundle/generate.go @@ -0,0 +1,168 @@ +package bundle + +import ( + "fmt" + "io/ioutil" + "path" + "path/filepath" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "gopkg.in/yaml.v2" +) + +const ( + defaultPermission = 0644 + registryV1Type = "registry+v1" + plainType = "plain" + helmType = "helm" + manifestsMetadata = "manifests+metadata" + annotationsFile = "annotations.yaml" + dockerFile = "Dockerfile" + resourcesLabel = "operators.operatorframework.io.bundle.resources" + mediatypeLabel = "operators.operatorframework.io.bundle.mediatype" +) + +type AnnotationMetadata struct { + Annotations AnnotationType `yaml:"annotations"` +} + +type AnnotationType struct { + Resources string `yaml:"operators.operatorframework.io.bundle.resources"` + MediaType string `yaml:"operators.operatorframework.io.bundle.mediatype"` +} + +// newBundleBuildCmd returns a command that will build operator bundle image. +func newBundleGenerateCmd() *cobra.Command { + bundleGenerateCmd := &cobra.Command{ + Use: "generate", + Short: "Generate operator bundle metadata and Dockerfile", + Long: `The operator-cli buindle generate command will generate operator + bundle metadata if needed and a Dockerfile to build Operator bundle image. + + $ operator-cli bundle generate -d /test/0.0.1/ + `, + RunE: generateFunc, + } + + bundleGenerateCmd.Flags().StringVarP(&dirBuildArgs, "directory", "d", "", "The directory where bundle manifests are located.") + if err := bundleGenerateCmd.MarkFlagRequired("directory"); err != nil { + log.Fatalf("Failed to mark `directory` flag for `generate` subcommand as required") + } + + return bundleGenerateCmd +} + +func generateFunc(cmd *cobra.Command, args []string) error { + var mediaType string + + // Determine mediaType + mediaType, err := getMediaType(dirBuildArgs) + if err != nil { + return err + } + + // Parent directory + parentDir := path.Dir(path.Clean(dirBuildArgs)) + + log.Info("Building annotations.yaml file") + + // Generate annotations.yaml + content, err := generateAnnotationsFunc(manifestsMetadata, mediaType) + if err != nil { + return err + } + err = writeFile(annotationsFile, parentDir, content) + if err != nil { + return err + } + + log.Info("Building Dockerfile") + + // Generate Dockerfile + content = generateDockerfileFunc(manifestsMetadata, mediaType, dirBuildArgs) + err = writeFile(dockerFile, parentDir, content) + if err != nil { + return err + } + + return nil +} + +func getMediaType(dirBuildArgs string) (string, error) { + var files []string + + // Read all file names in directory + items, _ := ioutil.ReadDir(dirBuildArgs) + for _, item := range items { + if item.IsDir() { + continue + } else { + files = append(files, item.Name()) + } + } + + if len(files) == 0 { + return "", fmt.Errorf("The directory %s contains no files", dirBuildArgs) + } + + // Validate the file names to determine media type + for _, file := range files { + if file == "Chart.yaml" { + return helmType, nil + } else if strings.HasSuffix(file, "clusterserviceversion.yaml") { + return registryV1Type, nil + } else { + continue + } + } + + return plainType, nil +} + +func generateAnnotationsFunc(resourcesType, mediaType string) ([]byte, error) { + annotations := &AnnotationMetadata{ + Annotations: AnnotationType{ + Resources: resourcesType, + MediaType: mediaType, + }, + } + + afile, err := yaml.Marshal(annotations) + if err != nil { + return nil, err + } + + return afile, nil +} + +func generateDockerfileFunc(resourcesType, mediaType, directory string) []byte { + var fileContent string + + metadataDir := path.Dir(path.Clean(directory)) + + // FROM + fileContent += "FROM scratch\n\n" + + // LABEL + fileContent += fmt.Sprintf("LABEL %s=%s\n", resourcesLabel, resourcesType) + fileContent += fmt.Sprintf("LABEL %s=%s\n\n", mediatypeLabel, mediaType) + + // CONTENT + fileContent += fmt.Sprintf("ADD %s %s\n", directory, "/manifests") + fileContent += fmt.Sprintf("ADD %s/%s %s%s\n", metadataDir, annotationsFile, "/metadata/", annotationsFile) + + return []byte(fileContent) +} + +// Write `fileName` file with `content` into a `directory` +// Note: Will overwrite the existing `fileName` file if it exists +func writeFile(fileName, directory string, content []byte) error { + err := ioutil.WriteFile(filepath.Join(directory, fileName), content, defaultPermission) + if err != nil { + return err + } + return nil +} diff --git a/cmd/operator-cli/bundle/generate_test.go b/cmd/operator-cli/bundle/generate_test.go new file mode 100644 index 000000000..a519bf4ee --- /dev/null +++ b/cmd/operator-cli/bundle/generate_test.go @@ -0,0 +1,85 @@ +package bundle + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func TestGetMediaType(t *testing.T) { + setup("") + defer cleanup() + + testDir := operatorDir + manifestsMetadata + tests := []struct { + directory string + mediaType string + errorMsg string + }{ + { + testDir, + registryV1Type, + "", + }, + { + testDir, + helmType, + "", + }, + { + testDir, + plainType, + "", + }, + { + testDir, + "", + fmt.Sprintf("The directory %s contains no files", testDir), + }, + } + + for _, item := range tests { + createFiles(testDir, item.mediaType) + manifestType, err := getMediaType(item.directory) + if item.errorMsg == "" { + require.Equal(t, item.mediaType, manifestType) + } else { + require.Equal(t, item.errorMsg, err.Error()) + } + clearDir(testDir) + } +} + +func TestGenerateAnnotationsFunc(t *testing.T) { + // Create test annotations struct + testAnnotations := &AnnotationMetadata{ + Annotations: AnnotationType{ + Resources: "test1", + MediaType: "test2", + }, + } + // Create result annotations struct + resultAnnotations := AnnotationMetadata{} + data, err := generateAnnotationsFunc("test1", "test2") + require.NoError(t, err) + + err = yaml.Unmarshal(data, &resultAnnotations) + require.NoError(t, err) + + require.Equal(t, testAnnotations.Annotations.Resources, resultAnnotations.Annotations.Resources) + require.Equal(t, testAnnotations.Annotations.MediaType, resultAnnotations.Annotations.Resources) +} + +func TestGenerateDockerfileFunc(t *testing.T) { + testDir := operatorDir + manifestsMetadata + output := "FROM scratch\n\n" + + "LABEL operators.operatorframework.io.bundle.resources=test1\n" + + "LABEL operators.operatorframework.io.bundle.mediatype=test2\n\n" + + "ADD test-operator/0.0.1 /manifests\n" + + "ADD test-operator/0.0.1/annotations.yaml /metadata/annotations.yaml\n" + + content := generateDockerfileFunc("test1", "test2", testDir) + require.Equal(t, output, string(content)) +} diff --git a/cmd/operator-cli/bundle/utils_test.go b/cmd/operator-cli/bundle/utils_test.go new file mode 100644 index 000000000..37f1cb3c3 --- /dev/null +++ b/cmd/operator-cli/bundle/utils_test.go @@ -0,0 +1,62 @@ +package bundle + +import ( + "io/ioutil" + "os" + "path/filepath" +) + +const ( + operatorDir = "/test-operator" + manifestsDir = "/0.0.1" + helmFile = "Chart.yaml" + csvFile = "test.clusterserviceversion.yaml" + crdFile = "test.crd.yaml" +) + +func setup(input string) { + // Create test directory + testDir := operatorDir + manifestsDir + createDir(testDir) + + // Create test files in test directory + createFiles(testDir, input) +} + +func cleanup() { + // Remove test directory + os.RemoveAll(operatorDir) +} + +func createDir(dir string) { + os.MkdirAll(dir, os.ModePerm) +} + +func createFiles(dir, input string) { + // Create test files in test directory + switch input { + case registryV1Type: + file, _ := os.Create(filepath.Join(dir, csvFile)) + file.Close() + case helmType: + file, _ := os.Create(filepath.Join(dir, helmFile)) + file.Close() + case plainType: + file, _ := os.Create(filepath.Join(dir, crdFile)) + file.Close() + default: + break + } +} + +func clearDir(dir string) { + items, _ := ioutil.ReadDir(dir) + + for _, item := range items { + if item.IsDir() { + continue + } else { + os.Remove(item.Name()) + } + } +} diff --git a/cmd/operator-cli/main.go b/cmd/operator-cli/main.go new file mode 100644 index 000000000..83c3ca502 --- /dev/null +++ b/cmd/operator-cli/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "os" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/operator-framework/operator-lifecycle-manager/cmd/operator-cli/bundle" +) + +func main() { + var rootCmd = &cobra.Command{ + Short: "operator-cli", + Long: `A CLI tool to perform operator-related tasks.`, + + PreRunE: func(cmd *cobra.Command, args []string) error { + if debug, _ := cmd.Flags().GetBool("debug"); debug { + log.SetLevel(log.DebugLevel) + } + return nil + }, + } + + rootCmd.AddCommand(bundle.NewCmd()) + + rootCmd.Flags().Bool("debug", false, "enable debug logging") + if err := rootCmd.Flags().MarkHidden("debug"); err != nil { + log.Panic(err.Error()) + } + + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/doc/design/operator-bundle.md b/doc/design/operator-bundle.md new file mode 100644 index 000000000..9c5b29a2d --- /dev/null +++ b/doc/design/operator-bundle.md @@ -0,0 +1,144 @@ +# Operator Bundle + +An `Operator Bundle` is a container image that stores Kubernetes manifests and metadata associated with an operator. A bundle is meant to present a specific version of an operator. + +## Operator Bundle Overview + +The operator manifests refers to a set of Kubernetes manifest(s) the defines the deployment and RBAC model of the operator. The operator metadata on the other hand are, but not limited to: +* Information that identifies the operator, its name, version etc. +* Additional information that drives the UI: + * Icon + * Example CR(s) +* Channel(s) +* API(s) provided and required. +* Related images. + +An `Operator Bundle` is built as scratch (non-runnable) container image that contains operator manifests and specific metadata in designated directories inside the image. Then, it can be pushed and pulled from an OCI-compliant container registry. Ultimately, an operator bundle will be used by [Operator Registry](https://github.com/operator-framework/operator-registry) and [Operator-Lifecycle-Manager (OLM)](https://github.com/operator-framework/operator-lifecycle-manager) to install an operator in OLM-enabled clusters. + +### Bundle Annotations + +We use the following labels to annotate the operator bundle image. +* The label `operators.operatorframework.io.bundle.resources` represents the bundle type: + * The value `manifests` implies that this bundle contains operator manifests only. + * The value `metadata` implies that this bundle has operator metadata only. + * The value `manifests+metadata` implies that this bundle contains both operator metadata and manifests. +* The label `operators.operatorframework.io.bundle.mediatype` reflects the media type or format of the operator bundle. It could be helm charts, plain Kubernetes manifests etc. + +The labels will also be put inside a YAML file, as shown below. + +*annotations.yaml* +```yaml +annotations: + operators.operatorframework.io.bundle.resources: "manifests+metadata" + operators.operatorframework.io.bundle.mediatype: "registry+v1" +``` + +*Notes:* +* In case of a mismatch, the `annotations.yaml` file is authoritative because on-cluster operator-registry that relies on these annotations has access to the yaml file only. +* The potential use case for the `LABELS` is - an external off-cluster tool can inspect the image to check the type of a given bundle image without downloading the content. + +This example uses [Operator Registry Manifests](https://github.com/operator-framework/operator-registry#manifest-format) format to build an operator bundle image. The source directory of an operator registry bundle has the following layout. +``` +$ tree test +test +├── 0.1.0 +│   ├── testbackup.crd.yaml +│   ├── testcluster.crd.yaml +│   ├── testoperator.v0.1.0.clusterserviceversion.yaml +│   └── testrestore.crd.yaml +└── annotations.yaml +``` + +### Bundle Dockerfile + +This is an example of a `Dockerfile` for operator bundle: +``` +FROM scratch + +# We are pushing an operator-registry bundle +# that has both metadata and manifests. +LABEL operators.operatorframework.io.bundle.resources=manifests+metadata +LABEL operators.operatorframework.io.bundle.mediatype=registry+v1 + +ADD test/0.1.0 /manifests +ADD test/annotations.yaml /metadata/annotations.yaml +``` + +Below is the directory layout of the operator bundle inside the image: +```bash +$ tree +/ +├── manifests +│ ├── testbackup.crd.yaml +│ ├── testcluster.crd.yaml +│ ├── testoperator.v0.1.0.clusterserviceversion.yaml +│ └── testrestore.crd.yaml +└── metadata + └── annotations.yaml +``` + +## Operator Bundle Command-Line Tool + +A CLI tool is available to generate Bundle annotations and Dockerfile based on provided operator manifests. + +### Bundle CLI + +In order to build `operator-cli` CLI tool, follow these steps: + +1. Clone [Operator-Lifecycle-Manager (OLM)](https://github.com/operator-framework/operator-lifecycle-manager) repository. +2. Build `operator-cli` binary: +```bash +$ go build ./cmd/operator-cli/ +``` + +Now, a binary named `operator-cli` is available in OLM's directory to use. +```bash +$ ./operator-cli +Generate operator bundle metadata and build bundle image. + +Usage: + bundle [command] + +Available Commands: + build Build operator bundle image + generate Generate operator bundle metadata and Dockerfile + +Flags: + -h, --help help for bundle + +Use " bundle [command] --help" for more information about a command. +``` + +### Generate Bundle Annotations and DockerFile + +Using `operator-cli` CLI, bundle annotations can be generated from provided operator manifests. The command for `generate` task is: +```bash +$ ./operator-cli bundle generate --directory /test/0.0.1/ +``` +The `--directory` or `-d` specifies the directory where the operator manifests are located. The `annotations.yaml` and `Dockerfile` are generated in the same directory where the manifests folder is located (not where the YAML manifests are located). For example: +```bash +$ tree test +test +├── 0.0.1 +│   ├── testbackup.crd.yaml +│   ├── testcluster.crd.yaml +│   ├── testoperator.v0.1.0.clusterserviceversion.yaml +│   └── testrestore.crd.yaml +├── annotations.yaml +└── Dockerfile +``` + +Note: If there are `annotations.yaml` and `Dockerfile` existing in the directory, they will be overwritten. + +### Build Bundle Image + +Operator bundle image can be built from provided operator manifests using `build` command: +```bash +$ ./operator-cli bundle build --directory /test/0.0.1/ --tag quay.io/coreos/test-operator.v0.0.1:latest +``` +The `--directory` or `-d` specifies the directory where the operator manifests are located. The `--tag` or `-t` specifies the image tag that you want the operator bundle image to have. By using `build` command, the `annotations.yaml` and `Dockerfile` are automatically generated in the background. + +The default image builder is `Docker`. However, ` Buildah` and `Podman` are also supported. An image builder can specified via `--image-builder` or `-b` optional tag in `build` command. For example: +```bash +$ ./operator-cli bundle build --directory /test/0.0.1/ --tag quay.io/coreos/test-operator.v0.0.1:latest --image-builder podman +``` From d8d5ec266bf4eae92f204842fcae103e1bc457d1 Mon Sep 17 00:00:00 2001 From: Vu Dinh Date: Tue, 1 Oct 2019 09:42:42 -0400 Subject: [PATCH 2/7] Clean up library code, unit tests and documentation details 1. Move bundle funcs to /pkg as a bundle package 2. ix some minor errors in unit test. 3. Modify bundle doc to reflect recent changes + operator-sdk information + Folder structure 4. Add bundle command to opm binary as hidden commands + The bundle commands are only available for internal development use as a part of opm binary. 5. Move bundle doc under /docs/design. Signed-off-by: Vu Dinh --- cmd/operator-cli/main.go | 35 --------- cmd/{operator-cli => opm}/bundle/build.go | 55 +------------ cmd/{operator-cli => opm}/bundle/cmd.go | 7 +- cmd/opm/bundle/generate.go | 59 ++++++++++++++ cmd/opm/main.go | 2 + {doc => docs}/design/operator-bundle.md | 77 ++++++++++--------- pkg/lib/bundle/build.go | 62 +++++++++++++++ .../lib}/bundle/build_test.go | 14 ++-- .../lib}/bundle/generate.go | 48 ++++-------- .../lib}/bundle/generate_test.go | 17 ++-- .../lib}/bundle/utils_test.go | 13 +++- 11 files changed, 209 insertions(+), 180 deletions(-) delete mode 100644 cmd/operator-cli/main.go rename cmd/{operator-cli => opm}/bundle/build.go (57%) rename cmd/{operator-cli => opm}/bundle/cmd.go (59%) create mode 100644 cmd/opm/bundle/generate.go rename {doc => docs}/design/operator-bundle.md (57%) create mode 100644 pkg/lib/bundle/build.go rename {cmd/operator-cli => pkg/lib}/bundle/build_test.go (70%) rename {cmd/operator-cli => pkg/lib}/bundle/generate.go (63%) rename {cmd/operator-cli => pkg/lib}/bundle/generate_test.go (77%) rename {cmd/operator-cli => pkg/lib}/bundle/utils_test.go (79%) diff --git a/cmd/operator-cli/main.go b/cmd/operator-cli/main.go deleted file mode 100644 index 83c3ca502..000000000 --- a/cmd/operator-cli/main.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "os" - - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - - "github.com/operator-framework/operator-lifecycle-manager/cmd/operator-cli/bundle" -) - -func main() { - var rootCmd = &cobra.Command{ - Short: "operator-cli", - Long: `A CLI tool to perform operator-related tasks.`, - - PreRunE: func(cmd *cobra.Command, args []string) error { - if debug, _ := cmd.Flags().GetBool("debug"); debug { - log.SetLevel(log.DebugLevel) - } - return nil - }, - } - - rootCmd.AddCommand(bundle.NewCmd()) - - rootCmd.Flags().Bool("debug", false, "enable debug logging") - if err := rootCmd.Flags().MarkHidden("debug"); err != nil { - log.Panic(err.Error()) - } - - if err := rootCmd.Execute(); err != nil { - os.Exit(1) - } -} diff --git a/cmd/operator-cli/bundle/build.go b/cmd/opm/bundle/build.go similarity index 57% rename from cmd/operator-cli/bundle/build.go rename to cmd/opm/bundle/build.go index b5a100497..8722dbe5d 100644 --- a/cmd/operator-cli/bundle/build.go +++ b/cmd/opm/bundle/build.go @@ -1,11 +1,7 @@ package bundle import ( - "fmt" - "os" - "os/exec" - "path" - + "github.com/operator-framework/operator-registry/pkg/lib/bundle" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -21,7 +17,7 @@ func newBundleBuildCmd() *cobra.Command { bundleBuildCmd := &cobra.Command{ Use: "build", Short: "Build operator bundle image", - Long: `The operator-cli bundle build command will generate operator + Long: `The opm bundle build command will generate operator bundle metadata if needed and build bundle image with operator manifest and metadata. @@ -33,7 +29,7 @@ func newBundleBuildCmd() *cobra.Command { After the build process is completed, a container image would be built locally in docker and available to push to a container registry. - $ operator-cli bundle build -dir /test/0.0.1/ -t quay.io/example/operator:v0.0.1 + $ opm bundle build -dir /test/0.0.1/ -t quay.io/example/operator:v0.0.1 Note: Bundle image is not runnable. `, @@ -55,54 +51,11 @@ func newBundleBuildCmd() *cobra.Command { return bundleBuildCmd } -// Create build command to build bundle manifests image -func buildBundleImage(directory, imageTag, imageBuilder string) (*exec.Cmd, error) { - var args []string - - dockerfilePath := path.Join(directory, dockerFile) - - switch imageBuilder { - case "docker", "podman": - args = append(args, "build", "-f", dockerfilePath, "-t", imageTag, ".") - case "buildah": - args = append(args, "bud", "--format=docker", "-f", dockerfilePath, "-t", imageTag, ".") - default: - return nil, fmt.Errorf("%s is not supported image builder", imageBuilder) - } - - return exec.Command(imageBuilder, args...), nil -} - -func executeCommand(cmd *exec.Cmd) error { - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - log.Debugf("Running %#v", cmd.Args) - - if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to exec %#v: %v", cmd.Args, err) - } - - return nil -} - func buildFunc(cmd *cobra.Command, args []string) error { - // Generate annotations.yaml and Dockerfile - err := generateFunc(cmd, args) - if err != nil { - return err - } - - // Build bundle image - log.Info("Building bundle image") - buildCmd, err := buildBundleImage(path.Dir(path.Clean(dirBuildArgs)), tagBuildArgs, imageBuilderArgs) + err := bundle.BuildFunc(dirBuildArgs, tagBuildArgs, imageBuilderArgs) if err != nil { return err } - if err := executeCommand(buildCmd); err != nil { - return err - } - return nil } diff --git a/cmd/operator-cli/bundle/cmd.go b/cmd/opm/bundle/cmd.go similarity index 59% rename from cmd/operator-cli/bundle/cmd.go rename to cmd/opm/bundle/cmd.go index 6abccd54e..1c05771b4 100644 --- a/cmd/operator-cli/bundle/cmd.go +++ b/cmd/opm/bundle/cmd.go @@ -6,9 +6,10 @@ import ( func NewCmd() *cobra.Command { runCmd := &cobra.Command{ - Use: "bundle", - Short: "Operator bundle commands", - Long: `Generate operator bundle metadata and build bundle image.`, + Hidden: true, + Use: "bundle", + Short: "Operator bundle commands", + Long: `Generate operator bundle metadata and build bundle image.`, } runCmd.AddCommand(newBundleGenerateCmd()) diff --git a/cmd/opm/bundle/generate.go b/cmd/opm/bundle/generate.go new file mode 100644 index 000000000..57b84f626 --- /dev/null +++ b/cmd/opm/bundle/generate.go @@ -0,0 +1,59 @@ +package bundle + +import ( + "github.com/operator-framework/operator-registry/pkg/lib/bundle" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +const ( + defaultPermission = 0644 + registryV1Type = "registry+v1" + plainType = "plain" + helmType = "helm" + manifestsMetadata = "manifests+metadata" + annotationsFile = "annotations.yaml" + dockerFile = "Dockerfile" + resourcesLabel = "operators.operatorframework.io.bundle.resources" + mediatypeLabel = "operators.operatorframework.io.bundle.mediatype" +) + +type AnnotationMetadata struct { + Annotations AnnotationType `yaml:"annotations"` +} + +type AnnotationType struct { + Resources string `yaml:"operators.operatorframework.io.bundle.resources"` + MediaType string `yaml:"operators.operatorframework.io.bundle.mediatype"` +} + +// newBundleGenerateCmd returns a command that will generate operator bundle +// annotations.yaml metadata +func newBundleGenerateCmd() *cobra.Command { + bundleGenerateCmd := &cobra.Command{ + Use: "generate", + Short: "Generate operator bundle metadata and Dockerfile", + Long: `The opm buindle generate command will generate operator + bundle metadata if needed and a Dockerfile to build Operator bundle image. + + $ opm bundle generate -d /test/0.0.1/ + `, + RunE: generateFunc, + } + + bundleGenerateCmd.Flags().StringVarP(&dirBuildArgs, "directory", "d", "", "The directory where bundle manifests are located.") + if err := bundleGenerateCmd.MarkFlagRequired("directory"); err != nil { + log.Fatalf("Failed to mark `directory` flag for `generate` subcommand as required") + } + + return bundleGenerateCmd +} + +func generateFunc(cmd *cobra.Command, args []string) error { + err := bundle.GenerateFunc(dirBuildArgs) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/opm/main.go b/cmd/opm/main.go index bcfcbd7fa..e1011c105 100644 --- a/cmd/opm/main.go +++ b/cmd/opm/main.go @@ -4,6 +4,7 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/operator-framework/operator-registry/cmd/opm/bundle" "github.com/operator-framework/operator-registry/cmd/opm/registry" ) @@ -15,6 +16,7 @@ func main() { } rootCmd.AddCommand(registry.NewOpmRegistryCmd()) + rootCmd.AddCommand(bundle.NewCmd()) if err := rootCmd.Execute(); err != nil { logrus.Panic(err.Error()) diff --git a/doc/design/operator-bundle.md b/docs/design/operator-bundle.md similarity index 57% rename from doc/design/operator-bundle.md rename to docs/design/operator-bundle.md index 9c5b29a2d..4566c0318 100644 --- a/doc/design/operator-bundle.md +++ b/docs/design/operator-bundle.md @@ -1,6 +1,6 @@ # Operator Bundle -An `Operator Bundle` is a container image that stores Kubernetes manifests and metadata associated with an operator. A bundle is meant to present a specific version of an operator. +An `Operator Bundle` is a container image that stores Kubernetes manifests and metadata associated with an operator. A bundle is meant to present a *specific* version of an operator. ## Operator Bundle Overview @@ -13,7 +13,7 @@ The operator manifests refers to a set of Kubernetes manifest(s) the defines the * API(s) provided and required. * Related images. -An `Operator Bundle` is built as scratch (non-runnable) container image that contains operator manifests and specific metadata in designated directories inside the image. Then, it can be pushed and pulled from an OCI-compliant container registry. Ultimately, an operator bundle will be used by [Operator Registry](https://github.com/operator-framework/operator-registry) and [Operator-Lifecycle-Manager (OLM)](https://github.com/operator-framework/operator-lifecycle-manager) to install an operator in OLM-enabled clusters. +An `Operator Bundle` is built as a scratch (non-runnable) container image that contains operator manifests and specific metadata in designated directories inside the image. Then, it can be pushed and pulled from an OCI-compliant container registry. Ultimately, an operator bundle will be used by [Operator Registry](https://github.com/operator-framework/operator-registry) and [Operator-Lifecycle-Manager (OLM)](https://github.com/operator-framework/operator-lifecycle-manager) to install an operator in OLM-enabled clusters. ### Bundle Annotations @@ -34,19 +34,19 @@ annotations: ``` *Notes:* -* In case of a mismatch, the `annotations.yaml` file is authoritative because on-cluster operator-registry that relies on these annotations has access to the yaml file only. +* In case of a mismatch, the `annotations.yaml` file is authoritative because the on-cluster operator-registry that relies on these annotations has access to the yaml file only. * The potential use case for the `LABELS` is - an external off-cluster tool can inspect the image to check the type of a given bundle image without downloading the content. This example uses [Operator Registry Manifests](https://github.com/operator-framework/operator-registry#manifest-format) format to build an operator bundle image. The source directory of an operator registry bundle has the following layout. ``` $ tree test test -├── 0.1.0 -│   ├── testbackup.crd.yaml -│   ├── testcluster.crd.yaml -│   ├── testoperator.v0.1.0.clusterserviceversion.yaml -│   └── testrestore.crd.yaml -└── annotations.yaml +├── testbackup.crd.yaml +├── testcluster.crd.yaml +├── testoperator.v0.1.0.clusterserviceversion.yaml +├── testrestore.crd.yaml +└── metadata + └── annotations.yaml ``` ### Bundle Dockerfile @@ -60,7 +60,7 @@ FROM scratch LABEL operators.operatorframework.io.bundle.resources=manifests+metadata LABEL operators.operatorframework.io.bundle.mediatype=registry+v1 -ADD test/0.1.0 /manifests +ADD test/*.yaml /manifests ADD test/annotations.yaml /metadata/annotations.yaml ``` @@ -77,68 +77,69 @@ $ tree └── annotations.yaml ``` -## Operator Bundle Command-Line Tool +## Operator Bundle Commands -A CLI tool is available to generate Bundle annotations and Dockerfile based on provided operator manifests. +Operator SDK CLI is available to generate Bundle annotations and Dockerfile based on provided operator manifests. -### Bundle CLI +### Operator SDK CLI -In order to build `operator-cli` CLI tool, follow these steps: +In order to use Operator SDK CLI, follow the operator-SDK installation instruction: -1. Clone [Operator-Lifecycle-Manager (OLM)](https://github.com/operator-framework/operator-lifecycle-manager) repository. -2. Build `operator-cli` binary: -```bash -$ go build ./cmd/operator-cli/ -``` +1. Install the [Operator SDK CLI](https://github.com/operator-framework/operator-sdk/blob/master/doc/user/install-operator-sdk.md) Now, a binary named `operator-cli` is available in OLM's directory to use. ```bash -$ ./operator-cli -Generate operator bundle metadata and build bundle image. +$ ./operator-sdk +An SDK for building operators with ease Usage: - bundle [command] + operator-sdk [command] Available Commands: - build Build operator bundle image - generate Generate operator bundle metadata and Dockerfile + bundle Operator bundle commands Flags: - -h, --help help for bundle + -h, --help help for operator-sdk + --verbose Enable verbose logging -Use " bundle [command] --help" for more information about a command. +Use "operator-sdk [command] --help" for more information about a command. ``` ### Generate Bundle Annotations and DockerFile -Using `operator-cli` CLI, bundle annotations can be generated from provided operator manifests. The command for `generate` task is: +Using `operator-sdk` CLI, bundle annotations can be generated from provided operator manifests. The command for `generate` task is: ```bash -$ ./operator-cli bundle generate --directory /test/0.0.1/ +$ ./operator-sdk bundle generate --directory /test/ ``` -The `--directory` or `-d` specifies the directory where the operator manifests are located. The `annotations.yaml` and `Dockerfile` are generated in the same directory where the manifests folder is located (not where the YAML manifests are located). For example: +The `--directory` or `-d` specifies the directory where the operator manifests are located. The `Dockerfile` is generated in the same directory where the YAML manifests are located while the `annotations.yaml` file is located in a folder named `metadata`. For example: ```bash $ tree test test -├── 0.0.1 -│   ├── testbackup.crd.yaml -│   ├── testcluster.crd.yaml -│   ├── testoperator.v0.1.0.clusterserviceversion.yaml -│   └── testrestore.crd.yaml -├── annotations.yaml +├── testbackup.crd.yaml +├── testcluster.crd.yaml +├── testoperator.v0.1.0.clusterserviceversion.yaml +├── testrestore.crd.yaml +├── metadata +│   └── annotations.yaml └── Dockerfile ``` -Note: If there are `annotations.yaml` and `Dockerfile` existing in the directory, they will be overwritten. +*Notes:* +* If there are `annotations.yaml` and `Dockerfile` existing in the directory, they will be overwritten. ### Build Bundle Image Operator bundle image can be built from provided operator manifests using `build` command: ```bash -$ ./operator-cli bundle build --directory /test/0.0.1/ --tag quay.io/coreos/test-operator.v0.0.1:latest +$ ./operator-sdk bundle build --directory /test/0.1.0/ --tag quay.io/coreos/test-operator.v0.1.0:latest ``` The `--directory` or `-d` specifies the directory where the operator manifests are located. The `--tag` or `-t` specifies the image tag that you want the operator bundle image to have. By using `build` command, the `annotations.yaml` and `Dockerfile` are automatically generated in the background. The default image builder is `Docker`. However, ` Buildah` and `Podman` are also supported. An image builder can specified via `--image-builder` or `-b` optional tag in `build` command. For example: ```bash -$ ./operator-cli bundle build --directory /test/0.0.1/ --tag quay.io/coreos/test-operator.v0.0.1:latest --image-builder podman +$ ./operator-sdk bundle build --directory /test/0.1.0/ --tag quay.io/coreos/test-operator.v0.1.0:latest --image-builder podman ``` + +*Notes:* +* If there are `annotations.yaml` and `Dockerfile` existing in the directory, they will be overwritten. +* The directory where the operator manifests are located must must be inside the context of the build which in this case is inside the directory where you run the command. diff --git a/pkg/lib/bundle/build.go b/pkg/lib/bundle/build.go new file mode 100644 index 000000000..38a49b4d8 --- /dev/null +++ b/pkg/lib/bundle/build.go @@ -0,0 +1,62 @@ +package bundle + +import ( + "fmt" + "os" + "os/exec" + "path" + + log "github.com/sirupsen/logrus" +) + +// Create build command to build bundle manifests image +func BuildBundleImage(directory, imageTag, imageBuilder string) (*exec.Cmd, error) { + var args []string + + dockerfilePath := path.Join(directory, dockerFile) + + switch imageBuilder { + case "docker", "podman": + args = append(args, "build", "-f", dockerfilePath, "-t", imageTag, ".") + case "buildah": + args = append(args, "bud", "--format=docker", "-f", dockerfilePath, "-t", imageTag, ".") + default: + return nil, fmt.Errorf("%s is not supported image builder", imageBuilder) + } + + return exec.Command(imageBuilder, args...), nil +} + +func ExecuteCommand(cmd *exec.Cmd) error { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + log.Debugf("Running %#v", cmd.Args) + + if err := cmd.Run(); err != nil { + return fmt.Errorf("Failed to exec %#v: %v", cmd.Args, err) + } + + return nil +} + +func BuildFunc(directory, imageTag, imageBuilder string) error { + // Generate annotations.yaml and Dockerfile + err := GenerateFunc(directory) + if err != nil { + return err + } + + // Build bundle image + log.Info("Building bundle image") + buildCmd, err := BuildBundleImage(path.Dir(path.Clean(directory)), imageBuilder, imageTag) + if err != nil { + return err + } + + if err := ExecuteCommand(buildCmd); err != nil { + return err + } + + return nil +} diff --git a/cmd/operator-cli/bundle/build_test.go b/pkg/lib/bundle/build_test.go similarity index 70% rename from cmd/operator-cli/bundle/build_test.go rename to pkg/lib/bundle/build_test.go index dd9611e36..bf28ef0de 100644 --- a/cmd/operator-cli/bundle/build_test.go +++ b/pkg/lib/bundle/build_test.go @@ -22,21 +22,21 @@ func TestBuildBundleImage(t *testing.T) { operatorDir, "test", "docker", - "docker build -f test-operator/Dockerfile -t test .", + "docker build -f /test-operator/Dockerfile -t test .", "", }, { operatorDir, "test", - "podman", - "podman bud --format=docker -f test-operator/Dockerfile -t test .", + "buildah", + "buildah bud --format=docker -f /test-operator/Dockerfile -t test .", "", }, { operatorDir, "test", - "buildah", - "buildah build -f test-operator/Dockerfile -t test .", + "podman", + "podman build -f /test-operator/Dockerfile -t test .", "", }, { @@ -50,9 +50,9 @@ func TestBuildBundleImage(t *testing.T) { for _, item := range tests { var cmd *exec.Cmd - cmd, err := buildBundleImage(item.directory, item.imageTag, item.imageBuilder) + cmd, err := BuildBundleImage(item.directory, item.imageTag, item.imageBuilder) if item.errorMsg == "" { - require.Equal(t, item.commandStr, cmd.String()) + require.Contains(t, cmd.String(), item.commandStr) } else { require.Equal(t, item.errorMsg, err.Error()) } diff --git a/cmd/operator-cli/bundle/generate.go b/pkg/lib/bundle/generate.go similarity index 63% rename from cmd/operator-cli/bundle/generate.go rename to pkg/lib/bundle/generate.go index 796cda9a0..39c8b752a 100644 --- a/cmd/operator-cli/bundle/generate.go +++ b/pkg/lib/bundle/generate.go @@ -8,7 +8,6 @@ import ( "strings" log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" "gopkg.in/yaml.v2" ) @@ -34,47 +33,26 @@ type AnnotationType struct { MediaType string `yaml:"operators.operatorframework.io.bundle.mediatype"` } -// newBundleBuildCmd returns a command that will build operator bundle image. -func newBundleGenerateCmd() *cobra.Command { - bundleGenerateCmd := &cobra.Command{ - Use: "generate", - Short: "Generate operator bundle metadata and Dockerfile", - Long: `The operator-cli buindle generate command will generate operator - bundle metadata if needed and a Dockerfile to build Operator bundle image. - - $ operator-cli bundle generate -d /test/0.0.1/ - `, - RunE: generateFunc, - } - - bundleGenerateCmd.Flags().StringVarP(&dirBuildArgs, "directory", "d", "", "The directory where bundle manifests are located.") - if err := bundleGenerateCmd.MarkFlagRequired("directory"); err != nil { - log.Fatalf("Failed to mark `directory` flag for `generate` subcommand as required") - } - - return bundleGenerateCmd -} - -func generateFunc(cmd *cobra.Command, args []string) error { +func GenerateFunc(directory string) error { var mediaType string // Determine mediaType - mediaType, err := getMediaType(dirBuildArgs) + mediaType, err := GetMediaType(directory) if err != nil { return err } // Parent directory - parentDir := path.Dir(path.Clean(dirBuildArgs)) + parentDir := path.Dir(path.Clean(directory)) log.Info("Building annotations.yaml file") // Generate annotations.yaml - content, err := generateAnnotationsFunc(manifestsMetadata, mediaType) + content, err := GenerateAnnotations(manifestsMetadata, mediaType) if err != nil { return err } - err = writeFile(annotationsFile, parentDir, content) + err = WriteFile(annotationsFile, parentDir, content) if err != nil { return err } @@ -82,8 +60,8 @@ func generateFunc(cmd *cobra.Command, args []string) error { log.Info("Building Dockerfile") // Generate Dockerfile - content = generateDockerfileFunc(manifestsMetadata, mediaType, dirBuildArgs) - err = writeFile(dockerFile, parentDir, content) + content = GenerateDockerfile(manifestsMetadata, mediaType, directory) + err = WriteFile(dockerFile, parentDir, content) if err != nil { return err } @@ -91,11 +69,11 @@ func generateFunc(cmd *cobra.Command, args []string) error { return nil } -func getMediaType(dirBuildArgs string) (string, error) { +func GetMediaType(directory string) (string, error) { var files []string // Read all file names in directory - items, _ := ioutil.ReadDir(dirBuildArgs) + items, _ := ioutil.ReadDir(directory) for _, item := range items { if item.IsDir() { continue @@ -105,7 +83,7 @@ func getMediaType(dirBuildArgs string) (string, error) { } if len(files) == 0 { - return "", fmt.Errorf("The directory %s contains no files", dirBuildArgs) + return "", fmt.Errorf("The directory %s contains no files", directory) } // Validate the file names to determine media type @@ -122,7 +100,7 @@ func getMediaType(dirBuildArgs string) (string, error) { return plainType, nil } -func generateAnnotationsFunc(resourcesType, mediaType string) ([]byte, error) { +func GenerateAnnotations(resourcesType, mediaType string) ([]byte, error) { annotations := &AnnotationMetadata{ Annotations: AnnotationType{ Resources: resourcesType, @@ -138,7 +116,7 @@ func generateAnnotationsFunc(resourcesType, mediaType string) ([]byte, error) { return afile, nil } -func generateDockerfileFunc(resourcesType, mediaType, directory string) []byte { +func GenerateDockerfile(resourcesType, mediaType, directory string) []byte { var fileContent string metadataDir := path.Dir(path.Clean(directory)) @@ -159,7 +137,7 @@ func generateDockerfileFunc(resourcesType, mediaType, directory string) []byte { // Write `fileName` file with `content` into a `directory` // Note: Will overwrite the existing `fileName` file if it exists -func writeFile(fileName, directory string, content []byte) error { +func WriteFile(fileName, directory string, content []byte) error { err := ioutil.WriteFile(filepath.Join(directory, fileName), content, defaultPermission) if err != nil { return err diff --git a/cmd/operator-cli/bundle/generate_test.go b/pkg/lib/bundle/generate_test.go similarity index 77% rename from cmd/operator-cli/bundle/generate_test.go rename to pkg/lib/bundle/generate_test.go index a519bf4ee..e559f0abd 100644 --- a/cmd/operator-cli/bundle/generate_test.go +++ b/pkg/lib/bundle/generate_test.go @@ -2,6 +2,7 @@ package bundle import ( "fmt" + "path/filepath" "testing" "github.com/stretchr/testify/require" @@ -12,7 +13,7 @@ func TestGetMediaType(t *testing.T) { setup("") defer cleanup() - testDir := operatorDir + manifestsMetadata + testDir := filepath.Join(getTestDir(), manifestsDir) tests := []struct { directory string mediaType string @@ -42,7 +43,7 @@ func TestGetMediaType(t *testing.T) { for _, item := range tests { createFiles(testDir, item.mediaType) - manifestType, err := getMediaType(item.directory) + manifestType, err := GetMediaType(item.directory) if item.errorMsg == "" { require.Equal(t, item.mediaType, manifestType) } else { @@ -62,24 +63,24 @@ func TestGenerateAnnotationsFunc(t *testing.T) { } // Create result annotations struct resultAnnotations := AnnotationMetadata{} - data, err := generateAnnotationsFunc("test1", "test2") + data, err := GenerateAnnotations("test1", "test2") require.NoError(t, err) err = yaml.Unmarshal(data, &resultAnnotations) require.NoError(t, err) require.Equal(t, testAnnotations.Annotations.Resources, resultAnnotations.Annotations.Resources) - require.Equal(t, testAnnotations.Annotations.MediaType, resultAnnotations.Annotations.Resources) + require.Equal(t, testAnnotations.Annotations.MediaType, resultAnnotations.Annotations.MediaType) } func TestGenerateDockerfileFunc(t *testing.T) { - testDir := operatorDir + manifestsMetadata + testDir := filepath.Join(operatorDir, manifestsDir) output := "FROM scratch\n\n" + "LABEL operators.operatorframework.io.bundle.resources=test1\n" + "LABEL operators.operatorframework.io.bundle.mediatype=test2\n\n" + - "ADD test-operator/0.0.1 /manifests\n" + - "ADD test-operator/0.0.1/annotations.yaml /metadata/annotations.yaml\n" + "ADD /test-operator/0.0.1 /manifests\n" + + "ADD /test-operator/annotations.yaml /metadata/annotations.yaml\n" - content := generateDockerfileFunc("test1", "test2", testDir) + content := GenerateDockerfile("test1", "test2", testDir) require.Equal(t, output, string(content)) } diff --git a/cmd/operator-cli/bundle/utils_test.go b/pkg/lib/bundle/utils_test.go similarity index 79% rename from cmd/operator-cli/bundle/utils_test.go rename to pkg/lib/bundle/utils_test.go index 37f1cb3c3..ae0cb89a7 100644 --- a/cmd/operator-cli/bundle/utils_test.go +++ b/pkg/lib/bundle/utils_test.go @@ -16,16 +16,23 @@ const ( func setup(input string) { // Create test directory - testDir := operatorDir + manifestsDir + testDir := filepath.Join(getTestDir(), manifestsDir) createDir(testDir) // Create test files in test directory createFiles(testDir, input) } +func getTestDir() string { + // Create test directory + dir, _ := os.Getwd() + testDir := filepath.Join(dir, operatorDir) + return testDir +} + func cleanup() { // Remove test directory - os.RemoveAll(operatorDir) + os.RemoveAll(getTestDir()) } func createDir(dir string) { @@ -56,7 +63,7 @@ func clearDir(dir string) { if item.IsDir() { continue } else { - os.Remove(item.Name()) + os.Remove(filepath.Join(dir, item.Name())) } } } From 505d8ea6484cb75cd5fa04dfc6843692bede7e93 Mon Sep 17 00:00:00 2001 From: Vu Dinh Date: Tue, 22 Oct 2019 03:21:06 -0400 Subject: [PATCH 3/7] Fix bundle library to add package and channels info to annotations 1. Package name, channels, default channel info is now added to annotations.yaml 2. `bundle generate` will retain overwritten ability while `bundle build` will only overwrite Dockerfile. 3. Validate annotations.yaml if existed. 4. `--overwrite/o` flag is available to overwrite annotations.yaml during build command. Signed-off-by: Vu Dinh --- pkg/lib/bundle/build.go | 6 +- pkg/lib/bundle/generate.go | 189 +++++++++++++++++++++++++++++-------- 2 files changed, 155 insertions(+), 40 deletions(-) diff --git a/pkg/lib/bundle/build.go b/pkg/lib/bundle/build.go index 38a49b4d8..756cc52d6 100644 --- a/pkg/lib/bundle/build.go +++ b/pkg/lib/bundle/build.go @@ -40,16 +40,16 @@ func ExecuteCommand(cmd *exec.Cmd) error { return nil } -func BuildFunc(directory, imageTag, imageBuilder string) error { +func BuildFunc(directory, imageTag, imageBuilder, packageName, channels, channelDefault string, overwrite bool) error { // Generate annotations.yaml and Dockerfile - err := GenerateFunc(directory) + err := GenerateFunc(directory, packageName, channels, channelDefault, overwrite) if err != nil { return err } // Build bundle image log.Info("Building bundle image") - buildCmd, err := BuildBundleImage(path.Dir(path.Clean(directory)), imageBuilder, imageTag) + buildCmd, err := BuildBundleImage(path.Clean(directory), imageBuilder, imageTag) if err != nil { return err } diff --git a/pkg/lib/bundle/generate.go b/pkg/lib/bundle/generate.go index 39c8b752a..ed42f135c 100644 --- a/pkg/lib/bundle/generate.go +++ b/pkg/lib/bundle/generate.go @@ -3,7 +3,7 @@ package bundle import ( "fmt" "io/ioutil" - "path" + "os" "path/filepath" "strings" @@ -13,27 +13,30 @@ import ( ) const ( - defaultPermission = 0644 - registryV1Type = "registry+v1" - plainType = "plain" - helmType = "helm" - manifestsMetadata = "manifests+metadata" - annotationsFile = "annotations.yaml" - dockerFile = "Dockerfile" - resourcesLabel = "operators.operatorframework.io.bundle.resources" - mediatypeLabel = "operators.operatorframework.io.bundle.mediatype" + defaultPermission = 0644 + registryV1Type = "registry+v1" + plainType = "plain" + helmType = "helm" + annotationsFile = "annotations.yaml" + dockerFile = "Dockerfile" + manifestsDir = "manifests/" + metadataDir = "metadata/" + manifestsLabel = "operators.operatorframework.io.bundle.manifests.v1" + metadataLabel = "operators.operatorframework.io.bundle.metadata.v1" + mediatypeLabel = "operators.operatorframework.io.bundle.mediatype.v1" + packageLabel = "operators.operatorframework.io.bundle.package.v1" + channelsLabel = "operators.operatorframework.io.bundle.channels.v1" + channelDefaultLabel = "operators.operatorframework.io.bundle.channel.default.v1" ) type AnnotationMetadata struct { - Annotations AnnotationType `yaml:"annotations"` + Annotations map[string]string `yaml:"annotations"` } -type AnnotationType struct { - Resources string `yaml:"operators.operatorframework.io.bundle.resources"` - MediaType string `yaml:"operators.operatorframework.io.bundle.mediatype"` -} - -func GenerateFunc(directory string) error { +// GenerateFunc builds annotations.yaml with mediatype, manifests & +// metadata directories in bundle image, package name, channels and default +// channels information and then writes the file to `/metadata` directory. +func GenerateFunc(directory, packageName, channels, channelDefault string, overwrite bool) error { var mediaType string // Determine mediaType @@ -42,26 +45,38 @@ func GenerateFunc(directory string) error { return err } - // Parent directory - parentDir := path.Dir(path.Clean(directory)) - - log.Info("Building annotations.yaml file") + log.Info("Building annotations.yaml") // Generate annotations.yaml - content, err := GenerateAnnotations(manifestsMetadata, mediaType) + content, err := GenerateAnnotations(mediaType, manifestsDir, metadataDir, packageName, channels, channelDefault) if err != nil { return err } - err = WriteFile(annotationsFile, parentDir, content) - if err != nil { + + file, err := ioutil.ReadFile(filepath.Join(directory, metadataDir, annotationsFile)) + if os.IsNotExist(err) || overwrite { + err = WriteFile(annotationsFile, filepath.Join(directory, metadataDir), content) + if err != nil { + return err + } + } else if err != nil { return err + } else { + log.Info("An annotations.yaml already exists in directory") + if err = ValidateAnnotations(file, content); err != nil { + return err + } } log.Info("Building Dockerfile") // Generate Dockerfile - content = GenerateDockerfile(manifestsMetadata, mediaType, directory) - err = WriteFile(dockerFile, parentDir, content) + content, err = GenerateDockerfile(directory, mediaType, manifestsDir, metadataDir, packageName, channels, channelDefault) + if err != nil { + return err + } + + err = WriteFile(dockerFile, directory, content) if err != nil { return err } @@ -69,6 +84,9 @@ func GenerateFunc(directory string) error { return nil } +// GenerateFunc determines mediatype from files (yaml) in given directory +// Currently able to detect helm chart, registry+v1 (CSV) and plain k8s resources +// such as CRD. func GetMediaType(directory string) (string, error) { var files []string @@ -100,14 +118,97 @@ func GetMediaType(directory string) (string, error) { return plainType, nil } -func GenerateAnnotations(resourcesType, mediaType string) ([]byte, error) { +// ValidateAnnotations validates existing annotations.yaml against generated +// annotations.yaml to ensure existing annotations.yaml contains expected values. +func ValidateAnnotations(existing, expected []byte) error { + var fileAnnotations AnnotationMetadata + var expectedAnnotations AnnotationMetadata + + log.Info("Validating existing annotations.yaml") + + err := yaml.Unmarshal(existing, &fileAnnotations) + if err != nil { + log.Errorf("Unable to parse existing annotations.yaml") + return err + } + + err = yaml.Unmarshal(expected, &expectedAnnotations) + if err != nil { + log.Errorf("Unable to parse expected annotations.yaml") + return err + } + + if len(fileAnnotations.Annotations) != len(expectedAnnotations.Annotations) { + return fmt.Errorf("Unmatched number of fields. Expected (%d) vs existing (%d)", + len(expectedAnnotations.Annotations), len(fileAnnotations.Annotations)) + } + + for label, item := range expectedAnnotations.Annotations { + value, ok := fileAnnotations.Annotations[label] + if ok == false { + return fmt.Errorf("Missing field: %s", label) + } + + if item != value { + return fmt.Errorf(`Expect field "%s" to have value "%s" instead of "%s"`, + label, item, value) + } + } + + return nil +} + +// ValidateAnnotations validates provided default channel to ensure it exists in +// provided channel list. +func ValidateChannelDefault(channels, channelDefault string) (string, error) { + var chanDefault string + var chanErr error + channelList := strings.Split(channels, ",") + + if channelDefault != "" { + for _, channel := range channelList { + if channel == channelDefault { + chanDefault = channelDefault + break + } + } + if chanDefault == "" { + chanDefault = channelList[0] + chanErr = fmt.Errorf(`The channel list "%s" doesn't contain channelDefault "%s"`, channels, channelDefault) + } + } else { + chanDefault = channelList[0] + } + + if chanDefault != "" { + return chanDefault, chanErr + } else { + return chanDefault, fmt.Errorf("Invalid channels is provied: %s", channels) + } +} + +// GenerateAnnotations builds annotations.yaml with mediatype, manifests & +// metadata directories in bundle image, package name, channels and default +// channels information. +func GenerateAnnotations(mediaType, manifests, metadata, packageName, channels, channelDefault string) ([]byte, error) { annotations := &AnnotationMetadata{ - Annotations: AnnotationType{ - Resources: resourcesType, - MediaType: mediaType, + Annotations: map[string]string{ + mediatypeLabel: mediaType, + manifestsLabel: manifests, + metadataLabel: metadata, + packageLabel: packageName, + channelsLabel: channels, + channelDefaultLabel: channelDefault, }, } + chanDefault, err := ValidateChannelDefault(channels, channelDefault) + if err != nil { + return nil, err + } + + annotations.Annotations[channelDefaultLabel] = chanDefault + afile, err := yaml.Marshal(annotations) if err != nil { return nil, err @@ -116,28 +217,42 @@ func GenerateAnnotations(resourcesType, mediaType string) ([]byte, error) { return afile, nil } -func GenerateDockerfile(resourcesType, mediaType, directory string) []byte { +// GenerateDockerfile builds Dockerfile with mediatype, manifests & +// metadata directories in bundle image, package name, channels and default +// channels information in LABEL section. +func GenerateDockerfile(directory, mediaType, manifests, metadata, packageName, channels, channelDefault string) ([]byte, error) { var fileContent string - metadataDir := path.Dir(path.Clean(directory)) + chanDefault, err := ValidateChannelDefault(channels, channelDefault) + if err != nil { + return nil, err + } // FROM fileContent += "FROM scratch\n\n" // LABEL - fileContent += fmt.Sprintf("LABEL %s=%s\n", resourcesLabel, resourcesType) - fileContent += fmt.Sprintf("LABEL %s=%s\n\n", mediatypeLabel, mediaType) + fileContent += fmt.Sprintf("LABEL %s=%s\n", mediatypeLabel, mediaType) + fileContent += fmt.Sprintf("LABEL %s=%s\n", manifestsLabel, manifests) + fileContent += fmt.Sprintf("LABEL %s=%s\n", metadataLabel, metadata) + fileContent += fmt.Sprintf("LABEL %s=%s\n", packageLabel, packageName) + fileContent += fmt.Sprintf("LABEL %s=%s\n", channelsLabel, channels) + fileContent += fmt.Sprintf("LABEL %s=%s\n\n", channelDefaultLabel, chanDefault) // CONTENT - fileContent += fmt.Sprintf("ADD %s %s\n", directory, "/manifests") - fileContent += fmt.Sprintf("ADD %s/%s %s%s\n", metadataDir, annotationsFile, "/metadata/", annotationsFile) + fileContent += fmt.Sprintf("ADD %s %s\n", filepath.Join(directory, "*.yaml"), "/manifests") + fileContent += fmt.Sprintf("ADD %s %s%s\n", filepath.Join(directory, metadata, annotationsFile), "/metadata/", annotationsFile) - return []byte(fileContent) + return []byte(fileContent), nil } // Write `fileName` file with `content` into a `directory` // Note: Will overwrite the existing `fileName` file if it exists func WriteFile(fileName, directory string, content []byte) error { + if _, err := os.Stat(directory); os.IsNotExist(err) { + os.Mkdir(directory, os.ModePerm) + } + err := ioutil.WriteFile(filepath.Join(directory, fileName), content, defaultPermission) if err != nil { return err From ff44b595dd6dc86606159f28f6cf99a204794e33 Mon Sep 17 00:00:00 2001 From: Vu Dinh Date: Tue, 22 Oct 2019 03:29:45 -0400 Subject: [PATCH 4/7] Fix bundle library unit tests to cover new code Signed-off-by: Vu Dinh --- pkg/lib/bundle/build_test.go | 8 +- pkg/lib/bundle/generate_test.go | 164 +++++++++++++++++++++++++++++--- pkg/lib/bundle/utils_test.go | 22 +++-- 3 files changed, 169 insertions(+), 25 deletions(-) diff --git a/pkg/lib/bundle/build_test.go b/pkg/lib/bundle/build_test.go index bf28ef0de..e0f8ad004 100644 --- a/pkg/lib/bundle/build_test.go +++ b/pkg/lib/bundle/build_test.go @@ -19,28 +19,28 @@ func TestBuildBundleImage(t *testing.T) { errorMsg string }{ { - operatorDir, + testOperatorDir, "test", "docker", "docker build -f /test-operator/Dockerfile -t test .", "", }, { - operatorDir, + testOperatorDir, "test", "buildah", "buildah bud --format=docker -f /test-operator/Dockerfile -t test .", "", }, { - operatorDir, + testOperatorDir, "test", "podman", "podman build -f /test-operator/Dockerfile -t test .", "", }, { - operatorDir, + testOperatorDir, "test", "hello", "", diff --git a/pkg/lib/bundle/generate_test.go b/pkg/lib/bundle/generate_test.go index e559f0abd..d9482f362 100644 --- a/pkg/lib/bundle/generate_test.go +++ b/pkg/lib/bundle/generate_test.go @@ -13,7 +13,7 @@ func TestGetMediaType(t *testing.T) { setup("") defer cleanup() - testDir := filepath.Join(getTestDir(), manifestsDir) + testDir := getTestDir() tests := []struct { directory string mediaType string @@ -53,34 +53,170 @@ func TestGetMediaType(t *testing.T) { } } +func TestValidateChannelDefault(t *testing.T) { + tests := []struct { + channels string + channelDefault string + result string + errorMsg string + }{ + { + "test5,test6", + "", + "test5", + "", + }, + { + "test5,test6", + "test7", + "test5", + `The channel list "test5,test6" doesn't contain channelDefault "test7"`, + }, + { + ",", + "", + "", + `Invalid channels is provied: ,`, + }, + } + + for _, item := range tests { + output, err := ValidateChannelDefault(item.channels, item.channelDefault) + if item.errorMsg == "" { + require.Equal(t, item.result, output) + } else { + require.Equal(t, item.errorMsg, err.Error()) + } + } +} + +func TestValidateAnnotations(t *testing.T) { + tests := []struct { + existing []byte + expected []byte + err error + }{ + { + buildTestAnnotations("annotations", + map[string]string{ + "test1": "stable", + "test2": "stable,beta", + }), + buildTestAnnotations("annotations", + map[string]string{ + "test1": "stable", + "test2": "stable,beta", + }), + nil, + }, + { + buildTestAnnotations("annotations", + map[string]string{ + "test1": "stable", + "test2": "stable,beta", + "test3": "beta", + }), + buildTestAnnotations("annotations", + map[string]string{ + "test1": "stable", + "test2": "stable,beta", + }), + fmt.Errorf("Unmatched number of fields. Expected (2) vs existing (3)"), + }, + { + buildTestAnnotations("annotations", + map[string]string{ + "test1": "stable", + "test2": "stable", + }), + buildTestAnnotations("annotations", + map[string]string{ + "test1": "stable", + "test2": "stable,beta", + }), + fmt.Errorf(`Expect field "test2" to have value "stable,beta" instead of "stable"`), + }, + { + buildTestAnnotations("annotations", + map[string]string{ + "test1": "stable", + "test3": "stable", + }), + buildTestAnnotations("annotations", + map[string]string{ + "test1": "stable", + "test2": "stable,beta", + }), + fmt.Errorf("Missing field: test2"), + }, + { + []byte("\t"), + buildTestAnnotations("annotations", + map[string]string{ + "test1": "stable", + "test2": "stable,beta", + }), + fmt.Errorf("yaml: found character that cannot start any token"), + }, + { + buildTestAnnotations("annotations", + map[string]string{ + "test1": "stable", + "test2": "stable,beta", + }), + []byte("\t"), + fmt.Errorf("yaml: found character that cannot start any token"), + }, + } + + for _, item := range tests { + err := ValidateAnnotations(item.existing, item.expected) + if item.err != nil { + require.Equal(t, item.err.Error(), err.Error()) + } else { + require.Nil(t, err) + } + } +} + func TestGenerateAnnotationsFunc(t *testing.T) { // Create test annotations struct testAnnotations := &AnnotationMetadata{ - Annotations: AnnotationType{ - Resources: "test1", - MediaType: "test2", + Annotations: map[string]string{ + mediatypeLabel: "test1", + manifestsLabel: "test2", + metadataLabel: "test3", + packageLabel: "test4", + channelsLabel: "test5", + channelDefaultLabel: "test5", }, } // Create result annotations struct resultAnnotations := AnnotationMetadata{} - data, err := GenerateAnnotations("test1", "test2") + data, err := GenerateAnnotations("test1", "test2", "test3", "test4", "test5", "test5") require.NoError(t, err) err = yaml.Unmarshal(data, &resultAnnotations) require.NoError(t, err) - require.Equal(t, testAnnotations.Annotations.Resources, resultAnnotations.Annotations.Resources) - require.Equal(t, testAnnotations.Annotations.MediaType, resultAnnotations.Annotations.MediaType) + for key, value := range testAnnotations.Annotations { + require.Equal(t, value, resultAnnotations.Annotations[key]) + } } func TestGenerateDockerfileFunc(t *testing.T) { - testDir := filepath.Join(operatorDir, manifestsDir) - output := "FROM scratch\n\n" + - "LABEL operators.operatorframework.io.bundle.resources=test1\n" + - "LABEL operators.operatorframework.io.bundle.mediatype=test2\n\n" + - "ADD /test-operator/0.0.1 /manifests\n" + - "ADD /test-operator/annotations.yaml /metadata/annotations.yaml\n" + output := fmt.Sprintf("FROM scratch\n\n"+ + "LABEL operators.operatorframework.io.bundle.mediatype.v1=test1\n"+ + "LABEL operators.operatorframework.io.bundle.manifests.v1=test2\n"+ + "LABEL operators.operatorframework.io.bundle.metadata.v1=%s\n"+ + "LABEL operators.operatorframework.io.bundle.package.v1=test4\n"+ + "LABEL operators.operatorframework.io.bundle.channels.v1=test5\n"+ + "LABEL operators.operatorframework.io.bundle.channel.default.v1=test5\n\n"+ + "ADD %s/*.yaml /manifests\n"+ + "ADD %s/annotations.yaml /metadata/annotations.yaml\n", metadataDir, getTestDir(), + filepath.Join(getTestDir(), metadataDir)) - content := GenerateDockerfile("test1", "test2", testDir) + content, err := GenerateDockerfile(getTestDir(), "test1", "test2", metadataDir, "test4", "test5", "") + require.NoError(t, err) require.Equal(t, output, string(content)) } diff --git a/pkg/lib/bundle/utils_test.go b/pkg/lib/bundle/utils_test.go index ae0cb89a7..d74b20dee 100644 --- a/pkg/lib/bundle/utils_test.go +++ b/pkg/lib/bundle/utils_test.go @@ -4,19 +4,20 @@ import ( "io/ioutil" "os" "path/filepath" + + "gopkg.in/yaml.v2" ) const ( - operatorDir = "/test-operator" - manifestsDir = "/0.0.1" - helmFile = "Chart.yaml" - csvFile = "test.clusterserviceversion.yaml" - crdFile = "test.crd.yaml" + testOperatorDir = "/test-operator" + helmFile = "Chart.yaml" + csvFile = "test.clusterserviceversion.yaml" + crdFile = "test.crd.yaml" ) func setup(input string) { // Create test directory - testDir := filepath.Join(getTestDir(), manifestsDir) + testDir := getTestDir() createDir(testDir) // Create test files in test directory @@ -26,7 +27,7 @@ func setup(input string) { func getTestDir() string { // Create test directory dir, _ := os.Getwd() - testDir := filepath.Join(dir, operatorDir) + testDir := filepath.Join(dir, testOperatorDir) return testDir } @@ -56,6 +57,13 @@ func createFiles(dir, input string) { } } +func buildTestAnnotations(key string, items map[string]string) []byte { + temp := make(map[string]interface{}) + temp[key] = items + output, _ := yaml.Marshal(temp) + return output +} + func clearDir(dir string) { items, _ := ioutil.ReadDir(dir) From 9762fd5183567e489c9d3aec717beae3de650374 Mon Sep 17 00:00:00 2001 From: Vu Dinh Date: Tue, 22 Oct 2019 03:30:20 -0400 Subject: [PATCH 5/7] Fix bundle library doc to reflect new changes/features Signed-off-by: Vu Dinh --- docs/design/operator-bundle.md | 61 ++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/docs/design/operator-bundle.md b/docs/design/operator-bundle.md index 4566c0318..5ac415c18 100644 --- a/docs/design/operator-bundle.md +++ b/docs/design/operator-bundle.md @@ -1,4 +1,4 @@ -# Operator Bundle +overwrite# Operator Bundle An `Operator Bundle` is a container image that stores Kubernetes manifests and metadata associated with an operator. A bundle is meant to present a *specific* version of an operator. @@ -87,7 +87,7 @@ In order to use Operator SDK CLI, follow the operator-SDK installation instructi 1. Install the [Operator SDK CLI](https://github.com/operator-framework/operator-sdk/blob/master/doc/user/install-operator-sdk.md) -Now, a binary named `operator-cli` is available in OLM's directory to use. +Now, a binary named `operator-sdk` is available in OLM's directory to use. ```bash $ ./operator-sdk An SDK for building operators with ease @@ -106,11 +106,29 @@ Use "operator-sdk [command] --help" for more information about a command. ``` ### Generate Bundle Annotations and DockerFile +*Notes:* +* If there are `annotations.yaml` and `Dockerfile` existing in the directory, they will be overwritten. -Using `operator-sdk` CLI, bundle annotations can be generated from provided operator manifests. The command for `generate` task is: +Using `operator-sdk` CLI, bundle annotations can be generated from provided operator manifests. The overall `bundle generate` command usage is: ```bash -$ ./operator-sdk bundle generate --directory /test/ +Usage: + operator-sdk bundle generate [flags] + +Flags: + -c, --channels string The list of channels that bundle image belongs to + -e, --default string The default channel for the bundle image + -d, --directory string The directory where bundle manifests are located. + -h, --help help for generate + -p, --package string The name of the package that bundle image belongs to ``` + +The `--directory/-d`, `--channels/-c`, `--package/-p` are required flags while `--default/-e` is optional. + +The command for `generate` task is: +```bash +$ ./operator-sdk bundle generate --directory /test --package test-operator --channels stable,beta --default stable +``` + The `--directory` or `-d` specifies the directory where the operator manifests are located. The `Dockerfile` is generated in the same directory where the YAML manifests are located while the `annotations.yaml` file is located in a folder named `metadata`. For example: ```bash $ tree test @@ -124,22 +142,43 @@ test └── Dockerfile ``` -*Notes:* -* If there are `annotations.yaml` and `Dockerfile` existing in the directory, they will be overwritten. +The `--package` or `-p` is the name of package fo the operator such as `etcd` which which map `channels` to a particular application definition. `channels` allow package authors to write different upgrade paths for different users (e.g. `beta` vs. `stable`). The `channels` list is provided via `--channels` or `-c` flag. Multiple `channels` are separated by a comma (`,`). The default channel is provided optionally via `--default` or `-e` flag. If the default channel is not provided, the first channel in channel list is selected as default. + +All information in `annotations.yaml` is also existed in `LABEL` section of `Dockerfile`. ### Build Bundle Image -Operator bundle image can be built from provided operator manifests using `build` command: +Operator bundle image can be built from provided operator manifests using `build` command (see *Notes* below). The overall `bundle build` command usage is: ```bash -$ ./operator-sdk bundle build --directory /test/0.1.0/ --tag quay.io/coreos/test-operator.v0.1.0:latest +Usage: + operator-SDK bundle build [flags] + +Flags: + -c, --channels string The list of channels that bundle image belongs to + -e, --default string The default channel for the bundle image + -d, --directory string The directory where bundle manifests are located + -h, --help help for build + -b, --image-builder string Tool to build container images. One of: [docker, podman, buildah] (default "docker") + -0, --overwrite To overwrite annotations.yaml if existing + -p, --package string The name of the package that bundle image belongs to + -t, --tag string The name of the bundle image will be built ``` + +The command for `build` task is: +```bash +$ ./operator-sdk bundle build --directory /test --tag quay.io/coreos/test-operator.v0.1.0:latest --package test-operator --channels stable,beta --default stable +``` + The `--directory` or `-d` specifies the directory where the operator manifests are located. The `--tag` or `-t` specifies the image tag that you want the operator bundle image to have. By using `build` command, the `annotations.yaml` and `Dockerfile` are automatically generated in the background. The default image builder is `Docker`. However, ` Buildah` and `Podman` are also supported. An image builder can specified via `--image-builder` or `-b` optional tag in `build` command. For example: ```bash -$ ./operator-sdk bundle build --directory /test/0.1.0/ --tag quay.io/coreos/test-operator.v0.1.0:latest --image-builder podman +$ ./operator-sdk bundle build --directory /test/0.1.0/ --tag quay.io/coreos/test-operator.v0.1.0:latest --image-builder podman --package test-operator --channels stable,beta --default stable ``` +The `--package` or `-p` is the name of package fo the operator such as `etcd` which which map `channels` to a particular application definition. `channels` allow package authors to write different upgrade paths for different users (e.g. `beta` vs. `stable`). The `channels` list is provided via `--channels` or `-c` flag. Multiple `channels` are separated by a comma (`,`). The default channel is provided optionally via `--default` or `-e` flag. If the default channel is not provided, the first channel in channel list is selected as default. + *Notes:* -* If there are `annotations.yaml` and `Dockerfile` existing in the directory, they will be overwritten. -* The directory where the operator manifests are located must must be inside the context of the build which in this case is inside the directory where you run the command. +* If there is `Dockerfile` existing in the directory, it will be overwritten. +* If there is an existing `annotations.yaml` in `/metadata` directory, the cli will attempt to validate it and returns any found errors. If the ``annotations.yaml`` is valid, it will be used as a part of build process. The optional boolean `--overwrite/-o` flag can be enabled (false by default) to allow cli to overwrite the `annotations.yaml` if existed. +* The directory where the operator manifests are located must be inside the context of the build which in this case is inside the directory where you run the command. From ee575b1f40887a67efe6e038be66170d60f1654e Mon Sep 17 00:00:00 2001 From: Vu Dinh Date: Tue, 22 Oct 2019 03:30:55 -0400 Subject: [PATCH 6/7] Clean up minor issues with command-line code and documentation 1. Clarify a few helper texts in cli code 2. Improve librar document to specific input information 3. Move `bundle` comands under hidden `alpha` command Signed-off-by: Vu Dinh --- cmd/opm/{ => alpha}/bundle/build.go | 36 ++++++++++++++---- cmd/opm/{ => alpha}/bundle/cmd.go | 7 ++-- cmd/opm/alpha/bundle/generate.go | 51 +++++++++++++++++++++++++ cmd/opm/alpha/cmd.go | 17 +++++++++ cmd/opm/bundle/generate.go | 59 ----------------------------- cmd/opm/main.go | 19 ++++++++-- docs/design/operator-bundle.md | 43 ++++++++++++++------- pkg/lib/bundle/build.go | 13 +++++++ pkg/lib/bundle/generate.go | 6 +++ 9 files changed, 163 insertions(+), 88 deletions(-) rename cmd/opm/{ => alpha}/bundle/build.go (54%) rename cmd/opm/{ => alpha}/bundle/cmd.go (59%) create mode 100644 cmd/opm/alpha/bundle/generate.go create mode 100644 cmd/opm/alpha/cmd.go delete mode 100644 cmd/opm/bundle/generate.go diff --git a/cmd/opm/bundle/build.go b/cmd/opm/alpha/bundle/build.go similarity index 54% rename from cmd/opm/bundle/build.go rename to cmd/opm/alpha/bundle/build.go index 8722dbe5d..9fe6e9888 100644 --- a/cmd/opm/bundle/build.go +++ b/cmd/opm/alpha/bundle/build.go @@ -7,9 +7,13 @@ import ( ) var ( - dirBuildArgs string - tagBuildArgs string - imageBuilderArgs string + dirBuildArgs string + tagBuildArgs string + imageBuilderArgs string + packageNameArgs string + channelsArgs string + channelDefaultArgs string + overwriteArgs bool ) // newBundleBuildCmd returns a command that will build operator bundle image. @@ -17,7 +21,7 @@ func newBundleBuildCmd() *cobra.Command { bundleBuildCmd := &cobra.Command{ Use: "build", Short: "Build operator bundle image", - Long: `The opm bundle build command will generate operator + Long: `The "opm alpha bundle build" command will generate operator bundle metadata if needed and build bundle image with operator manifest and metadata. @@ -29,30 +33,46 @@ func newBundleBuildCmd() *cobra.Command { After the build process is completed, a container image would be built locally in docker and available to push to a container registry. - $ opm bundle build -dir /test/0.0.1/ -t quay.io/example/operator:v0.0.1 + $ opm alpha bundle build --directory /test/ --tag quay.io/example/operator:v0.1.0 \ + --package test-operator --channels stable,beta --default stable --overwrite Note: Bundle image is not runnable. `, RunE: buildFunc, } - bundleBuildCmd.Flags().StringVarP(&dirBuildArgs, "directory", "d", "", "The directory where bundle manifests are located.") + bundleBuildCmd.Flags().StringVarP(&dirBuildArgs, "directory", "d", "", "The directory where bundle manifests and metadata are located") if err := bundleBuildCmd.MarkFlagRequired("directory"); err != nil { log.Fatalf("Failed to mark `directory` flag for `build` subcommand as required") } - bundleBuildCmd.Flags().StringVarP(&tagBuildArgs, "tag", "t", "", "The name of the bundle image will be built.") + bundleBuildCmd.Flags().StringVarP(&tagBuildArgs, "tag", "t", "", "The image tag applied to the bundle image") if err := bundleBuildCmd.MarkFlagRequired("tag"); err != nil { log.Fatalf("Failed to mark `tag` flag for `build` subcommand as required") } + bundleBuildCmd.Flags().StringVarP(&packageNameArgs, "package", "p", "", "The name of the package that bundle image belongs to") + if err := bundleBuildCmd.MarkFlagRequired("package"); err != nil { + log.Fatalf("Failed to mark `package` flag for `build` subcommand as required") + } + + bundleBuildCmd.Flags().StringVarP(&channelsArgs, "channels", "c", "", "The list of channels that bundle image belongs to") + if err := bundleBuildCmd.MarkFlagRequired("channels"); err != nil { + log.Fatalf("Failed to mark `channels` flag for `build` subcommand as required") + } + bundleBuildCmd.Flags().StringVarP(&imageBuilderArgs, "image-builder", "b", "docker", "Tool to build container images. One of: [docker, podman, buildah]") + bundleBuildCmd.Flags().StringVarP(&channelDefaultArgs, "default", "e", "", "The default channel for the bundle image") + + bundleBuildCmd.Flags().BoolVarP(&overwriteArgs, "overwrite", "o", false, "To overwrite annotations.yaml locally if existed. By default, overwrite is set to `false`.") + return bundleBuildCmd } func buildFunc(cmd *cobra.Command, args []string) error { - err := bundle.BuildFunc(dirBuildArgs, tagBuildArgs, imageBuilderArgs) + err := bundle.BuildFunc(dirBuildArgs, tagBuildArgs, imageBuilderArgs, + packageNameArgs, channelsArgs, channelDefaultArgs, overwriteArgs) if err != nil { return err } diff --git a/cmd/opm/bundle/cmd.go b/cmd/opm/alpha/bundle/cmd.go similarity index 59% rename from cmd/opm/bundle/cmd.go rename to cmd/opm/alpha/bundle/cmd.go index 1c05771b4..6abccd54e 100644 --- a/cmd/opm/bundle/cmd.go +++ b/cmd/opm/alpha/bundle/cmd.go @@ -6,10 +6,9 @@ import ( func NewCmd() *cobra.Command { runCmd := &cobra.Command{ - Hidden: true, - Use: "bundle", - Short: "Operator bundle commands", - Long: `Generate operator bundle metadata and build bundle image.`, + Use: "bundle", + Short: "Operator bundle commands", + Long: `Generate operator bundle metadata and build bundle image.`, } runCmd.AddCommand(newBundleGenerateCmd()) diff --git a/cmd/opm/alpha/bundle/generate.go b/cmd/opm/alpha/bundle/generate.go new file mode 100644 index 000000000..1f065550c --- /dev/null +++ b/cmd/opm/alpha/bundle/generate.go @@ -0,0 +1,51 @@ +package bundle + +import ( + "github.com/operator-framework/operator-registry/pkg/lib/bundle" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// newBundleGenerateCmd returns a command that will generate operator bundle +// annotations.yaml metadata +func newBundleGenerateCmd() *cobra.Command { + bundleGenerateCmd := &cobra.Command{ + Use: "generate", + Short: "Generate operator bundle metadata and Dockerfile", + Long: `The "opm alpha bundle generate" command will generate operator + bundle metadata if needed and a Dockerfile to build Operator bundle image. + + $ opm alpha bundle generate --directory /test/ --package test-operator \ + --channels stable,beta --default stable + `, + RunE: generateFunc, + } + + bundleGenerateCmd.Flags().StringVarP(&dirBuildArgs, "directory", "d", "", "The directory where bundle manifests are located.") + if err := bundleGenerateCmd.MarkFlagRequired("directory"); err != nil { + log.Fatalf("Failed to mark `directory` flag for `generate` subcommand as required") + } + + bundleGenerateCmd.Flags().StringVarP(&packageNameArgs, "package", "p", "", "The name of the package that bundle image belongs to") + if err := bundleGenerateCmd.MarkFlagRequired("package"); err != nil { + log.Fatalf("Failed to mark `package` flag for `generate` subcommand as required") + } + + bundleGenerateCmd.Flags().StringVarP(&channelsArgs, "channels", "c", "", "The list of channels that bundle image belongs to") + if err := bundleGenerateCmd.MarkFlagRequired("channels"); err != nil { + log.Fatalf("Failed to mark `channels` flag for `generate` subcommand as required") + } + + bundleGenerateCmd.Flags().StringVarP(&channelDefaultArgs, "default", "e", "", "The default channel for the bundle image") + + return bundleGenerateCmd +} + +func generateFunc(cmd *cobra.Command, args []string) error { + err := bundle.GenerateFunc(dirBuildArgs, packageNameArgs, channelsArgs, channelDefaultArgs, true) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/opm/alpha/cmd.go b/cmd/opm/alpha/cmd.go new file mode 100644 index 000000000..b068c5e29 --- /dev/null +++ b/cmd/opm/alpha/cmd.go @@ -0,0 +1,17 @@ +package alpha + +import ( + "github.com/operator-framework/operator-registry/cmd/opm/alpha/bundle" + "github.com/spf13/cobra" +) + +func NewCmd() *cobra.Command { + runCmd := &cobra.Command{ + Hidden: true, + Use: "alpha", + Short: "Run an alpha subcommand", + } + + runCmd.AddCommand(bundle.NewCmd()) + return runCmd +} diff --git a/cmd/opm/bundle/generate.go b/cmd/opm/bundle/generate.go deleted file mode 100644 index 57b84f626..000000000 --- a/cmd/opm/bundle/generate.go +++ /dev/null @@ -1,59 +0,0 @@ -package bundle - -import ( - "github.com/operator-framework/operator-registry/pkg/lib/bundle" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -const ( - defaultPermission = 0644 - registryV1Type = "registry+v1" - plainType = "plain" - helmType = "helm" - manifestsMetadata = "manifests+metadata" - annotationsFile = "annotations.yaml" - dockerFile = "Dockerfile" - resourcesLabel = "operators.operatorframework.io.bundle.resources" - mediatypeLabel = "operators.operatorframework.io.bundle.mediatype" -) - -type AnnotationMetadata struct { - Annotations AnnotationType `yaml:"annotations"` -} - -type AnnotationType struct { - Resources string `yaml:"operators.operatorframework.io.bundle.resources"` - MediaType string `yaml:"operators.operatorframework.io.bundle.mediatype"` -} - -// newBundleGenerateCmd returns a command that will generate operator bundle -// annotations.yaml metadata -func newBundleGenerateCmd() *cobra.Command { - bundleGenerateCmd := &cobra.Command{ - Use: "generate", - Short: "Generate operator bundle metadata and Dockerfile", - Long: `The opm buindle generate command will generate operator - bundle metadata if needed and a Dockerfile to build Operator bundle image. - - $ opm bundle generate -d /test/0.0.1/ - `, - RunE: generateFunc, - } - - bundleGenerateCmd.Flags().StringVarP(&dirBuildArgs, "directory", "d", "", "The directory where bundle manifests are located.") - if err := bundleGenerateCmd.MarkFlagRequired("directory"); err != nil { - log.Fatalf("Failed to mark `directory` flag for `generate` subcommand as required") - } - - return bundleGenerateCmd -} - -func generateFunc(cmd *cobra.Command, args []string) error { - err := bundle.GenerateFunc(dirBuildArgs) - if err != nil { - return err - } - - return nil -} diff --git a/cmd/opm/main.go b/cmd/opm/main.go index e1011c105..57db2780f 100644 --- a/cmd/opm/main.go +++ b/cmd/opm/main.go @@ -1,10 +1,12 @@ package main import ( + "os" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/operator-framework/operator-registry/cmd/opm/bundle" + "github.com/operator-framework/operator-registry/cmd/opm/alpha" "github.com/operator-framework/operator-registry/cmd/opm/registry" ) @@ -13,12 +15,23 @@ func main() { Use: "opm", Short: "operator package manager", Long: "CLI to interact with operator-registry and build indexes of operator content", + PreRunE: func(cmd *cobra.Command, args []string) error { + if debug, _ := cmd.Flags().GetBool("debug"); debug { + logrus.SetLevel(logrus.DebugLevel) + } + return nil + }, } rootCmd.AddCommand(registry.NewOpmRegistryCmd()) - rootCmd.AddCommand(bundle.NewCmd()) + rootCmd.AddCommand(alpha.NewCmd()) - if err := rootCmd.Execute(); err != nil { + rootCmd.Flags().Bool("debug", false, "enable debug logging") + if err := rootCmd.Flags().MarkHidden("debug"); err != nil { logrus.Panic(err.Error()) } + + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } } diff --git a/docs/design/operator-bundle.md b/docs/design/operator-bundle.md index 5ac415c18..b81a16aa0 100644 --- a/docs/design/operator-bundle.md +++ b/docs/design/operator-bundle.md @@ -1,4 +1,4 @@ -overwrite# Operator Bundle +# Operator Bundle An `Operator Bundle` is a container image that stores Kubernetes manifests and metadata associated with an operator. A bundle is meant to present a *specific* version of an operator. @@ -18,19 +18,27 @@ An `Operator Bundle` is built as a scratch (non-runnable) container image that c ### Bundle Annotations We use the following labels to annotate the operator bundle image. -* The label `operators.operatorframework.io.bundle.resources` represents the bundle type: - * The value `manifests` implies that this bundle contains operator manifests only. - * The value `metadata` implies that this bundle has operator metadata only. - * The value `manifests+metadata` implies that this bundle contains both operator metadata and manifests. -* The label `operators.operatorframework.io.bundle.mediatype` reflects the media type or format of the operator bundle. It could be helm charts, plain Kubernetes manifests etc. +* The label `operators.operatorframework.io.bundle.mediatype.v1` reflects the media type or format of the operator bundle. It could be helm charts, plain Kubernetes manifests etc. +* The label `operators.operatorframework.io.bundle.manifests.v1 `reflects the path in the image to the directory that contains the operator manifests. +* The label `operators.operatorframework.io.bundle.metadata.v1` reflects the path in the image to the directory that contains metadata files about the bundle. +* The `manifests.v1` and `metadata.v1` labels imply the bundle type: + * The value `manifests.v1` implies that this bundle contains operator manifests. + * The value `metadata.v1` implies that this bundle has operator metadata. +* The label `operators.operatorframework.io.bundle.package.v1` reflects the package name of the bundle. +* The label `operators.operatorframework.io.bundle.channels.v1` reflects the list of channels the bundle is subscribing to when added into an operator registry +* The label `operators.operatorframework.io.bundle.channel.default.v1` reflects the default channel an operator should be subscribed to when installed from a registry The labels will also be put inside a YAML file, as shown below. *annotations.yaml* ```yaml annotations: - operators.operatorframework.io.bundle.resources: "manifests+metadata" - operators.operatorframework.io.bundle.mediatype: "registry+v1" + operators.operatorframework.io.bundle.mediatype.v1: "registry+v1" + operators.operatorframework.io.bundle.manifests.v1: "manifests/" + operators.operatorframework.io.bundle.metadata.v1: "metadata/" + operators.operatorframework.io.bundle.package.v1: "test-operator" + operators.operatorframework.io.bundle.channels.v1: "beta,stable" + operators.operatorframework.io.bundle.channel.default.v1: "stable" ``` *Notes:* @@ -57,11 +65,15 @@ FROM scratch # We are pushing an operator-registry bundle # that has both metadata and manifests. -LABEL operators.operatorframework.io.bundle.resources=manifests+metadata -LABEL operators.operatorframework.io.bundle.mediatype=registry+v1 +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=test-operator +LABEL operators.operatorframework.io.bundle.channels.v1=beta,stable +LABEL operators.operatorframework.io.bundle.channel.default.v1=stable ADD test/*.yaml /manifests -ADD test/annotations.yaml /metadata/annotations.yaml +ADD test/metadata/annotations.yaml /metadata/annotations.yaml ``` Below is the directory layout of the operator bundle inside the image: @@ -126,7 +138,8 @@ The `--directory/-d`, `--channels/-c`, `--package/-p` are required flags while ` The command for `generate` task is: ```bash -$ ./operator-sdk bundle generate --directory /test --package test-operator --channels stable,beta --default stable +$ ./operator-sdk bundle generate --directory /test --package test-operator \ +--channels stable,beta --default stable ``` The `--directory` or `-d` specifies the directory where the operator manifests are located. The `Dockerfile` is generated in the same directory where the YAML manifests are located while the `annotations.yaml` file is located in a folder named `metadata`. For example: @@ -166,14 +179,16 @@ Flags: The command for `build` task is: ```bash -$ ./operator-sdk bundle build --directory /test --tag quay.io/coreos/test-operator.v0.1.0:latest --package test-operator --channels stable,beta --default stable +$ ./operator-sdk bundle build --directory /test --tag quay.io/coreos/test-operator.v0.1.0:latest \ +--package test-operator --channels stable,beta --default stable ``` The `--directory` or `-d` specifies the directory where the operator manifests are located. The `--tag` or `-t` specifies the image tag that you want the operator bundle image to have. By using `build` command, the `annotations.yaml` and `Dockerfile` are automatically generated in the background. The default image builder is `Docker`. However, ` Buildah` and `Podman` are also supported. An image builder can specified via `--image-builder` or `-b` optional tag in `build` command. For example: ```bash -$ ./operator-sdk bundle build --directory /test/0.1.0/ --tag quay.io/coreos/test-operator.v0.1.0:latest --image-builder podman --package test-operator --channels stable,beta --default stable +$ ./operator-sdk bundle build --directory /test/0.1.0/ --tag quay.io/coreos/test-operator.v0.1.0:latest \ +--image-builder podman --package test-operator --channels stable,beta --default stable ``` The `--package` or `-p` is the name of package fo the operator such as `etcd` which which map `channels` to a particular application definition. `channels` allow package authors to write different upgrade paths for different users (e.g. `beta` vs. `stable`). The `channels` list is provided via `--channels` or `-c` flag. Multiple `channels` are separated by a comma (`,`). The default channel is provided optionally via `--default` or `-e` flag. If the default channel is not provided, the first channel in channel list is selected as default. diff --git a/pkg/lib/bundle/build.go b/pkg/lib/bundle/build.go index 756cc52d6..7c729529f 100644 --- a/pkg/lib/bundle/build.go +++ b/pkg/lib/bundle/build.go @@ -40,6 +40,19 @@ func ExecuteCommand(cmd *exec.Cmd) error { return nil } +// BuildFunc is used to build bundle container image from a list of manifests +// that exist in local directory and it also generates Dockerfile annotations.yaml +// which contains media type, package name and channels information if the file +// doesn't exist locally. +// Inputs: +// @directory: The local directory where bundle manifests and metadata are located +// @imageTag: The image tag that is applied to the bundle image +// @imageBuilder: The image builder tool that is used to build container image +// (docker, buildah or podman) +// @packageName: The name of the package that bundle image belongs to +// @channels: The list of channels that bundle image belongs to +// @channelDefault: The default channel for the bundle image +// @overwrite: Boolean flag to enable overwriting annotations.yaml locally if existed func BuildFunc(directory, imageTag, imageBuilder, packageName, channels, channelDefault string, overwrite bool) error { // Generate annotations.yaml and Dockerfile err := GenerateFunc(directory, packageName, channels, channelDefault, overwrite) diff --git a/pkg/lib/bundle/generate.go b/pkg/lib/bundle/generate.go index ed42f135c..f85c8bb6a 100644 --- a/pkg/lib/bundle/generate.go +++ b/pkg/lib/bundle/generate.go @@ -36,6 +36,12 @@ type AnnotationMetadata struct { // GenerateFunc builds annotations.yaml with mediatype, manifests & // metadata directories in bundle image, package name, channels and default // channels information and then writes the file to `/metadata` directory. +// Inputs: +// @directory: The local directory where bundle manifests and metadata are located +// @packageName: The name of the package that bundle image belongs to +// @channels: The list of channels that bundle image belongs to +// @channelDefault: The default channel for the bundle image +// @overwrite: Boolean flag to enable overwriting annotations.yaml locally if existed func GenerateFunc(directory, packageName, channels, channelDefault string, overwrite bool) error { var mediaType string From f5dc03e5d3fadd9b675af2249e768ce2b449312d Mon Sep 17 00:00:00 2001 From: Vu Dinh Date: Mon, 28 Oct 2019 13:22:40 -0400 Subject: [PATCH 7/7] Change const to upper case so it can be used Signed-off-by: Vu Dinh --- pkg/lib/bundle/build.go | 2 +- pkg/lib/bundle/generate.go | 74 ++++++++++++++++----------------- pkg/lib/bundle/generate_test.go | 24 +++++------ pkg/lib/bundle/utils_test.go | 6 +-- 4 files changed, 53 insertions(+), 53 deletions(-) diff --git a/pkg/lib/bundle/build.go b/pkg/lib/bundle/build.go index 7c729529f..0094f33d0 100644 --- a/pkg/lib/bundle/build.go +++ b/pkg/lib/bundle/build.go @@ -13,7 +13,7 @@ import ( func BuildBundleImage(directory, imageTag, imageBuilder string) (*exec.Cmd, error) { var args []string - dockerfilePath := path.Join(directory, dockerFile) + dockerfilePath := path.Join(directory, DockerFile) switch imageBuilder { case "docker", "podman": diff --git a/pkg/lib/bundle/generate.go b/pkg/lib/bundle/generate.go index f85c8bb6a..971196941 100644 --- a/pkg/lib/bundle/generate.go +++ b/pkg/lib/bundle/generate.go @@ -13,20 +13,20 @@ import ( ) const ( - defaultPermission = 0644 - registryV1Type = "registry+v1" - plainType = "plain" - helmType = "helm" - annotationsFile = "annotations.yaml" - dockerFile = "Dockerfile" - manifestsDir = "manifests/" - metadataDir = "metadata/" - manifestsLabel = "operators.operatorframework.io.bundle.manifests.v1" - metadataLabel = "operators.operatorframework.io.bundle.metadata.v1" - mediatypeLabel = "operators.operatorframework.io.bundle.mediatype.v1" - packageLabel = "operators.operatorframework.io.bundle.package.v1" - channelsLabel = "operators.operatorframework.io.bundle.channels.v1" - channelDefaultLabel = "operators.operatorframework.io.bundle.channel.default.v1" + DefaultPermission = 0644 + RegistryV1Type = "registry+v1" + PlainType = "plain" + HelmType = "helm" + AnnotationsFile = "annotations.yaml" + DockerFile = "Dockerfile" + ManifestsDir = "manifests/" + MetadataDir = "metadata/" + ManifestsLabel = "operators.operatorframework.io.bundle.manifests.v1" + MetadataLabel = "operators.operatorframework.io.bundle.metadata.v1" + MediatypeLabel = "operators.operatorframework.io.bundle.mediatype.v1" + PackageLabel = "operators.operatorframework.io.bundle.package.v1" + ChannelsLabel = "operators.operatorframework.io.bundle.channels.v1" + ChannelDefaultLabel = "operators.operatorframework.io.bundle.channel.default.v1" ) type AnnotationMetadata struct { @@ -54,14 +54,14 @@ func GenerateFunc(directory, packageName, channels, channelDefault string, overw log.Info("Building annotations.yaml") // Generate annotations.yaml - content, err := GenerateAnnotations(mediaType, manifestsDir, metadataDir, packageName, channels, channelDefault) + content, err := GenerateAnnotations(mediaType, ManifestsDir, MetadataDir, packageName, channels, channelDefault) if err != nil { return err } - file, err := ioutil.ReadFile(filepath.Join(directory, metadataDir, annotationsFile)) + file, err := ioutil.ReadFile(filepath.Join(directory, MetadataDir, AnnotationsFile)) if os.IsNotExist(err) || overwrite { - err = WriteFile(annotationsFile, filepath.Join(directory, metadataDir), content) + err = WriteFile(AnnotationsFile, filepath.Join(directory, MetadataDir), content) if err != nil { return err } @@ -77,12 +77,12 @@ func GenerateFunc(directory, packageName, channels, channelDefault string, overw log.Info("Building Dockerfile") // Generate Dockerfile - content, err = GenerateDockerfile(directory, mediaType, manifestsDir, metadataDir, packageName, channels, channelDefault) + content, err = GenerateDockerfile(directory, mediaType, ManifestsDir, MetadataDir, packageName, channels, channelDefault) if err != nil { return err } - err = WriteFile(dockerFile, directory, content) + err = WriteFile(DockerFile, directory, content) if err != nil { return err } @@ -113,15 +113,15 @@ func GetMediaType(directory string) (string, error) { // Validate the file names to determine media type for _, file := range files { if file == "Chart.yaml" { - return helmType, nil + return HelmType, nil } else if strings.HasSuffix(file, "clusterserviceversion.yaml") { - return registryV1Type, nil + return RegistryV1Type, nil } else { continue } } - return plainType, nil + return PlainType, nil } // ValidateAnnotations validates existing annotations.yaml against generated @@ -199,12 +199,12 @@ func ValidateChannelDefault(channels, channelDefault string) (string, error) { func GenerateAnnotations(mediaType, manifests, metadata, packageName, channels, channelDefault string) ([]byte, error) { annotations := &AnnotationMetadata{ Annotations: map[string]string{ - mediatypeLabel: mediaType, - manifestsLabel: manifests, - metadataLabel: metadata, - packageLabel: packageName, - channelsLabel: channels, - channelDefaultLabel: channelDefault, + MediatypeLabel: mediaType, + ManifestsLabel: manifests, + MetadataLabel: metadata, + PackageLabel: packageName, + ChannelsLabel: channels, + ChannelDefaultLabel: channelDefault, }, } @@ -213,7 +213,7 @@ func GenerateAnnotations(mediaType, manifests, metadata, packageName, channels, return nil, err } - annotations.Annotations[channelDefaultLabel] = chanDefault + annotations.Annotations[ChannelDefaultLabel] = chanDefault afile, err := yaml.Marshal(annotations) if err != nil { @@ -238,16 +238,16 @@ func GenerateDockerfile(directory, mediaType, manifests, metadata, packageName, fileContent += "FROM scratch\n\n" // LABEL - fileContent += fmt.Sprintf("LABEL %s=%s\n", mediatypeLabel, mediaType) - fileContent += fmt.Sprintf("LABEL %s=%s\n", manifestsLabel, manifests) - fileContent += fmt.Sprintf("LABEL %s=%s\n", metadataLabel, metadata) - fileContent += fmt.Sprintf("LABEL %s=%s\n", packageLabel, packageName) - fileContent += fmt.Sprintf("LABEL %s=%s\n", channelsLabel, channels) - fileContent += fmt.Sprintf("LABEL %s=%s\n\n", channelDefaultLabel, chanDefault) + fileContent += fmt.Sprintf("LABEL %s=%s\n", MediatypeLabel, mediaType) + fileContent += fmt.Sprintf("LABEL %s=%s\n", ManifestsLabel, manifests) + fileContent += fmt.Sprintf("LABEL %s=%s\n", MetadataLabel, metadata) + fileContent += fmt.Sprintf("LABEL %s=%s\n", PackageLabel, packageName) + fileContent += fmt.Sprintf("LABEL %s=%s\n", ChannelsLabel, channels) + fileContent += fmt.Sprintf("LABEL %s=%s\n\n", ChannelDefaultLabel, chanDefault) // CONTENT fileContent += fmt.Sprintf("ADD %s %s\n", filepath.Join(directory, "*.yaml"), "/manifests") - fileContent += fmt.Sprintf("ADD %s %s%s\n", filepath.Join(directory, metadata, annotationsFile), "/metadata/", annotationsFile) + fileContent += fmt.Sprintf("ADD %s %s%s\n", filepath.Join(directory, metadata, AnnotationsFile), "/metadata/", AnnotationsFile) return []byte(fileContent), nil } @@ -259,7 +259,7 @@ func WriteFile(fileName, directory string, content []byte) error { os.Mkdir(directory, os.ModePerm) } - err := ioutil.WriteFile(filepath.Join(directory, fileName), content, defaultPermission) + err := ioutil.WriteFile(filepath.Join(directory, fileName), content, DefaultPermission) if err != nil { return err } diff --git a/pkg/lib/bundle/generate_test.go b/pkg/lib/bundle/generate_test.go index d9482f362..7aeab8a05 100644 --- a/pkg/lib/bundle/generate_test.go +++ b/pkg/lib/bundle/generate_test.go @@ -21,17 +21,17 @@ func TestGetMediaType(t *testing.T) { }{ { testDir, - registryV1Type, + RegistryV1Type, "", }, { testDir, - helmType, + HelmType, "", }, { testDir, - plainType, + PlainType, "", }, { @@ -183,12 +183,12 @@ func TestGenerateAnnotationsFunc(t *testing.T) { // Create test annotations struct testAnnotations := &AnnotationMetadata{ Annotations: map[string]string{ - mediatypeLabel: "test1", - manifestsLabel: "test2", - metadataLabel: "test3", - packageLabel: "test4", - channelsLabel: "test5", - channelDefaultLabel: "test5", + MediatypeLabel: "test1", + ManifestsLabel: "test2", + MetadataLabel: "test3", + PackageLabel: "test4", + ChannelsLabel: "test5", + ChannelDefaultLabel: "test5", }, } // Create result annotations struct @@ -213,10 +213,10 @@ func TestGenerateDockerfileFunc(t *testing.T) { "LABEL operators.operatorframework.io.bundle.channels.v1=test5\n"+ "LABEL operators.operatorframework.io.bundle.channel.default.v1=test5\n\n"+ "ADD %s/*.yaml /manifests\n"+ - "ADD %s/annotations.yaml /metadata/annotations.yaml\n", metadataDir, getTestDir(), - filepath.Join(getTestDir(), metadataDir)) + "ADD %s/annotations.yaml /metadata/annotations.yaml\n", MetadataDir, getTestDir(), + filepath.Join(getTestDir(), MetadataDir)) - content, err := GenerateDockerfile(getTestDir(), "test1", "test2", metadataDir, "test4", "test5", "") + content, err := GenerateDockerfile(getTestDir(), "test1", "test2", MetadataDir, "test4", "test5", "") require.NoError(t, err) require.Equal(t, output, string(content)) } diff --git a/pkg/lib/bundle/utils_test.go b/pkg/lib/bundle/utils_test.go index d74b20dee..64281bc00 100644 --- a/pkg/lib/bundle/utils_test.go +++ b/pkg/lib/bundle/utils_test.go @@ -43,13 +43,13 @@ func createDir(dir string) { func createFiles(dir, input string) { // Create test files in test directory switch input { - case registryV1Type: + case RegistryV1Type: file, _ := os.Create(filepath.Join(dir, csvFile)) file.Close() - case helmType: + case HelmType: file, _ := os.Create(filepath.Join(dir, helmFile)) file.Close() - case plainType: + case PlainType: file, _ := os.Create(filepath.Join(dir, crdFile)) file.Close() default: