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  }