knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/test/upgrade/shell/executor.go (about)

     1  /*
     2  Copyright 2020 The Knative 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 shell
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"os"
    23  	"os/exec"
    24  	"strings"
    25  	"time"
    26  )
    27  
    28  const (
    29  	defaultLabelOut = "[OUT]"
    30  	defaultLabelErr = "[ERR]"
    31  	executeMode     = 0o700
    32  )
    33  
    34  // NewExecutor creates a new executor.
    35  func NewExecutor(t TestingT, loc ProjectLocation, opts ...Option) Executor {
    36  	config := &ExecutorConfig{
    37  		ProjectLocation: loc,
    38  		Streams:         testingTStreams(t),
    39  	}
    40  	for _, opt := range opts {
    41  		opt(config)
    42  	}
    43  	configureDefaultValues(config)
    44  	return &streamingExecutor{
    45  		ExecutorConfig: *config,
    46  	}
    47  }
    48  
    49  // testingTStreams returns Streams which writes to test log.
    50  func testingTStreams(t TestingT) Streams {
    51  	tWriter := testingWriter{t: t}
    52  	return Streams{
    53  		Out: tWriter,
    54  		Err: tWriter,
    55  	}
    56  }
    57  
    58  // RunScript executes a shell script with args.
    59  func (s *streamingExecutor) RunScript(script Script, args ...string) error {
    60  	cnt := script.scriptContent(s.ProjectLocation, args)
    61  	return withTempScript(cnt, func(bin string) error {
    62  		return stream(bin, s.ExecutorConfig, script.Label)
    63  	})
    64  }
    65  
    66  // RunFunction executes a shell function with args.
    67  func (s *streamingExecutor) RunFunction(fn Function, args ...string) error {
    68  	cnt := fn.scriptContent(s.ProjectLocation, args)
    69  	return withTempScript(cnt, func(bin string) error {
    70  		return stream(bin, s.ExecutorConfig, fn.Label)
    71  	})
    72  }
    73  
    74  type streamingExecutor struct {
    75  	ExecutorConfig
    76  }
    77  
    78  func configureDefaultValues(config *ExecutorConfig) {
    79  	if config.LabelOut == "" {
    80  		config.LabelOut = defaultLabelOut
    81  	}
    82  	if config.LabelErr == "" {
    83  		config.LabelErr = defaultLabelErr
    84  	}
    85  	if config.Environ == nil {
    86  		config.Environ = os.Environ()
    87  	}
    88  	if !config.SkipDate && config.DateFormat == "" {
    89  		config.DateFormat = time.StampMilli
    90  	}
    91  	if config.PrefixFunc == nil {
    92  		config.PrefixFunc = defaultPrefixFunc
    93  	}
    94  }
    95  
    96  func stream(bin string, cfg ExecutorConfig, label string) error {
    97  	c := exec.Command(bin)
    98  	c.Env = cfg.Environ
    99  	c.Stdout = NewPrefixer(cfg.Out, prefixFunc(StreamTypeOut, label, cfg))
   100  	c.Stderr = NewPrefixer(cfg.Err, prefixFunc(StreamTypeErr, label, cfg))
   101  	return c.Run()
   102  }
   103  
   104  func prefixFunc(st StreamType, label string, cfg ExecutorConfig) func() string {
   105  	return func() string {
   106  		return cfg.PrefixFunc(st, label, cfg)
   107  	}
   108  }
   109  
   110  func defaultPrefixFunc(st StreamType, label string, cfg ExecutorConfig) string {
   111  	sep := " "
   112  	var buf []string
   113  	if !cfg.SkipDate {
   114  		dt := time.Now().Format(cfg.DateFormat)
   115  		buf = append(buf, dt)
   116  	}
   117  	buf = append(buf, label)
   118  	switch st {
   119  	case StreamTypeOut:
   120  		buf = append(buf, cfg.LabelOut)
   121  	case StreamTypeErr:
   122  		buf = append(buf, cfg.LabelErr)
   123  	}
   124  	return strings.Join(buf, sep) + sep
   125  }
   126  
   127  func withTempScript(contents string, fn func(bin string) error) error {
   128  	tmpfile, err := os.CreateTemp("", "shellout-*.sh")
   129  	if err != nil {
   130  		return err
   131  	}
   132  	_, err = tmpfile.WriteString(contents)
   133  	if err != nil {
   134  		return err
   135  	}
   136  	err = tmpfile.Chmod(executeMode)
   137  	if err != nil {
   138  		return err
   139  	}
   140  	err = tmpfile.Close()
   141  	if err != nil {
   142  		return err
   143  	}
   144  	defer func() {
   145  		// clean up
   146  		_ = os.Remove(tmpfile.Name())
   147  	}()
   148  
   149  	return fn(tmpfile.Name())
   150  }
   151  
   152  func (fn *Function) scriptContent(location ProjectLocation, args []string) string {
   153  	return fmt.Sprintf(`#!/usr/bin/env bash
   154  
   155  set -Eeuo pipefail
   156  
   157  cd "%s"
   158  source %s
   159  
   160  %s %s
   161  `, location.RootPath(), fn.ScriptPath, fn.FunctionName, quoteArgs(args))
   162  }
   163  
   164  func (sc *Script) scriptContent(location ProjectLocation, args []string) string {
   165  	return fmt.Sprintf(`#!/usr/bin/env bash
   166  
   167  set -Eeuo pipefail
   168  
   169  cd "%s"
   170  %s %s
   171  `, location.RootPath(), sc.ScriptPath, quoteArgs(args))
   172  }
   173  
   174  func quoteArgs(args []string) string {
   175  	quoted := make([]string, len(args))
   176  	for i, arg := range args {
   177  		quoted[i] = "\"" + strings.ReplaceAll(arg, "\"", "\\\"") + "\""
   178  	}
   179  	return strings.Join(quoted, " ")
   180  }
   181  
   182  func (w testingWriter) Write(p []byte) (n int, err error) {
   183  	n = len(p)
   184  
   185  	// Strip trailing newline because t.Log always adds one.
   186  	p = bytes.TrimRight(p, "\n")
   187  
   188  	for _, line := range strings.Split(string(p), "\n") {
   189  		w.t.Logf(line)
   190  	}
   191  
   192  	return n, nil
   193  }