github.com/jmrodri/operator-sdk@v0.5.0/commands/operator-sdk/cmd/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 cmdtest 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/util/fileutil" 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/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 file, err := yamlutil.GenerateCombinedNamespacedManifest() 126 if err != nil { 127 return err 128 } 129 tlConfig.namespacedManPath = file.Name() 130 defer func() { 131 err := os.Remove(tlConfig.namespacedManPath) 132 if err != nil { 133 log.Errorf("Could not delete temporary namespace manifest file: (%v)", err) 134 } 135 }() 136 } 137 if tlConfig.globalManPath == "" && !tlConfig.noSetup { 138 file, err := yamlutil.GenerateCombinedGlobalManifest() 139 if err != nil { 140 return err 141 } 142 tlConfig.globalManPath = file.Name() 143 defer func() { 144 err := os.Remove(tlConfig.globalManPath) 145 if err != nil { 146 log.Errorf("Could not delete global manifest file: (%v)", err) 147 } 148 }() 149 } 150 if tlConfig.noSetup { 151 err := os.MkdirAll(deployTestDir, os.FileMode(fileutil.DefaultDirFileMode)) 152 if err != nil { 153 return fmt.Errorf("could not create %s: (%v)", deployTestDir, err) 154 } 155 tlConfig.namespacedManPath = filepath.Join(deployTestDir, "empty.yaml") 156 tlConfig.globalManPath = filepath.Join(deployTestDir, "empty.yaml") 157 emptyBytes := []byte{} 158 err = ioutil.WriteFile(tlConfig.globalManPath, emptyBytes, os.FileMode(fileutil.DefaultFileMode)) 159 if err != nil { 160 return fmt.Errorf("could not create empty manifest file: (%v)", err) 161 } 162 defer func() { 163 err := os.Remove(tlConfig.globalManPath) 164 if err != nil { 165 log.Errorf("Could not delete empty manifest file: (%v)", err) 166 } 167 }() 168 } 169 if tlConfig.image != "" { 170 err := replaceImage(tlConfig.namespacedManPath, tlConfig.image) 171 if err != nil { 172 return fmt.Errorf("failed to overwrite operator image in the namespaced manifest: %v", err) 173 } 174 } 175 testArgs := []string{"test", args[0] + "/..."} 176 if tlConfig.kubeconfig != "" { 177 testArgs = append(testArgs, "-"+test.KubeConfigFlag, tlConfig.kubeconfig) 178 } 179 testArgs = append(testArgs, "-"+test.NamespacedManPathFlag, tlConfig.namespacedManPath) 180 testArgs = append(testArgs, "-"+test.GlobalManPathFlag, tlConfig.globalManPath) 181 testArgs = append(testArgs, "-"+test.ProjRootFlag, projutil.MustGetwd()) 182 // if we do the append using an empty go flags, it inserts an empty arg, which causes 183 // any later flags to be ignored 184 if tlConfig.goTestFlags != "" { 185 testArgs = append(testArgs, strings.Split(tlConfig.goTestFlags, " ")...) 186 } 187 if tlConfig.namespace != "" || tlConfig.noSetup { 188 testArgs = append(testArgs, "-"+test.SingleNamespaceFlag, "-parallel=1") 189 } 190 if tlConfig.upLocal { 191 testArgs = append(testArgs, "-"+test.LocalOperatorFlag) 192 } 193 dc := exec.Command("go", testArgs...) 194 dc.Env = append(os.Environ(), fmt.Sprintf("%v=%v", test.TestNamespaceEnv, tlConfig.namespace)) 195 dc.Dir = projutil.MustGetwd() 196 if err := projutil.ExecCmd(dc); err != nil { 197 return err 198 } 199 200 log.Info("Local operator test successfully completed.") 201 return nil 202 } 203 204 // TODO: add support for multiple deployments and containers (user would have to 205 // provide extra information in that case) 206 207 // replaceImage searches for a deployment and replaces the image in the container 208 // to the one specified in the function call. The function will fail if the 209 // number of deployments is not equal to one or if the deployment has multiple 210 // containers 211 func replaceImage(manifestPath, image string) error { 212 yamlFile, err := ioutil.ReadFile(manifestPath) 213 if err != nil { 214 return err 215 } 216 foundDeployment := false 217 newManifest := []byte{} 218 scanner := yamlutil.NewYAMLScanner(yamlFile) 219 for scanner.Scan() { 220 yamlSpec := scanner.Bytes() 221 222 decoded := make(map[string]interface{}) 223 err = yaml.Unmarshal(yamlSpec, &decoded) 224 if err != nil { 225 return err 226 } 227 kind, ok := decoded["kind"].(string) 228 if !ok || kind != "Deployment" { 229 newManifest = yamlutil.CombineManifests(newManifest, yamlSpec) 230 continue 231 } 232 if foundDeployment { 233 return fmt.Errorf("cannot use `image` flag on namespaced manifest with more than 1 deployment") 234 } 235 foundDeployment = true 236 scheme := runtime.NewScheme() 237 // scheme for client go 238 if err := cgoscheme.AddToScheme(scheme); err != nil { 239 log.Fatalf("Failed to add client-go scheme to runtime client: (%v)", err) 240 } 241 dynamicDecoder := serializer.NewCodecFactory(scheme).UniversalDeserializer() 242 243 obj, _, err := dynamicDecoder.Decode(yamlSpec, nil, nil) 244 if err != nil { 245 return err 246 } 247 dep := &appsv1.Deployment{} 248 switch o := obj.(type) { 249 case *appsv1.Deployment: 250 dep = o 251 default: 252 return fmt.Errorf("error in replaceImage switch case; could not convert runtime.Object to deployment") 253 } 254 if len(dep.Spec.Template.Spec.Containers) != 1 { 255 return fmt.Errorf("cannot use `image` flag on namespaced manifest containing more than 1 container in the operator deployment") 256 } 257 dep.Spec.Template.Spec.Containers[0].Image = image 258 updatedYamlSpec, err := yaml.Marshal(dep) 259 if err != nil { 260 return fmt.Errorf("failed to convert deployment object back to yaml: %v", err) 261 } 262 newManifest = yamlutil.CombineManifests(newManifest, updatedYamlSpec) 263 } 264 if err := scanner.Err(); err != nil { 265 return fmt.Errorf("failed to scan %s: (%v)", manifestPath, err) 266 } 267 268 return ioutil.WriteFile(manifestPath, newManifest, fileutil.DefaultFileMode) 269 }