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  }