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