github.com/jmrodri/operator-sdk@v0.5.0/commands/operator-sdk/cmd/build.go (about)

     1  // Copyright 2018 The Operator-SDK Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cmd
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"io/ioutil"
    21  	"os"
    22  	"os/exec"
    23  	"path/filepath"
    24  	"strings"
    25  
    26  	"github.com/operator-framework/operator-sdk/internal/util/projutil"
    27  	"github.com/operator-framework/operator-sdk/internal/util/yamlutil"
    28  	"github.com/operator-framework/operator-sdk/pkg/scaffold"
    29  	"github.com/operator-framework/operator-sdk/pkg/scaffold/input"
    30  	"github.com/operator-framework/operator-sdk/pkg/test"
    31  
    32  	"github.com/ghodss/yaml"
    33  	log "github.com/sirupsen/logrus"
    34  	"github.com/spf13/cobra"
    35  )
    36  
    37  var (
    38  	namespacedManBuild string
    39  	testLocationBuild  string
    40  	enableTests        bool
    41  	dockerBuildArgs    string
    42  )
    43  
    44  func NewBuildCmd() *cobra.Command {
    45  	buildCmd := &cobra.Command{
    46  		Use:   "build <image>",
    47  		Short: "Compiles code and builds artifacts",
    48  		Long: `The operator-sdk build command compiles the code, builds the executables,
    49  and generates Kubernetes manifests.
    50  
    51  <image> is the container image to be built, e.g. "quay.io/example/operator:v0.0.1".
    52  This image will be automatically set in the deployment manifests.
    53  
    54  After build completes, the image would be built locally in docker. Then it needs to
    55  be pushed to remote registry.
    56  For example:
    57  	$ operator-sdk build quay.io/example/operator:v0.0.1
    58  	$ docker push quay.io/example/operator:v0.0.1
    59  `,
    60  		RunE: buildFunc,
    61  	}
    62  	buildCmd.Flags().BoolVar(&enableTests, "enable-tests", false, "Enable in-cluster testing by adding test binary to the image")
    63  	buildCmd.Flags().StringVar(&testLocationBuild, "test-location", "./test/e2e", "Location of tests")
    64  	buildCmd.Flags().StringVar(&namespacedManBuild, "namespaced-manifest", "deploy/operator.yaml", "Path of namespaced resources manifest for tests")
    65  	buildCmd.Flags().StringVar(&dockerBuildArgs, "docker-build-args", "", "Extra docker build arguments as one string such as \"--build-arg https_proxy=$https_proxy\"")
    66  	return buildCmd
    67  }
    68  
    69  /*
    70   * verifyDeploymentImages checks image names of pod 0 in deployments found in the provided yaml file.
    71   * This is done because e2e tests require a namespaced manifest file to configure a namespace with
    72   * required resources. This function is intended to identify if a user used a different image name
    73   * for their operator in the provided yaml, which would result in the testing of the wrong operator
    74   * image. As it is possible for a namespaced yaml to have multiple deployments (such as the vault
    75   * operator, which depends on the etcd-operator), this is just a warning, not a fatal error.
    76   */
    77  func verifyDeploymentImage(yamlFile []byte, imageName string) error {
    78  	warningMessages := ""
    79  	scanner := yamlutil.NewYAMLScanner(yamlFile)
    80  	for scanner.Scan() {
    81  		yamlSpec := scanner.Bytes()
    82  
    83  		yamlMap := make(map[string]interface{})
    84  		err := yaml.Unmarshal(yamlSpec, &yamlMap)
    85  		if err != nil {
    86  			return fmt.Errorf("could not unmarshal YAML namespaced spec: (%v)", err)
    87  		}
    88  		kind, ok := yamlMap["kind"].(string)
    89  		if !ok {
    90  			return fmt.Errorf("yaml manifest file contains a 'kind' field that is not a string")
    91  		}
    92  		if kind == "Deployment" {
    93  			// this is ugly and hacky; we should probably make this cleaner
    94  			nestedMap, ok := yamlMap["spec"].(map[string]interface{})
    95  			if !ok {
    96  				continue
    97  			}
    98  			nestedMap, ok = nestedMap["template"].(map[string]interface{})
    99  			if !ok {
   100  				continue
   101  			}
   102  			nestedMap, ok = nestedMap["spec"].(map[string]interface{})
   103  			if !ok {
   104  				continue
   105  			}
   106  			containersArray, ok := nestedMap["containers"].([]interface{})
   107  			if !ok {
   108  				continue
   109  			}
   110  			for _, item := range containersArray {
   111  				image, ok := item.(map[string]interface{})["image"].(string)
   112  				if !ok {
   113  					continue
   114  				}
   115  				if image != imageName {
   116  					warningMessages = fmt.Sprintf("%s\nWARNING: Namespace manifest contains a deployment with image %v, which does not match the name of the image being built: %v", warningMessages, image, imageName)
   117  				}
   118  			}
   119  		}
   120  	}
   121  	if err := scanner.Err(); err != nil {
   122  		return fmt.Errorf("failed to verify deployment image: (%v)", err)
   123  	}
   124  	if warningMessages == "" {
   125  		return nil
   126  	}
   127  	return errors.New(warningMessages)
   128  }
   129  
   130  func verifyTestManifest(image string) error {
   131  	namespacedBytes, err := ioutil.ReadFile(namespacedManBuild)
   132  	if err != nil {
   133  		return fmt.Errorf("could not read namespaced manifest: (%v)", err)
   134  	}
   135  
   136  	err = verifyDeploymentImage(namespacedBytes, image)
   137  	// the error from verifyDeploymentImage is just a warning, not fatal error
   138  	if err != nil {
   139  		log.Warn(err)
   140  	}
   141  	return nil
   142  }
   143  
   144  func buildFunc(cmd *cobra.Command, args []string) error {
   145  	if len(args) != 1 {
   146  		return fmt.Errorf("command %s requires exactly one argument", cmd.CommandPath())
   147  	}
   148  
   149  	projutil.MustInProjectRoot()
   150  	goBuildEnv := append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=0")
   151  	goTrimFlags := []string{"-gcflags", "all=-trimpath=${GOPATH}", "-asmflags", "all=-trimpath=${GOPATH}"}
   152  	absProjectPath := projutil.MustGetwd()
   153  	projectName := filepath.Base(absProjectPath)
   154  
   155  	// Don't need to build Go code if a non-Go Operator.
   156  	if projutil.GetOperatorType() == projutil.OperatorTypeGo {
   157  		managerDir := filepath.Join(projutil.CheckAndGetProjectGoPkg(), scaffold.ManagerDir)
   158  		outputBinName := filepath.Join(absProjectPath, scaffold.BuildBinDir, projectName)
   159  		goBuildArgs := append(append([]string{"build"}, goTrimFlags...), "-o", outputBinName, managerDir)
   160  		buildCmd := exec.Command("go", goBuildArgs...)
   161  		buildCmd.Env = goBuildEnv
   162  		if err := projutil.ExecCmd(buildCmd); err != nil {
   163  			return fmt.Errorf("failed to build operator binary: (%v)", err)
   164  		}
   165  	}
   166  
   167  	image := args[0]
   168  	baseImageName := image
   169  	if enableTests {
   170  		baseImageName += "-intermediate"
   171  	}
   172  
   173  	log.Infof("Building Docker image %s", baseImageName)
   174  
   175  	dbArgs := []string{"build", ".", "-f", "build/Dockerfile", "-t", baseImageName}
   176  
   177  	if dockerBuildArgs != "" {
   178  		splitArgs := strings.Fields(dockerBuildArgs)
   179  		dbArgs = append(dbArgs, splitArgs...)
   180  	}
   181  
   182  	dbcmd := exec.Command("docker", dbArgs...)
   183  	if err := projutil.ExecCmd(dbcmd); err != nil {
   184  		if enableTests {
   185  			return fmt.Errorf("failed to output intermediate image %s: (%v)", image, err)
   186  		}
   187  		return fmt.Errorf("failed to output build image %s: (%v)", image, err)
   188  	}
   189  
   190  	if enableTests {
   191  		if projutil.GetOperatorType() == projutil.OperatorTypeGo {
   192  			testBinary := filepath.Join(absProjectPath, scaffold.BuildBinDir, projectName+"-test")
   193  			goTestBuildArgs := append(append([]string{"test"}, goTrimFlags...), "-c", "-o", testBinary, testLocationBuild+"/...")
   194  			buildTestCmd := exec.Command("go", goTestBuildArgs...)
   195  			buildTestCmd.Env = goBuildEnv
   196  			if err := projutil.ExecCmd(buildTestCmd); err != nil {
   197  				return fmt.Errorf("failed to build test binary: (%v)", err)
   198  			}
   199  		}
   200  
   201  		// if a user is using an older sdk repo as their library, make sure they have required build files
   202  		testDockerfile := filepath.Join(scaffold.BuildTestDir, scaffold.DockerfileFile)
   203  		_, err := os.Stat(testDockerfile)
   204  		if err != nil && os.IsNotExist(err) {
   205  
   206  			log.Info("Generating build manifests for test-framework.")
   207  
   208  			cfg := &input.Config{
   209  				Repo:           projutil.CheckAndGetProjectGoPkg(),
   210  				AbsProjectPath: absProjectPath,
   211  				ProjectName:    projectName,
   212  			}
   213  
   214  			s := &scaffold.Scaffold{}
   215  			t := projutil.GetOperatorType()
   216  			switch t {
   217  			case projutil.OperatorTypeGo:
   218  				err = s.Execute(cfg,
   219  					&scaffold.TestFrameworkDockerfile{},
   220  					&scaffold.GoTestScript{},
   221  					&scaffold.TestPod{Image: image, TestNamespaceEnv: test.TestNamespaceEnv},
   222  				)
   223  			case projutil.OperatorTypeAnsible:
   224  				return fmt.Errorf("test scaffolding for Ansible Operators is not implemented")
   225  			case projutil.OperatorTypeHelm:
   226  				return fmt.Errorf("test scaffolding for Helm Operators is not implemented")
   227  			default:
   228  				return fmt.Errorf("unknown operator type '%v'", t)
   229  			}
   230  
   231  			if err != nil {
   232  				return fmt.Errorf("test framework manifest scaffold failed: (%v)", err)
   233  			}
   234  		}
   235  
   236  		log.Infof("Building test Docker image %s", image)
   237  
   238  		testDbArgs := []string{"build", ".", "-f", testDockerfile, "-t", image, "--build-arg", "NAMESPACEDMAN=" + namespacedManBuild, "--build-arg", "BASEIMAGE=" + baseImageName}
   239  
   240  		if dockerBuildArgs != "" {
   241  			splitArgs := strings.Fields(dockerBuildArgs)
   242  			testDbArgs = append(testDbArgs, splitArgs...)
   243  		}
   244  
   245  		testDbcmd := exec.Command("docker", testDbArgs...)
   246  		if err := projutil.ExecCmd(testDbcmd); err != nil {
   247  			return fmt.Errorf("failed to output test image %s: (%v)", image, err)
   248  		}
   249  		// Check image name of deployments in namespaced manifest
   250  		if err := verifyTestManifest(image); err != nil {
   251  			return nil
   252  		}
   253  	}
   254  
   255  	log.Info("Operator build complete.")
   256  	return nil
   257  }