github.skymusic.top/operator-framework/operator-sdk@v0.8.2/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 switch t := projutil.GetOperatorType(); t { 79 case projutil.OperatorTypeGo: 80 return testLocalGoFunc(cmd, args) 81 case projutil.OperatorTypeAnsible: 82 return testLocalAnsibleFunc(cmd, args) 83 case projutil.OperatorTypeHelm: 84 return fmt.Errorf("`test local` for Helm operators is not implemented") 85 } 86 return projutil.ErrUnknownOperatorType{} 87 } 88 89 func testLocalAnsibleFunc(cmd *cobra.Command, args []string) error { 90 projutil.MustInProjectRoot() 91 testArgs := []string{} 92 if tlConfig.debug { 93 testArgs = append(testArgs, "--debug") 94 } 95 testArgs = append(testArgs, "test", "-s", "test-local") 96 97 if tlConfig.moleculeTestFlags != "" { 98 testArgs = append(testArgs, strings.Split(tlConfig.moleculeTestFlags, " ")...) 99 } 100 101 dc := exec.Command("molecule", testArgs...) 102 dc.Env = append(os.Environ(), fmt.Sprintf("%v=%v", test.TestNamespaceEnv, tlConfig.namespace)) 103 dc.Dir = projutil.MustGetwd() 104 return projutil.ExecCmd(dc) 105 } 106 107 func testLocalGoFunc(cmd *cobra.Command, args []string) error { 108 if len(args) != 1 { 109 return fmt.Errorf("command %s requires exactly one argument", cmd.CommandPath()) 110 } 111 if (tlConfig.noSetup && tlConfig.globalManPath != "") || 112 (tlConfig.noSetup && tlConfig.namespacedManPath != "") { 113 return fmt.Errorf("the global-manifest and namespaced-manifest flags cannot be enabled at the same time as the no-setup flag") 114 } 115 116 if tlConfig.upLocal && tlConfig.namespace == "" { 117 return fmt.Errorf("must specify a namespace to run in when -up-local flag is set") 118 } 119 120 log.Info("Testing operator locally.") 121 122 // if no namespaced manifest path is given, combine deploy/service_account.yaml, deploy/role.yaml, deploy/role_binding.yaml and deploy/operator.yaml 123 if tlConfig.namespacedManPath == "" && !tlConfig.noSetup { 124 if !tlConfig.upLocal { 125 file, err := yamlutil.GenerateCombinedNamespacedManifest(scaffold.DeployDir) 126 if err != nil { 127 return err 128 } 129 tlConfig.namespacedManPath = file.Name() 130 } else { 131 file, err := ioutil.TempFile("", "empty.yaml") 132 if err != nil { 133 return fmt.Errorf("could not create empty manifest file: (%v)", err) 134 } 135 tlConfig.namespacedManPath = file.Name() 136 emptyBytes := []byte{} 137 if err := file.Chmod(os.FileMode(fileutil.DefaultFileMode)); err != nil { 138 return fmt.Errorf("could not chown temporary namespaced manifest file: (%v)", err) 139 } 140 if _, err := file.Write(emptyBytes); err != nil { 141 return fmt.Errorf("could not write temporary namespaced manifest file: (%v)", err) 142 } 143 if err := file.Close(); err != nil { 144 return err 145 } 146 } 147 defer func() { 148 err := os.Remove(tlConfig.namespacedManPath) 149 if err != nil { 150 log.Errorf("Could not delete temporary namespace manifest file: (%v)", err) 151 } 152 }() 153 } 154 if tlConfig.globalManPath == "" && !tlConfig.noSetup { 155 file, err := yamlutil.GenerateCombinedGlobalManifest(scaffold.CRDsDir) 156 if err != nil { 157 return err 158 } 159 tlConfig.globalManPath = file.Name() 160 defer func() { 161 err := os.Remove(tlConfig.globalManPath) 162 if err != nil { 163 log.Errorf("Could not delete global manifest file: (%v)", err) 164 } 165 }() 166 } 167 if tlConfig.noSetup { 168 err := os.MkdirAll(deployTestDir, os.FileMode(fileutil.DefaultDirFileMode)) 169 if err != nil { 170 return fmt.Errorf("could not create %s: (%v)", deployTestDir, err) 171 } 172 tlConfig.namespacedManPath = filepath.Join(deployTestDir, "empty.yaml") 173 tlConfig.globalManPath = filepath.Join(deployTestDir, "empty.yaml") 174 emptyBytes := []byte{} 175 err = ioutil.WriteFile(tlConfig.globalManPath, emptyBytes, os.FileMode(fileutil.DefaultFileMode)) 176 if err != nil { 177 return fmt.Errorf("could not create empty manifest file: (%v)", err) 178 } 179 defer func() { 180 err := os.Remove(tlConfig.globalManPath) 181 if err != nil { 182 log.Errorf("Could not delete empty manifest file: (%v)", err) 183 } 184 }() 185 } 186 if tlConfig.image != "" { 187 err := replaceImage(tlConfig.namespacedManPath, tlConfig.image) 188 if err != nil { 189 return fmt.Errorf("failed to overwrite operator image in the namespaced manifest: %v", err) 190 } 191 } 192 testArgs := []string{ 193 "-" + test.NamespacedManPathFlag, tlConfig.namespacedManPath, 194 "-" + test.GlobalManPathFlag, tlConfig.globalManPath, 195 "-" + test.ProjRootFlag, projutil.MustGetwd(), 196 } 197 if tlConfig.kubeconfig != "" { 198 testArgs = append(testArgs, "-"+test.KubeConfigFlag, tlConfig.kubeconfig) 199 } 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 opts := projutil.GoTestOptions{ 212 GoCmdOptions: projutil.GoCmdOptions{ 213 PackagePath: args[0] + "/...", 214 Env: append(os.Environ(), fmt.Sprintf("%v=%v", test.TestNamespaceEnv, tlConfig.namespace)), 215 Dir: projutil.MustGetwd(), 216 GoMod: projutil.IsDepManagerGoMod(), 217 }, 218 TestBinaryArgs: testArgs, 219 } 220 if err := projutil.GoTest(opts); err != nil { 221 return fmt.Errorf("failed to build test binary: (%v)", err) 222 } 223 log.Info("Local operator test successfully completed.") 224 return nil 225 } 226 227 // TODO: add support for multiple deployments and containers (user would have to 228 // provide extra information in that case) 229 230 // replaceImage searches for a deployment and replaces the image in the container 231 // to the one specified in the function call. The function will fail if the 232 // number of deployments is not equal to one or if the deployment has multiple 233 // containers 234 func replaceImage(manifestPath, image string) error { 235 yamlFile, err := ioutil.ReadFile(manifestPath) 236 if err != nil { 237 return err 238 } 239 foundDeployment := false 240 newManifest := []byte{} 241 scanner := yamlutil.NewYAMLScanner(yamlFile) 242 for scanner.Scan() { 243 yamlSpec := scanner.Bytes() 244 245 decoded := make(map[string]interface{}) 246 err = yaml.Unmarshal(yamlSpec, &decoded) 247 if err != nil { 248 return err 249 } 250 kind, ok := decoded["kind"].(string) 251 if !ok || kind != "Deployment" { 252 newManifest = yamlutil.CombineManifests(newManifest, yamlSpec) 253 continue 254 } 255 if foundDeployment { 256 return fmt.Errorf("cannot use `image` flag on namespaced manifest with more than 1 deployment") 257 } 258 foundDeployment = true 259 scheme := runtime.NewScheme() 260 // scheme for client go 261 if err := cgoscheme.AddToScheme(scheme); err != nil { 262 log.Fatalf("Failed to add client-go scheme to runtime client: (%v)", err) 263 } 264 dynamicDecoder := serializer.NewCodecFactory(scheme).UniversalDeserializer() 265 266 obj, _, err := dynamicDecoder.Decode(yamlSpec, nil, nil) 267 if err != nil { 268 return err 269 } 270 dep := &appsv1.Deployment{} 271 switch o := obj.(type) { 272 case *appsv1.Deployment: 273 dep = o 274 default: 275 return fmt.Errorf("error in replaceImage switch case; could not convert runtime.Object to deployment") 276 } 277 if len(dep.Spec.Template.Spec.Containers) != 1 { 278 return fmt.Errorf("cannot use `image` flag on namespaced manifest containing more than 1 container in the operator deployment") 279 } 280 dep.Spec.Template.Spec.Containers[0].Image = image 281 updatedYamlSpec, err := yaml.Marshal(dep) 282 if err != nil { 283 return fmt.Errorf("failed to convert deployment object back to yaml: %v", err) 284 } 285 newManifest = yamlutil.CombineManifests(newManifest, updatedYamlSpec) 286 } 287 if err := scanner.Err(); err != nil { 288 return fmt.Errorf("failed to scan %s: (%v)", manifestPath, err) 289 } 290 291 return ioutil.WriteFile(manifestPath, newManifest, fileutil.DefaultFileMode) 292 }