github.com/nektos/act@v0.2.63-0.20240520024548-8acde99bfa9c/pkg/container/host_environment.go (about) 1 package container 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "context" 7 "errors" 8 "fmt" 9 "io" 10 "io/fs" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "runtime" 15 "strings" 16 "time" 17 18 "github.com/go-git/go-billy/v5/helper/polyfill" 19 "github.com/go-git/go-billy/v5/osfs" 20 "github.com/go-git/go-git/v5/plumbing/format/gitignore" 21 "golang.org/x/term" 22 23 "github.com/nektos/act/pkg/common" 24 "github.com/nektos/act/pkg/filecollector" 25 "github.com/nektos/act/pkg/lookpath" 26 ) 27 28 type HostEnvironment struct { 29 Path string 30 TmpDir string 31 ToolCache string 32 Workdir string 33 ActPath string 34 CleanUp func() 35 StdOut io.Writer 36 } 37 38 func (e *HostEnvironment) Create(_ []string, _ []string) common.Executor { 39 return func(ctx context.Context) error { 40 return nil 41 } 42 } 43 44 func (e *HostEnvironment) Close() common.Executor { 45 return func(ctx context.Context) error { 46 return nil 47 } 48 } 49 50 func (e *HostEnvironment) Copy(destPath string, files ...*FileEntry) common.Executor { 51 return func(ctx context.Context) error { 52 for _, f := range files { 53 if err := os.MkdirAll(filepath.Dir(filepath.Join(destPath, f.Name)), 0o777); err != nil { 54 return err 55 } 56 if err := os.WriteFile(filepath.Join(destPath, f.Name), []byte(f.Body), fs.FileMode(f.Mode)); err != nil { 57 return err 58 } 59 } 60 return nil 61 } 62 } 63 64 func (e *HostEnvironment) CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error { 65 if err := os.RemoveAll(destPath); err != nil { 66 return err 67 } 68 tr := tar.NewReader(tarStream) 69 cp := &filecollector.CopyCollector{ 70 DstDir: destPath, 71 } 72 for { 73 ti, err := tr.Next() 74 if errors.Is(err, io.EOF) { 75 return nil 76 } else if err != nil { 77 return err 78 } 79 if ti.FileInfo().IsDir() { 80 continue 81 } 82 if ctx.Err() != nil { 83 return fmt.Errorf("CopyTarStream has been cancelled") 84 } 85 if err := cp.WriteFile(ti.Name, ti.FileInfo(), ti.Linkname, tr); err != nil { 86 return err 87 } 88 } 89 } 90 91 func (e *HostEnvironment) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor { 92 return func(ctx context.Context) error { 93 logger := common.Logger(ctx) 94 srcPrefix := filepath.Dir(srcPath) 95 if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) { 96 srcPrefix += string(filepath.Separator) 97 } 98 logger.Debugf("Stripping prefix:%s src:%s", srcPrefix, srcPath) 99 var ignorer gitignore.Matcher 100 if useGitIgnore { 101 ps, err := gitignore.ReadPatterns(polyfill.New(osfs.New(srcPath)), nil) 102 if err != nil { 103 logger.Debugf("Error loading .gitignore: %v", err) 104 } 105 106 ignorer = gitignore.NewMatcher(ps) 107 } 108 fc := &filecollector.FileCollector{ 109 Fs: &filecollector.DefaultFs{}, 110 Ignorer: ignorer, 111 SrcPath: srcPath, 112 SrcPrefix: srcPrefix, 113 Handler: &filecollector.CopyCollector{ 114 DstDir: destPath, 115 }, 116 } 117 return filepath.Walk(srcPath, fc.CollectFiles(ctx, []string{})) 118 } 119 } 120 121 func (e *HostEnvironment) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) { 122 buf := &bytes.Buffer{} 123 tw := tar.NewWriter(buf) 124 defer tw.Close() 125 srcPath = filepath.Clean(srcPath) 126 fi, err := os.Lstat(srcPath) 127 if err != nil { 128 return nil, err 129 } 130 tc := &filecollector.TarCollector{ 131 TarWriter: tw, 132 } 133 if fi.IsDir() { 134 srcPrefix := srcPath 135 if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) { 136 srcPrefix += string(filepath.Separator) 137 } 138 fc := &filecollector.FileCollector{ 139 Fs: &filecollector.DefaultFs{}, 140 SrcPath: srcPath, 141 SrcPrefix: srcPrefix, 142 Handler: tc, 143 } 144 err = filepath.Walk(srcPath, fc.CollectFiles(ctx, []string{})) 145 if err != nil { 146 return nil, err 147 } 148 } else { 149 var f io.ReadCloser 150 var linkname string 151 if fi.Mode()&fs.ModeSymlink != 0 { 152 linkname, err = os.Readlink(srcPath) 153 if err != nil { 154 return nil, err 155 } 156 } else { 157 f, err = os.Open(srcPath) 158 if err != nil { 159 return nil, err 160 } 161 defer f.Close() 162 } 163 err := tc.WriteFile(fi.Name(), fi, linkname, f) 164 if err != nil { 165 return nil, err 166 } 167 } 168 return io.NopCloser(buf), nil 169 } 170 171 func (e *HostEnvironment) Pull(_ bool) common.Executor { 172 return func(ctx context.Context) error { 173 return nil 174 } 175 } 176 177 func (e *HostEnvironment) Start(_ bool) common.Executor { 178 return func(ctx context.Context) error { 179 return nil 180 } 181 } 182 183 type ptyWriter struct { 184 Out io.Writer 185 AutoStop bool 186 dirtyLine bool 187 } 188 189 func (w *ptyWriter) Write(buf []byte) (int, error) { 190 if w.AutoStop && len(buf) > 0 && buf[len(buf)-1] == 4 { 191 n, err := w.Out.Write(buf[:len(buf)-1]) 192 if err != nil { 193 return n, err 194 } 195 if w.dirtyLine || len(buf) > 1 && buf[len(buf)-2] != '\n' { 196 _, _ = w.Out.Write([]byte("\n")) 197 return n, io.EOF 198 } 199 return n, io.EOF 200 } 201 w.dirtyLine = strings.LastIndex(string(buf), "\n") < len(buf)-1 202 return w.Out.Write(buf) 203 } 204 205 type localEnv struct { 206 env map[string]string 207 } 208 209 func (l *localEnv) Getenv(name string) string { 210 if runtime.GOOS == "windows" { 211 for k, v := range l.env { 212 if strings.EqualFold(name, k) { 213 return v 214 } 215 } 216 return "" 217 } 218 return l.env[name] 219 } 220 221 func lookupPathHost(cmd string, env map[string]string, writer io.Writer) (string, error) { 222 f, err := lookpath.LookPath2(cmd, &localEnv{env: env}) 223 if err != nil { 224 err := "Cannot find: " + fmt.Sprint(cmd) + " in PATH" 225 if _, _err := writer.Write([]byte(err + "\n")); _err != nil { 226 return "", fmt.Errorf("%v: %w", err, _err) 227 } 228 return "", errors.New(err) 229 } 230 return f, nil 231 } 232 233 func setupPty(cmd *exec.Cmd, cmdline string) (*os.File, *os.File, error) { 234 ppty, tty, err := openPty() 235 if err != nil { 236 return nil, nil, err 237 } 238 if term.IsTerminal(int(tty.Fd())) { 239 _, err := term.MakeRaw(int(tty.Fd())) 240 if err != nil { 241 ppty.Close() 242 tty.Close() 243 return nil, nil, err 244 } 245 } 246 cmd.Stdin = tty 247 cmd.Stdout = tty 248 cmd.Stderr = tty 249 cmd.SysProcAttr = getSysProcAttr(cmdline, true) 250 return ppty, tty, nil 251 } 252 253 func writeKeepAlive(ppty io.Writer) { 254 c := 1 255 var err error 256 for c == 1 && err == nil { 257 c, err = ppty.Write([]byte{4}) 258 <-time.After(time.Second) 259 } 260 } 261 262 func copyPtyOutput(writer io.Writer, ppty io.Reader, finishLog context.CancelFunc) { 263 defer func() { 264 finishLog() 265 }() 266 if _, err := io.Copy(writer, ppty); err != nil { 267 return 268 } 269 } 270 271 func (e *HostEnvironment) UpdateFromImageEnv(_ *map[string]string) common.Executor { 272 return func(ctx context.Context) error { 273 return nil 274 } 275 } 276 277 func getEnvListFromMap(env map[string]string) []string { 278 envList := make([]string, 0) 279 for k, v := range env { 280 envList = append(envList, fmt.Sprintf("%s=%s", k, v)) 281 } 282 return envList 283 } 284 285 func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline string, env map[string]string, _, workdir string) error { 286 envList := getEnvListFromMap(env) 287 var wd string 288 if workdir != "" { 289 if filepath.IsAbs(workdir) { 290 wd = workdir 291 } else { 292 wd = filepath.Join(e.Path, workdir) 293 } 294 } else { 295 wd = e.Path 296 } 297 f, err := lookupPathHost(command[0], env, e.StdOut) 298 if err != nil { 299 return err 300 } 301 cmd := exec.CommandContext(ctx, f) 302 cmd.Path = f 303 cmd.Args = command 304 cmd.Stdin = nil 305 cmd.Stdout = e.StdOut 306 cmd.Env = envList 307 cmd.Stderr = e.StdOut 308 cmd.Dir = wd 309 cmd.SysProcAttr = getSysProcAttr(cmdline, false) 310 var ppty *os.File 311 var tty *os.File 312 defer func() { 313 if ppty != nil { 314 ppty.Close() 315 } 316 if tty != nil { 317 tty.Close() 318 } 319 }() 320 if true /* allocate Terminal */ { 321 var err error 322 ppty, tty, err = setupPty(cmd, cmdline) 323 if err != nil { 324 common.Logger(ctx).Debugf("Failed to setup Pty %v\n", err.Error()) 325 } 326 } 327 writer := &ptyWriter{Out: e.StdOut} 328 logctx, finishLog := context.WithCancel(context.Background()) 329 if ppty != nil { 330 go copyPtyOutput(writer, ppty, finishLog) 331 } else { 332 finishLog() 333 } 334 if ppty != nil { 335 go writeKeepAlive(ppty) 336 } 337 err = cmd.Run() 338 if err != nil { 339 return err 340 } 341 if tty != nil { 342 writer.AutoStop = true 343 if _, err := tty.Write([]byte("\x04")); err != nil { 344 common.Logger(ctx).Debug("Failed to write EOT") 345 } 346 } 347 <-logctx.Done() 348 349 if ppty != nil { 350 ppty.Close() 351 ppty = nil 352 } 353 return err 354 } 355 356 func (e *HostEnvironment) Exec(command []string /*cmdline string, */, env map[string]string, user, workdir string) common.Executor { 357 return e.ExecWithCmdLine(command, "", env, user, workdir) 358 } 359 360 func (e *HostEnvironment) ExecWithCmdLine(command []string, cmdline string, env map[string]string, user, workdir string) common.Executor { 361 return func(ctx context.Context) error { 362 if err := e.exec(ctx, command, cmdline, env, user, workdir); err != nil { 363 select { 364 case <-ctx.Done(): 365 return fmt.Errorf("this step has been cancelled: %w", err) 366 default: 367 return err 368 } 369 } 370 return nil 371 } 372 } 373 374 func (e *HostEnvironment) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor { 375 return parseEnvFile(e, srcPath, env) 376 } 377 378 func (e *HostEnvironment) Remove() common.Executor { 379 return func(ctx context.Context) error { 380 if e.CleanUp != nil { 381 e.CleanUp() 382 } 383 return os.RemoveAll(e.Path) 384 } 385 } 386 387 func (e *HostEnvironment) ToContainerPath(path string) string { 388 if bp, err := filepath.Rel(e.Workdir, path); err != nil { 389 return filepath.Join(e.Path, bp) 390 } else if filepath.Clean(e.Workdir) == filepath.Clean(path) { 391 return e.Path 392 } 393 return path 394 } 395 396 func (e *HostEnvironment) GetActPath() string { 397 actPath := e.ActPath 398 if runtime.GOOS == "windows" { 399 actPath = strings.ReplaceAll(actPath, "\\", "/") 400 } 401 return actPath 402 } 403 404 func (*HostEnvironment) GetPathVariableName() string { 405 if runtime.GOOS == "plan9" { 406 return "path" 407 } else if runtime.GOOS == "windows" { 408 return "Path" // Actually we need a case insensitive map 409 } 410 return "PATH" 411 } 412 413 func (e *HostEnvironment) DefaultPathVariable() string { 414 v, _ := os.LookupEnv(e.GetPathVariableName()) 415 return v 416 } 417 418 func (*HostEnvironment) JoinPathVariable(paths ...string) string { 419 return strings.Join(paths, string(filepath.ListSeparator)) 420 } 421 422 // Reference for Arch values for runner.arch 423 // https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context 424 func goArchToActionArch(arch string) string { 425 archMapper := map[string]string{ 426 "x86_64": "X64", 427 "386": "X86", 428 "aarch64": "ARM64", 429 } 430 if arch, ok := archMapper[arch]; ok { 431 return arch 432 } 433 return arch 434 } 435 436 func goOsToActionOs(os string) string { 437 osMapper := map[string]string{ 438 "darwin": "macOS", 439 } 440 if os, ok := osMapper[os]; ok { 441 return os 442 } 443 return os 444 } 445 446 func (e *HostEnvironment) GetRunnerContext(_ context.Context) map[string]interface{} { 447 return map[string]interface{}{ 448 "os": goOsToActionOs(runtime.GOOS), 449 "arch": goArchToActionArch(runtime.GOARCH), 450 "temp": e.TmpDir, 451 "tool_cache": e.ToolCache, 452 } 453 } 454 455 func (e *HostEnvironment) ReplaceLogWriter(stdout io.Writer, _ io.Writer) (io.Writer, io.Writer) { 456 org := e.StdOut 457 e.StdOut = stdout 458 return org, org 459 } 460 461 func (*HostEnvironment) IsEnvironmentCaseInsensitive() bool { 462 return runtime.GOOS == "windows" 463 }