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 }