github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/test/custom/custom.go (about)

     1  /*
     2  Copyright 2021 The Skaffold Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package custom
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"os/exec"
    25  	"path/filepath"
    26  	"runtime"
    27  	"time"
    28  
    29  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/list"
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/event"
    32  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output"
    33  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    34  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    35  )
    36  
    37  var (
    38  	// for tests
    39  	testContext = retrieveTestContext
    40  )
    41  
    42  const Windows string = "windows"
    43  
    44  type Runner struct {
    45  	cfg        docker.Config
    46  	customTest latest.CustomTest
    47  	imageName  string
    48  	workspace  string
    49  }
    50  
    51  // New creates a new custom.Runner.
    52  func New(cfg docker.Config, imageName string, ws string, ct latest.CustomTest) (*Runner, error) {
    53  	return &Runner{
    54  		cfg:        cfg,
    55  		imageName:  imageName,
    56  		customTest: ct,
    57  		workspace:  ws,
    58  	}, nil
    59  }
    60  
    61  // Test is the entrypoint for running custom tests
    62  func (ct *Runner) Test(ctx context.Context, out io.Writer, imageTag string) error {
    63  	event.TestInProgress()
    64  	if err := ct.runCustomTest(ctx, out, imageTag); err != nil {
    65  		event.TestFailed(ct.imageName, err)
    66  		return err
    67  	}
    68  	event.TestComplete()
    69  	return nil
    70  }
    71  
    72  func (ct *Runner) runCustomTest(ctx context.Context, out io.Writer, imageTag string) error {
    73  	test := ct.customTest
    74  
    75  	// Expand command
    76  	command, err := util.ExpandEnvTemplate(test.Command, nil)
    77  	if err != nil {
    78  		return cmdRunParsingErr(test.Command, err)
    79  	}
    80  
    81  	if test.TimeoutSeconds <= 0 {
    82  		output.Default.Fprintf(out, "Running custom test command: %q\n", command)
    83  	} else {
    84  		output.Default.Fprintf(out, "Running custom test command: %q with timeout %d s\n", command, test.TimeoutSeconds)
    85  		newCtx, cancel := context.WithTimeout(ctx, time.Duration(test.TimeoutSeconds)*time.Second)
    86  
    87  		defer cancel()
    88  		ctx = newCtx
    89  	}
    90  
    91  	cmd, err := ct.retrieveCmd(ctx, out, command, imageTag)
    92  	if err != nil {
    93  		return cmdRunRetrieveErr(command, ct.imageName, err)
    94  	}
    95  
    96  	if err := util.RunCmd(ctx, cmd); err != nil {
    97  		if e, ok := err.(*exec.ExitError); ok {
    98  			// If the process exited by itself, just return the error
    99  			if e.Exited() {
   100  				output.Red.Fprintf(out, "Command finished with non-0 exit code.\n")
   101  				return cmdRunNonZeroExitErr(command, e)
   102  			}
   103  			// If the context is done, it has been killed by the exec.Command
   104  			select {
   105  			case <-ctx.Done():
   106  				if ctx.Err() == context.DeadlineExceeded {
   107  					output.Red.Fprintf(out, "Command timed out\n")
   108  					return cmdRunTimedoutErr(test.TimeoutSeconds, ctx.Err())
   109  				} else if ctx.Err() == context.Canceled {
   110  					output.Red.Fprintf(out, "Command cancelled\n")
   111  					return cmdRunCancelledErr(ctx.Err())
   112  				}
   113  				return cmdRunExecutionErr(ctx.Err())
   114  			default:
   115  				return cmdRunExited(e)
   116  			}
   117  		}
   118  		return cmdRunErr(err)
   119  	}
   120  	output.Green.Fprintf(out, "Command finished successfully.\n")
   121  
   122  	return nil
   123  }
   124  
   125  // TestDependencies returns dependencies listed for a custom test
   126  func (ct *Runner) TestDependencies(ctx context.Context) ([]string, error) {
   127  	test := ct.customTest
   128  
   129  	if test.Dependencies != nil {
   130  		switch {
   131  		case test.Dependencies.Command != "":
   132  			var cmd *exec.Cmd
   133  			// We evaluate the command with a shell so that it can contain env variables.
   134  			if runtime.GOOS == Windows {
   135  				cmd = exec.CommandContext(context.Background(), "cmd.exe", "/C", test.Dependencies.Command)
   136  			} else {
   137  				cmd = exec.CommandContext(context.Background(), "sh", "-c", test.Dependencies.Command)
   138  			}
   139  
   140  			output, err := util.RunCmdOut(ctx, cmd)
   141  			if err != nil {
   142  				return nil, gettingDependenciesCommandErr(test.Dependencies.Command, err)
   143  			}
   144  			var deps []string
   145  			if err := json.Unmarshal(output, &deps); err != nil {
   146  				return nil, dependencyOutputUnmarshallErr(test.Dependencies.Paths, err)
   147  			}
   148  			return deps, nil
   149  
   150  		case test.Dependencies.Paths != nil:
   151  			return list.Files(ct.workspace, test.Dependencies.Paths, test.Dependencies.Ignore)
   152  		}
   153  	}
   154  	return nil, nil
   155  }
   156  
   157  func (ct *Runner) retrieveCmd(ctx context.Context, out io.Writer, command string, imageTag string) (*exec.Cmd, error) {
   158  	var cmd *exec.Cmd
   159  	// We evaluate the command with a shell so that it can contain env variables.
   160  
   161  	if runtime.GOOS == Windows {
   162  		cmd = exec.CommandContext(ctx, "cmd.exe", "/C", command)
   163  	} else {
   164  		cmd = exec.CommandContext(ctx, "sh", "-c", command)
   165  	}
   166  	cmd.Stdout = out
   167  	cmd.Stderr = out
   168  
   169  	env, err := ct.getEnv(ctx, imageTag)
   170  	if err != nil {
   171  		return nil, fmt.Errorf("setting env variables: %w", err)
   172  	}
   173  	cmd.Env = env
   174  
   175  	dir, err := testContext(ct.workspace)
   176  	if err != nil {
   177  		return nil, fmt.Errorf("getting context for test: %w", err)
   178  	}
   179  	cmd.Dir = dir
   180  
   181  	return cmd, nil
   182  }
   183  
   184  func (ct *Runner) getEnv(ctx context.Context, imageTag string) ([]string, error) {
   185  	testContext, err := testContext(ct.workspace)
   186  	if err != nil {
   187  		return nil, fmt.Errorf("getting absolute path for test context: %w", err)
   188  	}
   189  
   190  	envs := []string{
   191  		fmt.Sprintf("%s=%s", "IMAGE", imageTag),
   192  		fmt.Sprintf("%s=%s", "TEST_CONTEXT", testContext),
   193  	}
   194  
   195  	envs = append(envs, util.OSEnviron()...)
   196  
   197  	// add minikube docker env vars to command env context if minikube cluster detected
   198  	localDaemon, err := docker.NewAPIClient(ctx, ct.cfg)
   199  	if err != nil {
   200  		return nil, fmt.Errorf("getting docker client information: %w", err)
   201  	}
   202  	envs = append(envs, localDaemon.ExtraEnv()...)
   203  
   204  	return envs, nil
   205  }
   206  
   207  func retrieveTestContext(workspace string) (string, error) {
   208  	return filepath.Abs(workspace)
   209  }