github.com/fabianvf/ocp-release-operator-sdk@v0.0.0-20190426141702-57620ee2f090/cmd/operator-sdk/test/local.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 test 16 17 import ( 18 "fmt" 19 "io/ioutil" 20 "os" 21 "os/exec" 22 "path/filepath" 23 "strings" 24 25 "github.com/operator-framework/operator-sdk/internal/pkg/scaffold" 26 "github.com/operator-framework/operator-sdk/internal/util/fileutil" 27 "github.com/operator-framework/operator-sdk/internal/util/projutil" 28 "github.com/operator-framework/operator-sdk/internal/util/yamlutil" 29 "github.com/operator-framework/operator-sdk/pkg/test" 30 31 "github.com/ghodss/yaml" 32 log "github.com/sirupsen/logrus" 33 "github.com/spf13/cobra" 34 appsv1 "k8s.io/api/apps/v1" 35 "k8s.io/apimachinery/pkg/runtime" 36 "k8s.io/apimachinery/pkg/runtime/serializer" 37 cgoscheme "k8s.io/client-go/kubernetes/scheme" 38 ) 39 40 var deployTestDir = filepath.Join(scaffold.DeployDir, "test") 41 42 type testLocalConfig struct { 43 kubeconfig string 44 globalManPath string 45 namespacedManPath string 46 goTestFlags string 47 moleculeTestFlags string 48 namespace string 49 upLocal bool 50 noSetup bool 51 debug bool 52 image string 53 } 54 55 var tlConfig testLocalConfig 56 57 func newTestLocalCmd() *cobra.Command { 58 testCmd := &cobra.Command{ 59 Use: "local <path to tests directory> [flags]", 60 Short: "Run End-To-End tests locally", 61 RunE: testLocalFunc, 62 } 63 testCmd.Flags().StringVar(&tlConfig.kubeconfig, "kubeconfig", "", "Kubeconfig path") 64 testCmd.Flags().StringVar(&tlConfig.globalManPath, "global-manifest", "", "Path to manifest for Global resources (e.g. CRD manifests)") 65 testCmd.Flags().StringVar(&tlConfig.namespacedManPath, "namespaced-manifest", "", "Path to manifest for per-test, namespaced resources (e.g. RBAC and Operator manifest)") 66 testCmd.Flags().StringVar(&tlConfig.goTestFlags, "go-test-flags", "", "Additional flags to pass to go test") 67 testCmd.Flags().StringVar(&tlConfig.moleculeTestFlags, "molecule-test-flags", "", "Additional flags to pass to molecule test") 68 testCmd.Flags().StringVar(&tlConfig.namespace, "namespace", "", "If non-empty, single namespace to run tests in") 69 testCmd.Flags().BoolVar(&tlConfig.upLocal, "up-local", false, "Enable running operator locally with go run instead of as an image in the cluster") 70 testCmd.Flags().BoolVar(&tlConfig.noSetup, "no-setup", false, "Disable test resource creation") 71 testCmd.Flags().BoolVar(&tlConfig.debug, "debug", false, "Enable debug-level logging") 72 testCmd.Flags().StringVar(&tlConfig.image, "image", "", "Use a different operator image from the one specified in the namespaced manifest") 73 74 return testCmd 75 } 76 77 func testLocalFunc(cmd *cobra.Command, args []string) error { 78 t := projutil.GetOperatorType() 79 switch t { 80 case projutil.OperatorTypeGo: 81 return testLocalGoFunc(cmd, args) 82 case projutil.OperatorTypeAnsible: 83 return testLocalAnsibleFunc(cmd, args) 84 case projutil.OperatorTypeHelm: 85 return fmt.Errorf("`test local` for Helm operators is not implemented") 86 } 87 return fmt.Errorf("unknown operator type '%v'", t) 88 } 89 90 func testLocalAnsibleFunc(cmd *cobra.Command, args []string) error { 91 projutil.MustInProjectRoot() 92 testArgs := []string{} 93 if tlConfig.debug { 94 testArgs = append(testArgs, "--debug") 95 } 96 testArgs = append(testArgs, "test", "-s", "test-local") 97 98 if tlConfig.moleculeTestFlags != "" { 99 testArgs = append(testArgs, strings.Split(tlConfig.moleculeTestFlags, " ")...) 100 } 101 102 dc := exec.Command("molecule", testArgs...) 103 dc.Env = append(os.Environ(), fmt.Sprintf("%v=%v", test.TestNamespaceEnv, tlConfig.namespace)) 104 dc.Dir = projutil.MustGetwd() 105 return projutil.ExecCmd(dc) 106 } 107 108 func testLocalGoFunc(cmd *cobra.Command, args []string) error { 109 if len(args) != 1 { 110 return fmt.Errorf("command %s requires exactly one argument", cmd.CommandPath()) 111 } 112 if (tlConfig.noSetup && tlConfig.globalManPath != "") || 113 (tlConfig.noSetup && tlConfig.namespacedManPath != "") { 114 return fmt.Errorf("the global-manifest and namespaced-manifest flags cannot be enabled at the same time as the no-setup flag") 115 } 116 117 if tlConfig.upLocal && tlConfig.namespace == "" { 118 return fmt.Errorf("must specify a namespace to run in when -up-local flag is set") 119 } 120 121 log.Info("Testing operator locally.") 122 123 // if no namespaced manifest path is given, combine deploy/service_account.yaml, deploy/role.yaml, deploy/role_binding.yaml and deploy/operator.yaml 124 if tlConfig.namespacedManPath == "" && !tlConfig.noSetup { 125 if !tlConfig.upLocal { 126 file, err := yamlutil.GenerateCombinedNamespacedManifest(scaffold.DeployDir) 127 if err != nil { 128 return err 129 } 130 tlConfig.namespacedManPath = file.Name() 131 } else { 132 file, err := ioutil.TempFile("", "empty.yaml") 133 if err != nil { 134 return fmt.Errorf("could not create empty manifest file: (%v)", err) 135 } 136 tlConfig.namespacedManPath = file.Name() 137 emptyBytes := []byte{} 138 if err := file.Chmod(os.FileMode(fileutil.DefaultFileMode)); err != nil { 139 return fmt.Errorf("could not chown temporary namespaced manifest file: (%v)", err) 140 } 141 if _, err := file.Write(emptyBytes); err != nil { 142 return fmt.Errorf("could not write temporary namespaced manifest file: (%v)", err) 143 } 144 if err := file.Close(); err != nil { 145 return err 146 } 147 } 148 defer func() { 149 err := os.Remove(tlConfig.namespacedManPath) 150 if err != nil { 151 log.Errorf("Could not delete temporary namespace manifest file: (%v)", err) 152 } 153 }() 154 } 155 if tlConfig.globalManPath == "" && !tlConfig.noSetup { 156 file, err := yamlutil.GenerateCombinedGlobalManifest(scaffold.CRDsDir) 157 if err != nil { 158 return err 159 } 160 tlConfig.globalManPath = file.Name() 161 defer func() { 162 err := os.Remove(tlConfig.globalManPath) 163 if err != nil { 164 log.Errorf("Could not delete global manifest file: (%v)", err) 165 } 166 }() 167 } 168 if tlConfig.noSetup { 169 err := os.MkdirAll(deployTestDir, os.FileMode(fileutil.DefaultDirFileMode)) 170 if err != nil { 171 return fmt.Errorf("could not create %s: (%v)", deployTestDir, err) 172 } 173 tlConfig.namespacedManPath = filepath.Join(deployTestDir, "empty.yaml") 174 tlConfig.globalManPath = filepath.Join(deployTestDir, "empty.yaml") 175 emptyBytes := []byte{} 176 err = ioutil.WriteFile(tlConfig.globalManPath, emptyBytes, os.FileMode(fileutil.DefaultFileMode)) 177 if err != nil { 178 return fmt.Errorf("could not create empty manifest file: (%v)", err) 179 } 180 defer func() { 181 err := os.Remove(tlConfig.globalManPath) 182 if err != nil { 183 log.Errorf("Could not delete empty manifest file: (%v)", err) 184 } 185 }() 186 } 187 if tlConfig.image != "" { 188 err := replaceImage(tlConfig.namespacedManPath, tlConfig.image) 189 if err != nil { 190 return fmt.Errorf("failed to overwrite operator image in the namespaced manifest: %v", err) 191 } 192 } 193 testArgs := []string{"test", args[0] + "/..."} 194 if tlConfig.kubeconfig != "" { 195 testArgs = append(testArgs, "-"+test.KubeConfigFlag, tlConfig.kubeconfig) 196 } 197 testArgs = append(testArgs, "-"+test.NamespacedManPathFlag, tlConfig.namespacedManPath) 198 testArgs = append(testArgs, "-"+test.GlobalManPathFlag, tlConfig.globalManPath) 199 testArgs = append(testArgs, "-"+test.ProjRootFlag, projutil.MustGetwd()) 200 // if we do the append using an empty go flags, it inserts an empty arg, which causes 201 // any later flags to be ignored 202 if tlConfig.goTestFlags != "" { 203 testArgs = append(testArgs, strings.Split(tlConfig.goTestFlags, " ")...) 204 } 205 if tlConfig.namespace != "" || tlConfig.noSetup { 206 testArgs = append(testArgs, "-"+test.SingleNamespaceFlag, "-parallel=1") 207 } 208 if tlConfig.upLocal { 209 testArgs = append(testArgs, "-"+test.LocalOperatorFlag) 210 } 211 dc := exec.Command("go", testArgs...) 212 dc.Env = append(os.Environ(), fmt.Sprintf("%v=%v", test.TestNamespaceEnv, tlConfig.namespace)) 213 dc.Dir = projutil.MustGetwd() 214 if err := projutil.ExecCmd(dc); err != nil { 215 return err 216 } 217 218 log.Info("Local operator test successfully completed.") 219 return nil 220 } 221 222 // TODO: add support for multiple deployments and containers (user would have to 223 // provide extra information in that case) 224 225 // replaceImage searches for a deployment and replaces the image in the container 226 // to the one specified in the function call. The function will fail if the 227 // number of deployments is not equal to one or if the deployment has multiple 228 // containers 229 func replaceImage(manifestPath, image string) error { 230 yamlFile, err := ioutil.ReadFile(manifestPath) 231 if err != nil { 232 return err 233 } 234 foundDeployment := false 235 newManifest := []byte{} 236 scanner := yamlutil.NewYAMLScanner(yamlFile) 237 for scanner.Scan() { 238 yamlSpec := scanner.Bytes() 239 240 decoded := make(map[string]interface{}) 241 err = yaml.Unmarshal(yamlSpec, &decoded) 242 if err != nil { 243 return err 244 } 245 kind, ok := decoded["kind"].(string) 246 if !ok || kind != "Deployment" { 247 newManifest = yamlutil.CombineManifests(newManifest, yamlSpec) 248 continue 249 } 250 if foundDeployment { 251 return fmt.Errorf("cannot use `image` flag on namespaced manifest with more than 1 deployment") 252 } 253 foundDeployment = true 254 scheme := runtime.NewScheme() 255 // scheme for client go 256 if err := cgoscheme.AddToScheme(scheme); err != nil { 257 log.Fatalf("Failed to add client-go scheme to runtime client: (%v)", err) 258 } 259 dynamicDecoder := serializer.NewCodecFactory(scheme).UniversalDeserializer() 260 261 obj, _, err := dynamicDecoder.Decode(yamlSpec, nil, nil) 262 if err != nil { 263 return err 264 } 265 dep := &appsv1.Deployment{} 266 switch o := obj.(type) { 267 case *appsv1.Deployment: 268 dep = o 269 default: 270 return fmt.Errorf("error in replaceImage switch case; could not convert runtime.Object to deployment") 271 } 272 if len(dep.Spec.Template.Spec.Containers) != 1 { 273 return fmt.Errorf("cannot use `image` flag on namespaced manifest containing more than 1 container in the operator deployment") 274 } 275 dep.Spec.Template.Spec.Containers[0].Image = image 276 updatedYamlSpec, err := yaml.Marshal(dep) 277 if err != nil { 278 return fmt.Errorf("failed to convert deployment object back to yaml: %v", err) 279 } 280 newManifest = yamlutil.CombineManifests(newManifest, updatedYamlSpec) 281 } 282 if err := scanner.Err(); err != nil { 283 return fmt.Errorf("failed to scan %s: (%v)", manifestPath, err) 284 } 285 286 return ioutil.WriteFile(manifestPath, newManifest, fileutil.DefaultFileMode) 287 }