Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions cmd/operator-cli/bundle/build.go
Original file line number Diff line number Diff line change
@@ -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
}
60 changes: 60 additions & 0 deletions cmd/operator-cli/bundle/build_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
}
}
17 changes: 17 additions & 0 deletions cmd/operator-cli/bundle/cmd.go
Original file line number Diff line number Diff line change
@@ -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
}
169 changes: 169 additions & 0 deletions cmd/operator-cli/bundle/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
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"`
}

// 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 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
}
Loading