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 }