github.com/nektos/act@v0.2.63/pkg/container/docker_run.go (about) 1 //go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd)) 2 3 package container 4 5 import ( 6 "archive/tar" 7 "bytes" 8 "context" 9 "errors" 10 "fmt" 11 "io" 12 "os" 13 "path/filepath" 14 "regexp" 15 "runtime" 16 "strconv" 17 "strings" 18 19 "github.com/Masterminds/semver" 20 "github.com/docker/cli/cli/connhelper" 21 "github.com/docker/docker/api/types" 22 "github.com/docker/docker/api/types/container" 23 "github.com/docker/docker/api/types/mount" 24 "github.com/docker/docker/api/types/network" 25 "github.com/docker/docker/api/types/system" 26 "github.com/docker/docker/client" 27 "github.com/docker/docker/pkg/stdcopy" 28 "github.com/go-git/go-billy/v5/helper/polyfill" 29 "github.com/go-git/go-billy/v5/osfs" 30 "github.com/go-git/go-git/v5/plumbing/format/gitignore" 31 "github.com/imdario/mergo" 32 "github.com/joho/godotenv" 33 "github.com/kballard/go-shellquote" 34 specs "github.com/opencontainers/image-spec/specs-go/v1" 35 "github.com/spf13/pflag" 36 "golang.org/x/term" 37 38 "github.com/nektos/act/pkg/common" 39 "github.com/nektos/act/pkg/filecollector" 40 ) 41 42 // NewContainer creates a reference to a container 43 func NewContainer(input *NewContainerInput) ExecutionsEnvironment { 44 cr := new(containerReference) 45 cr.input = input 46 return cr 47 } 48 49 // supportsContainerImagePlatform returns true if the underlying Docker server 50 // API version is 1.41 and beyond 51 func supportsContainerImagePlatform(ctx context.Context, cli client.APIClient) bool { 52 logger := common.Logger(ctx) 53 ver, err := cli.ServerVersion(ctx) 54 if err != nil { 55 logger.Panicf("Failed to get Docker API Version: %s", err) 56 return false 57 } 58 sv, err := semver.NewVersion(ver.APIVersion) 59 if err != nil { 60 logger.Panicf("Failed to unmarshal Docker Version: %s", err) 61 return false 62 } 63 constraint, _ := semver.NewConstraint(">= 1.41") 64 return constraint.Check(sv) 65 } 66 67 func (cr *containerReference) Create(capAdd []string, capDrop []string) common.Executor { 68 return common. 69 NewInfoExecutor("%sdocker create image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode). 70 Then( 71 common.NewPipelineExecutor( 72 cr.connect(), 73 cr.find(), 74 cr.create(capAdd, capDrop), 75 ).IfNot(common.Dryrun), 76 ) 77 } 78 79 func (cr *containerReference) Start(attach bool) common.Executor { 80 return common. 81 NewInfoExecutor("%sdocker run image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode). 82 Then( 83 common.NewPipelineExecutor( 84 cr.connect(), 85 cr.find(), 86 cr.attach().IfBool(attach), 87 cr.start(), 88 cr.wait().IfBool(attach), 89 cr.tryReadUID(), 90 cr.tryReadGID(), 91 func(ctx context.Context) error { 92 // If this fails, then folders have wrong permissions on non root container 93 if cr.UID != 0 || cr.GID != 0 { 94 _ = cr.Exec([]string{"chown", "-R", fmt.Sprintf("%d:%d", cr.UID, cr.GID), cr.input.WorkingDir}, nil, "0", "")(ctx) 95 } 96 return nil 97 }, 98 ).IfNot(common.Dryrun), 99 ) 100 } 101 102 func (cr *containerReference) Pull(forcePull bool) common.Executor { 103 return common. 104 NewInfoExecutor("%sdocker pull image=%s platform=%s username=%s forcePull=%t", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Username, forcePull). 105 Then( 106 NewDockerPullExecutor(NewDockerPullExecutorInput{ 107 Image: cr.input.Image, 108 ForcePull: forcePull, 109 Platform: cr.input.Platform, 110 Username: cr.input.Username, 111 Password: cr.input.Password, 112 }), 113 ) 114 } 115 116 func (cr *containerReference) Copy(destPath string, files ...*FileEntry) common.Executor { 117 return common.NewPipelineExecutor( 118 cr.connect(), 119 cr.find(), 120 cr.copyContent(destPath, files...), 121 ).IfNot(common.Dryrun) 122 } 123 124 func (cr *containerReference) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor { 125 return common.NewPipelineExecutor( 126 common.NewInfoExecutor("%sdocker cp src=%s dst=%s", logPrefix, srcPath, destPath), 127 cr.copyDir(destPath, srcPath, useGitIgnore), 128 func(ctx context.Context) error { 129 // If this fails, then folders have wrong permissions on non root container 130 if cr.UID != 0 || cr.GID != 0 { 131 _ = cr.Exec([]string{"chown", "-R", fmt.Sprintf("%d:%d", cr.UID, cr.GID), destPath}, nil, "0", "")(ctx) 132 } 133 return nil 134 }, 135 ).IfNot(common.Dryrun) 136 } 137 138 func (cr *containerReference) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) { 139 if common.Dryrun(ctx) { 140 return nil, fmt.Errorf("DRYRUN is not supported in GetContainerArchive") 141 } 142 a, _, err := cr.cli.CopyFromContainer(ctx, cr.id, srcPath) 143 return a, err 144 } 145 146 func (cr *containerReference) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor { 147 return parseEnvFile(cr, srcPath, env).IfNot(common.Dryrun) 148 } 149 150 func (cr *containerReference) UpdateFromImageEnv(env *map[string]string) common.Executor { 151 return cr.extractFromImageEnv(env).IfNot(common.Dryrun) 152 } 153 154 func (cr *containerReference) Exec(command []string, env map[string]string, user, workdir string) common.Executor { 155 return common.NewPipelineExecutor( 156 common.NewInfoExecutor("%sdocker exec cmd=[%s] user=%s workdir=%s", logPrefix, strings.Join(command, " "), user, workdir), 157 cr.connect(), 158 cr.find(), 159 cr.exec(command, env, user, workdir), 160 ).IfNot(common.Dryrun) 161 } 162 163 func (cr *containerReference) Remove() common.Executor { 164 return common.NewPipelineExecutor( 165 cr.connect(), 166 cr.find(), 167 ).Finally( 168 cr.remove(), 169 ).IfNot(common.Dryrun) 170 } 171 172 func (cr *containerReference) ReplaceLogWriter(stdout io.Writer, stderr io.Writer) (io.Writer, io.Writer) { 173 out := cr.input.Stdout 174 err := cr.input.Stderr 175 176 cr.input.Stdout = stdout 177 cr.input.Stderr = stderr 178 179 return out, err 180 } 181 182 type containerReference struct { 183 cli client.APIClient 184 id string 185 input *NewContainerInput 186 UID int 187 GID int 188 LinuxContainerEnvironmentExtensions 189 } 190 191 func GetDockerClient(ctx context.Context) (cli client.APIClient, err error) { 192 dockerHost := os.Getenv("DOCKER_HOST") 193 194 if strings.HasPrefix(dockerHost, "ssh://") { 195 var helper *connhelper.ConnectionHelper 196 197 helper, err = connhelper.GetConnectionHelper(dockerHost) 198 if err != nil { 199 return nil, err 200 } 201 cli, err = client.NewClientWithOpts( 202 client.WithHost(helper.Host), 203 client.WithDialContext(helper.Dialer), 204 ) 205 } else { 206 cli, err = client.NewClientWithOpts(client.FromEnv) 207 } 208 if err != nil { 209 return nil, fmt.Errorf("failed to connect to docker daemon: %w", err) 210 } 211 cli.NegotiateAPIVersion(ctx) 212 213 return cli, nil 214 } 215 216 func GetHostInfo(ctx context.Context) (info system.Info, err error) { 217 var cli client.APIClient 218 cli, err = GetDockerClient(ctx) 219 if err != nil { 220 return info, err 221 } 222 defer cli.Close() 223 224 info, err = cli.Info(ctx) 225 if err != nil { 226 return info, err 227 } 228 229 return info, nil 230 } 231 232 // Arch fetches values from docker info and translates architecture to 233 // GitHub actions compatible runner.arch values 234 // https://github.com/github/docs/blob/main/data/reusables/actions/runner-arch-description.md 235 func RunnerArch(ctx context.Context) string { 236 info, err := GetHostInfo(ctx) 237 if err != nil { 238 return "" 239 } 240 241 archMapper := map[string]string{ 242 "x86_64": "X64", 243 "amd64": "X64", 244 "386": "X86", 245 "aarch64": "ARM64", 246 "arm64": "ARM64", 247 } 248 if arch, ok := archMapper[info.Architecture]; ok { 249 return arch 250 } 251 return info.Architecture 252 } 253 254 func (cr *containerReference) connect() common.Executor { 255 return func(ctx context.Context) error { 256 if cr.cli != nil { 257 return nil 258 } 259 cli, err := GetDockerClient(ctx) 260 if err != nil { 261 return err 262 } 263 cr.cli = cli 264 return nil 265 } 266 } 267 268 func (cr *containerReference) Close() common.Executor { 269 return func(ctx context.Context) error { 270 if cr.cli != nil { 271 err := cr.cli.Close() 272 cr.cli = nil 273 if err != nil { 274 return fmt.Errorf("failed to close client: %w", err) 275 } 276 } 277 return nil 278 } 279 } 280 281 func (cr *containerReference) find() common.Executor { 282 return func(ctx context.Context) error { 283 if cr.id != "" { 284 return nil 285 } 286 containers, err := cr.cli.ContainerList(ctx, container.ListOptions{ 287 All: true, 288 }) 289 if err != nil { 290 return fmt.Errorf("failed to list containers: %w", err) 291 } 292 293 for _, c := range containers { 294 for _, name := range c.Names { 295 if name[1:] == cr.input.Name { 296 cr.id = c.ID 297 return nil 298 } 299 } 300 } 301 302 cr.id = "" 303 return nil 304 } 305 } 306 307 func (cr *containerReference) remove() common.Executor { 308 return func(ctx context.Context) error { 309 if cr.id == "" { 310 return nil 311 } 312 313 logger := common.Logger(ctx) 314 err := cr.cli.ContainerRemove(ctx, cr.id, container.RemoveOptions{ 315 RemoveVolumes: true, 316 Force: true, 317 }) 318 if err != nil { 319 logger.Error(fmt.Errorf("failed to remove container: %w", err)) 320 } 321 322 logger.Debugf("Removed container: %v", cr.id) 323 cr.id = "" 324 return nil 325 } 326 } 327 328 func (cr *containerReference) mergeContainerConfigs(ctx context.Context, config *container.Config, hostConfig *container.HostConfig) (*container.Config, *container.HostConfig, error) { 329 logger := common.Logger(ctx) 330 input := cr.input 331 332 if input.Options == "" { 333 return config, hostConfig, nil 334 } 335 336 // parse configuration from CLI container.options 337 flags := pflag.NewFlagSet("container_flags", pflag.ContinueOnError) 338 copts := addFlags(flags) 339 340 optionsArgs, err := shellquote.Split(input.Options) 341 if err != nil { 342 return nil, nil, fmt.Errorf("Cannot split container options: '%s': '%w'", input.Options, err) 343 } 344 345 err = flags.Parse(optionsArgs) 346 if err != nil { 347 return nil, nil, fmt.Errorf("Cannot parse container options: '%s': '%w'", input.Options, err) 348 } 349 350 if len(copts.netMode.Value()) == 0 { 351 if err = copts.netMode.Set(cr.input.NetworkMode); err != nil { 352 return nil, nil, fmt.Errorf("Cannot parse networkmode=%s. This is an internal error and should not happen: '%w'", cr.input.NetworkMode, err) 353 } 354 } 355 356 containerConfig, err := parse(flags, copts, runtime.GOOS) 357 if err != nil { 358 return nil, nil, fmt.Errorf("Cannot process container options: '%s': '%w'", input.Options, err) 359 } 360 361 logger.Debugf("Custom container.Config from options ==> %+v", containerConfig.Config) 362 363 err = mergo.Merge(config, containerConfig.Config, mergo.WithOverride) 364 if err != nil { 365 return nil, nil, fmt.Errorf("Cannot merge container.Config options: '%s': '%w'", input.Options, err) 366 } 367 logger.Debugf("Merged container.Config ==> %+v", config) 368 369 logger.Debugf("Custom container.HostConfig from options ==> %+v", containerConfig.HostConfig) 370 371 hostConfig.Binds = append(hostConfig.Binds, containerConfig.HostConfig.Binds...) 372 hostConfig.Mounts = append(hostConfig.Mounts, containerConfig.HostConfig.Mounts...) 373 binds := hostConfig.Binds 374 mounts := hostConfig.Mounts 375 err = mergo.Merge(hostConfig, containerConfig.HostConfig, mergo.WithOverride) 376 if err != nil { 377 return nil, nil, fmt.Errorf("Cannot merge container.HostConfig options: '%s': '%w'", input.Options, err) 378 } 379 hostConfig.Binds = binds 380 hostConfig.Mounts = mounts 381 logger.Debugf("Merged container.HostConfig ==> %+v", hostConfig) 382 383 return config, hostConfig, nil 384 } 385 386 func (cr *containerReference) create(capAdd []string, capDrop []string) common.Executor { 387 return func(ctx context.Context) error { 388 if cr.id != "" { 389 return nil 390 } 391 logger := common.Logger(ctx) 392 isTerminal := term.IsTerminal(int(os.Stdout.Fd())) 393 input := cr.input 394 395 config := &container.Config{ 396 Image: input.Image, 397 WorkingDir: input.WorkingDir, 398 Env: input.Env, 399 ExposedPorts: input.ExposedPorts, 400 Tty: isTerminal, 401 } 402 logger.Debugf("Common container.Config ==> %+v", config) 403 404 if len(input.Cmd) != 0 { 405 config.Cmd = input.Cmd 406 } 407 408 if len(input.Entrypoint) != 0 { 409 config.Entrypoint = input.Entrypoint 410 } 411 412 mounts := make([]mount.Mount, 0) 413 for mountSource, mountTarget := range input.Mounts { 414 mounts = append(mounts, mount.Mount{ 415 Type: mount.TypeVolume, 416 Source: mountSource, 417 Target: mountTarget, 418 }) 419 } 420 421 var platSpecs *specs.Platform 422 if supportsContainerImagePlatform(ctx, cr.cli) && cr.input.Platform != "" { 423 desiredPlatform := strings.SplitN(cr.input.Platform, `/`, 2) 424 425 if len(desiredPlatform) != 2 { 426 return fmt.Errorf("incorrect container platform option '%s'", cr.input.Platform) 427 } 428 429 platSpecs = &specs.Platform{ 430 Architecture: desiredPlatform[1], 431 OS: desiredPlatform[0], 432 } 433 } 434 435 hostConfig := &container.HostConfig{ 436 CapAdd: capAdd, 437 CapDrop: capDrop, 438 Binds: input.Binds, 439 Mounts: mounts, 440 NetworkMode: container.NetworkMode(input.NetworkMode), 441 Privileged: input.Privileged, 442 UsernsMode: container.UsernsMode(input.UsernsMode), 443 PortBindings: input.PortBindings, 444 } 445 logger.Debugf("Common container.HostConfig ==> %+v", hostConfig) 446 447 config, hostConfig, err := cr.mergeContainerConfigs(ctx, config, hostConfig) 448 if err != nil { 449 return err 450 } 451 452 var networkingConfig *network.NetworkingConfig 453 logger.Debugf("input.NetworkAliases ==> %v", input.NetworkAliases) 454 n := hostConfig.NetworkMode 455 // IsUserDefined and IsHost are broken on windows 456 if n.IsUserDefined() && n != "host" && len(input.NetworkAliases) > 0 { 457 endpointConfig := &network.EndpointSettings{ 458 Aliases: input.NetworkAliases, 459 } 460 networkingConfig = &network.NetworkingConfig{ 461 EndpointsConfig: map[string]*network.EndpointSettings{ 462 input.NetworkMode: endpointConfig, 463 }, 464 } 465 } 466 467 resp, err := cr.cli.ContainerCreate(ctx, config, hostConfig, networkingConfig, platSpecs, input.Name) 468 if err != nil { 469 return fmt.Errorf("failed to create container: '%w'", err) 470 } 471 472 logger.Debugf("Created container name=%s id=%v from image %v (platform: %s)", input.Name, resp.ID, input.Image, input.Platform) 473 logger.Debugf("ENV ==> %v", input.Env) 474 475 cr.id = resp.ID 476 return nil 477 } 478 } 479 480 func (cr *containerReference) extractFromImageEnv(env *map[string]string) common.Executor { 481 envMap := *env 482 return func(ctx context.Context) error { 483 logger := common.Logger(ctx) 484 485 inspect, _, err := cr.cli.ImageInspectWithRaw(ctx, cr.input.Image) 486 if err != nil { 487 logger.Error(err) 488 return fmt.Errorf("inspect image: %w", err) 489 } 490 491 if inspect.Config == nil { 492 return nil 493 } 494 495 imageEnv, err := godotenv.Unmarshal(strings.Join(inspect.Config.Env, "\n")) 496 if err != nil { 497 logger.Error(err) 498 return fmt.Errorf("unmarshal image env: %w", err) 499 } 500 501 for k, v := range imageEnv { 502 if k == "PATH" { 503 if envMap[k] == "" { 504 envMap[k] = v 505 } else { 506 envMap[k] += `:` + v 507 } 508 } else if envMap[k] == "" { 509 envMap[k] = v 510 } 511 } 512 513 env = &envMap 514 return nil 515 } 516 } 517 518 func (cr *containerReference) exec(cmd []string, env map[string]string, user, workdir string) common.Executor { 519 return func(ctx context.Context) error { 520 logger := common.Logger(ctx) 521 // Fix slashes when running on Windows 522 if runtime.GOOS == "windows" { 523 var newCmd []string 524 for _, v := range cmd { 525 newCmd = append(newCmd, strings.ReplaceAll(v, `\`, `/`)) 526 } 527 cmd = newCmd 528 } 529 530 logger.Debugf("Exec command '%s'", cmd) 531 isTerminal := term.IsTerminal(int(os.Stdout.Fd())) 532 envList := make([]string, 0) 533 for k, v := range env { 534 envList = append(envList, fmt.Sprintf("%s=%s", k, v)) 535 } 536 537 var wd string 538 if workdir != "" { 539 if strings.HasPrefix(workdir, "/") { 540 wd = workdir 541 } else { 542 wd = fmt.Sprintf("%s/%s", cr.input.WorkingDir, workdir) 543 } 544 } else { 545 wd = cr.input.WorkingDir 546 } 547 logger.Debugf("Working directory '%s'", wd) 548 549 idResp, err := cr.cli.ContainerExecCreate(ctx, cr.id, types.ExecConfig{ 550 User: user, 551 Cmd: cmd, 552 WorkingDir: wd, 553 Env: envList, 554 Tty: isTerminal, 555 AttachStderr: true, 556 AttachStdout: true, 557 }) 558 if err != nil { 559 return fmt.Errorf("failed to create exec: %w", err) 560 } 561 562 resp, err := cr.cli.ContainerExecAttach(ctx, idResp.ID, types.ExecStartCheck{ 563 Tty: isTerminal, 564 }) 565 if err != nil { 566 return fmt.Errorf("failed to attach to exec: %w", err) 567 } 568 defer resp.Close() 569 570 err = cr.waitForCommand(ctx, isTerminal, resp, idResp, user, workdir) 571 if err != nil { 572 return err 573 } 574 575 inspectResp, err := cr.cli.ContainerExecInspect(ctx, idResp.ID) 576 if err != nil { 577 return fmt.Errorf("failed to inspect exec: %w", err) 578 } 579 580 switch inspectResp.ExitCode { 581 case 0: 582 return nil 583 case 127: 584 return fmt.Errorf("exitcode '%d': command not found, please refer to https://github.com/nektos/act/issues/107 for more information", inspectResp.ExitCode) 585 default: 586 return fmt.Errorf("exitcode '%d': failure", inspectResp.ExitCode) 587 } 588 } 589 } 590 591 func (cr *containerReference) tryReadID(opt string, cbk func(id int)) common.Executor { 592 return func(ctx context.Context) error { 593 idResp, err := cr.cli.ContainerExecCreate(ctx, cr.id, types.ExecConfig{ 594 Cmd: []string{"id", opt}, 595 AttachStdout: true, 596 AttachStderr: true, 597 }) 598 if err != nil { 599 return nil 600 } 601 602 resp, err := cr.cli.ContainerExecAttach(ctx, idResp.ID, types.ExecStartCheck{}) 603 if err != nil { 604 return nil 605 } 606 defer resp.Close() 607 608 sid, err := resp.Reader.ReadString('\n') 609 if err != nil { 610 return nil 611 } 612 exp := regexp.MustCompile(`\d+\n`) 613 found := exp.FindString(sid) 614 id, err := strconv.ParseInt(strings.TrimSpace(found), 10, 32) 615 if err != nil { 616 return nil 617 } 618 cbk(int(id)) 619 620 return nil 621 } 622 } 623 624 func (cr *containerReference) tryReadUID() common.Executor { 625 return cr.tryReadID("-u", func(id int) { cr.UID = id }) 626 } 627 628 func (cr *containerReference) tryReadGID() common.Executor { 629 return cr.tryReadID("-g", func(id int) { cr.GID = id }) 630 } 631 632 func (cr *containerReference) waitForCommand(ctx context.Context, isTerminal bool, resp types.HijackedResponse, _ types.IDResponse, _ string, _ string) error { 633 logger := common.Logger(ctx) 634 635 cmdResponse := make(chan error) 636 637 go func() { 638 var outWriter io.Writer 639 outWriter = cr.input.Stdout 640 if outWriter == nil { 641 outWriter = os.Stdout 642 } 643 errWriter := cr.input.Stderr 644 if errWriter == nil { 645 errWriter = os.Stderr 646 } 647 648 var err error 649 if !isTerminal || os.Getenv("NORAW") != "" { 650 _, err = stdcopy.StdCopy(outWriter, errWriter, resp.Reader) 651 } else { 652 _, err = io.Copy(outWriter, resp.Reader) 653 } 654 cmdResponse <- err 655 }() 656 657 select { 658 case <-ctx.Done(): 659 // send ctrl + c 660 _, err := resp.Conn.Write([]byte{3}) 661 if err != nil { 662 logger.Warnf("Failed to send CTRL+C: %+s", err) 663 } 664 665 // we return the context canceled error to prevent other steps 666 // from executing 667 return ctx.Err() 668 case err := <-cmdResponse: 669 if err != nil { 670 logger.Error(err) 671 } 672 673 return nil 674 } 675 } 676 677 func (cr *containerReference) CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error { 678 // Mkdir 679 buf := &bytes.Buffer{} 680 tw := tar.NewWriter(buf) 681 _ = tw.WriteHeader(&tar.Header{ 682 Name: destPath, 683 Mode: 0o777, 684 Typeflag: tar.TypeDir, 685 }) 686 tw.Close() 687 err := cr.cli.CopyToContainer(ctx, cr.id, "/", buf, types.CopyToContainerOptions{}) 688 if err != nil { 689 return fmt.Errorf("failed to mkdir to copy content to container: %w", err) 690 } 691 // Copy Content 692 err = cr.cli.CopyToContainer(ctx, cr.id, destPath, tarStream, types.CopyToContainerOptions{}) 693 if err != nil { 694 return fmt.Errorf("failed to copy content to container: %w", err) 695 } 696 // If this fails, then folders have wrong permissions on non root container 697 if cr.UID != 0 || cr.GID != 0 { 698 _ = cr.Exec([]string{"chown", "-R", fmt.Sprintf("%d:%d", cr.UID, cr.GID), destPath}, nil, "0", "")(ctx) 699 } 700 return nil 701 } 702 703 func (cr *containerReference) copyDir(dstPath string, srcPath string, useGitIgnore bool) common.Executor { 704 return func(ctx context.Context) error { 705 logger := common.Logger(ctx) 706 tarFile, err := os.CreateTemp("", "act") 707 if err != nil { 708 return err 709 } 710 logger.Debugf("Writing tarball %s from %s", tarFile.Name(), srcPath) 711 defer func(tarFile *os.File) { 712 name := tarFile.Name() 713 err := tarFile.Close() 714 if !errors.Is(err, os.ErrClosed) { 715 logger.Error(err) 716 } 717 err = os.Remove(name) 718 if err != nil { 719 logger.Error(err) 720 } 721 }(tarFile) 722 tw := tar.NewWriter(tarFile) 723 724 srcPrefix := filepath.Dir(srcPath) 725 if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) { 726 srcPrefix += string(filepath.Separator) 727 } 728 logger.Debugf("Stripping prefix:%s src:%s", srcPrefix, srcPath) 729 730 var ignorer gitignore.Matcher 731 if useGitIgnore { 732 ps, err := gitignore.ReadPatterns(polyfill.New(osfs.New(srcPath)), nil) 733 if err != nil { 734 logger.Debugf("Error loading .gitignore: %v", err) 735 } 736 737 ignorer = gitignore.NewMatcher(ps) 738 } 739 740 fc := &filecollector.FileCollector{ 741 Fs: &filecollector.DefaultFs{}, 742 Ignorer: ignorer, 743 SrcPath: srcPath, 744 SrcPrefix: srcPrefix, 745 Handler: &filecollector.TarCollector{ 746 TarWriter: tw, 747 UID: cr.UID, 748 GID: cr.GID, 749 DstDir: dstPath[1:], 750 }, 751 } 752 753 err = filepath.Walk(srcPath, fc.CollectFiles(ctx, []string{})) 754 if err != nil { 755 return err 756 } 757 if err := tw.Close(); err != nil { 758 return err 759 } 760 761 logger.Debugf("Extracting content from '%s' to '%s'", tarFile.Name(), dstPath) 762 _, err = tarFile.Seek(0, 0) 763 if err != nil { 764 return fmt.Errorf("failed to seek tar archive: %w", err) 765 } 766 err = cr.cli.CopyToContainer(ctx, cr.id, "/", tarFile, types.CopyToContainerOptions{}) 767 if err != nil { 768 return fmt.Errorf("failed to copy content to container: %w", err) 769 } 770 return nil 771 } 772 } 773 774 func (cr *containerReference) copyContent(dstPath string, files ...*FileEntry) common.Executor { 775 return func(ctx context.Context) error { 776 logger := common.Logger(ctx) 777 var buf bytes.Buffer 778 tw := tar.NewWriter(&buf) 779 for _, file := range files { 780 logger.Debugf("Writing entry to tarball %s len:%d", file.Name, len(file.Body)) 781 hdr := &tar.Header{ 782 Name: file.Name, 783 Mode: file.Mode, 784 Size: int64(len(file.Body)), 785 Uid: cr.UID, 786 Gid: cr.GID, 787 } 788 if err := tw.WriteHeader(hdr); err != nil { 789 return err 790 } 791 if _, err := tw.Write([]byte(file.Body)); err != nil { 792 return err 793 } 794 } 795 if err := tw.Close(); err != nil { 796 return err 797 } 798 799 logger.Debugf("Extracting content to '%s'", dstPath) 800 err := cr.cli.CopyToContainer(ctx, cr.id, dstPath, &buf, types.CopyToContainerOptions{}) 801 if err != nil { 802 return fmt.Errorf("failed to copy content to container: %w", err) 803 } 804 return nil 805 } 806 } 807 808 func (cr *containerReference) attach() common.Executor { 809 return func(ctx context.Context) error { 810 out, err := cr.cli.ContainerAttach(ctx, cr.id, container.AttachOptions{ 811 Stream: true, 812 Stdout: true, 813 Stderr: true, 814 }) 815 if err != nil { 816 return fmt.Errorf("failed to attach to container: %w", err) 817 } 818 isTerminal := term.IsTerminal(int(os.Stdout.Fd())) 819 820 var outWriter io.Writer 821 outWriter = cr.input.Stdout 822 if outWriter == nil { 823 outWriter = os.Stdout 824 } 825 errWriter := cr.input.Stderr 826 if errWriter == nil { 827 errWriter = os.Stderr 828 } 829 go func() { 830 if !isTerminal || os.Getenv("NORAW") != "" { 831 _, err = stdcopy.StdCopy(outWriter, errWriter, out.Reader) 832 } else { 833 _, err = io.Copy(outWriter, out.Reader) 834 } 835 if err != nil { 836 common.Logger(ctx).Error(err) 837 } 838 }() 839 return nil 840 } 841 } 842 843 func (cr *containerReference) start() common.Executor { 844 return func(ctx context.Context) error { 845 logger := common.Logger(ctx) 846 logger.Debugf("Starting container: %v", cr.id) 847 848 if err := cr.cli.ContainerStart(ctx, cr.id, container.StartOptions{}); err != nil { 849 return fmt.Errorf("failed to start container: %w", err) 850 } 851 852 logger.Debugf("Started container: %v", cr.id) 853 return nil 854 } 855 } 856 857 func (cr *containerReference) wait() common.Executor { 858 return func(ctx context.Context) error { 859 logger := common.Logger(ctx) 860 statusCh, errCh := cr.cli.ContainerWait(ctx, cr.id, container.WaitConditionNotRunning) 861 var statusCode int64 862 select { 863 case err := <-errCh: 864 if err != nil { 865 return fmt.Errorf("failed to wait for container: %w", err) 866 } 867 case status := <-statusCh: 868 statusCode = status.StatusCode 869 } 870 871 logger.Debugf("Return status: %v", statusCode) 872 873 if statusCode == 0 { 874 return nil 875 } 876 877 return fmt.Errorf("exit with `FAILURE`: %v", statusCode) 878 } 879 }