2019-01-12 22:45:25 -06:00
package container
import (
2020-02-23 17:01:25 -06:00
"archive/tar"
2021-01-12 00:39:43 -06:00
"bufio"
2020-02-23 17:01:25 -06:00
"bytes"
2019-01-12 22:45:25 -06:00
"context"
2022-08-07 01:07:54 -05:00
"errors"
2019-01-12 22:45:25 -06:00
"fmt"
"io"
"os"
2020-02-24 18:38:49 -06:00
"path/filepath"
2021-01-12 00:39:43 -06:00
"regexp"
2021-01-12 00:41:35 -06:00
"runtime"
2022-06-20 17:47:39 -05:00
"strconv"
2021-01-14 23:37:38 -06:00
"strings"
2019-01-12 22:45:25 -06:00
2020-04-16 18:24:30 -05:00
"github.com/go-git/go-billy/v5/helper/polyfill"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
2021-09-27 14:01:14 -05:00
"github.com/joho/godotenv"
2020-04-16 18:24:30 -05:00
2022-10-06 17:09:43 -05:00
"github.com/imdario/mergo"
"github.com/kballard/go-shellquote"
"github.com/spf13/pflag"
2020-05-03 23:15:42 -05:00
"github.com/docker/cli/cli/connhelper"
2019-01-12 22:45:25 -06:00
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
2020-02-23 17:01:25 -06:00
"github.com/docker/docker/api/types/mount"
2019-01-12 22:45:25 -06:00
"github.com/docker/docker/client"
2020-02-11 11:10:35 -06:00
"github.com/docker/docker/pkg/stdcopy"
2021-03-28 23:08:40 -05:00
specs "github.com/opencontainers/image-spec/specs-go/v1"
2021-03-30 12:10:42 -05:00
"github.com/Masterminds/semver"
2021-01-12 00:39:43 -06:00
"golang.org/x/term"
2021-03-28 23:08:40 -05:00
"github.com/nektos/act/pkg/common"
2019-01-12 22:45:25 -06:00
)
2020-02-23 17:01:25 -06:00
// NewContainerInput the input for the New function
type NewContainerInput struct {
2020-03-09 19:43:24 -05:00
Image string
2021-05-05 11:37:17 -05:00
Username string
Password string
2020-03-09 19:43:24 -05:00
Entrypoint [ ] string
Cmd [ ] string
WorkingDir string
Env [ ] string
Binds [ ] string
Mounts map [ string ] string
Name string
Stdout io . Writer
Stderr io . Writer
NetworkMode string
2020-08-01 15:21:49 -05:00
Privileged bool
2021-02-27 10:31:25 -06:00
UsernsMode string
2021-03-28 23:08:40 -05:00
Platform string
2022-10-06 17:09:43 -05:00
Options string
2020-02-23 17:01:25 -06:00
}
// FileEntry is a file to copy to a container
type FileEntry struct {
Name string
Mode int64
Body string
}
// Container for managing docker run containers
type Container interface {
2021-06-04 11:06:59 -05:00
Create ( capAdd [ ] string , capDrop [ ] string ) common . Executor
2020-02-23 17:01:25 -06:00
Copy ( destPath string , files ... * FileEntry ) common . Executor
2021-05-03 09:37:20 -05:00
CopyDir ( destPath string , srcPath string , useGitIgnore bool ) common . Executor
2021-08-03 12:39:56 -05:00
GetContainerArchive ( ctx context . Context , srcPath string ) ( io . ReadCloser , error )
2020-02-23 17:01:25 -06:00
Pull ( forcePull bool ) common . Executor
Start ( attach bool ) common . Executor
2021-08-10 14:40:20 -05:00
Exec ( command [ ] string , env map [ string ] string , user , workdir string ) common . Executor
2021-05-05 18:11:43 -05:00
UpdateFromEnv ( srcPath string , env * map [ string ] string ) common . Executor
2021-09-27 14:01:14 -05:00
UpdateFromImageEnv ( env * map [ string ] string ) common . Executor
2021-05-06 08:30:12 -05:00
UpdateFromPath ( env * map [ string ] string ) common . Executor
2020-02-23 17:01:25 -06:00
Remove ( ) common . Executor
2021-10-24 11:50:43 -05:00
Close ( ) common . Executor
2022-05-11 14:06:05 -05:00
ReplaceLogWriter ( io . Writer , io . Writer ) ( io . Writer , io . Writer )
2020-02-23 17:01:25 -06:00
}
// NewContainer creates a reference to a container
func NewContainer ( input * NewContainerInput ) Container {
2020-02-07 00:17:58 -06:00
cr := new ( containerReference )
cr . input = input
2020-02-23 17:01:25 -06:00
return cr
}
2020-02-07 00:17:58 -06:00
2021-03-30 12:10:42 -05:00
// supportsContainerImagePlatform returns true if the underlying Docker server
// API version is 1.41 and beyond
2022-05-24 09:52:25 -05:00
func supportsContainerImagePlatform ( ctx context . Context , cli client . APIClient ) bool {
2021-03-30 12:10:42 -05:00
logger := common . Logger ( ctx )
ver , err := cli . ServerVersion ( ctx )
if err != nil {
logger . Panicf ( "Failed to get Docker API Version: %s" , err )
return false
}
sv , err := semver . NewVersion ( ver . APIVersion )
if err != nil {
logger . Panicf ( "Failed to unmarshal Docker Version: %s" , err )
return false
}
constraint , _ := semver . NewConstraint ( ">= 1.41" )
return constraint . Check ( sv )
}
2021-06-04 11:06:59 -05:00
func ( cr * containerReference ) Create ( capAdd [ ] string , capDrop [ ] string ) common . Executor {
2020-02-07 00:17:58 -06:00
return common .
2021-08-10 14:40:20 -05:00
NewInfoExecutor ( "%sdocker create image=%s platform=%s entrypoint=%+q cmd=%+q" , logPrefix , cr . input . Image , cr . input . Platform , cr . input . Entrypoint , cr . input . Cmd ) .
2020-02-07 00:17:58 -06:00
Then (
common . NewPipelineExecutor (
cr . connect ( ) ,
cr . find ( ) ,
2021-06-04 11:06:59 -05:00
cr . create ( capAdd , capDrop ) ,
2020-02-23 17:01:25 -06:00
) . IfNot ( common . Dryrun ) ,
)
}
2021-08-10 14:40:20 -05:00
2020-02-23 17:01:25 -06:00
func ( cr * containerReference ) Start ( attach bool ) common . Executor {
return common .
2021-03-28 23:08:40 -05:00
NewInfoExecutor ( "%sdocker run image=%s platform=%s entrypoint=%+q cmd=%+q" , logPrefix , cr . input . Image , cr . input . Platform , cr . input . Entrypoint , cr . input . Cmd ) .
2020-02-23 17:01:25 -06:00
Then (
common . NewPipelineExecutor (
cr . connect ( ) ,
cr . find ( ) ,
cr . attach ( ) . IfBool ( attach ) ,
2020-02-07 00:17:58 -06:00
cr . start ( ) ,
2020-02-23 17:01:25 -06:00
cr . wait ( ) . IfBool ( attach ) ,
2022-06-20 17:47:39 -05:00
cr . tryReadUID ( ) ,
cr . tryReadGID ( ) ,
func ( ctx context . Context ) error {
// If this fails, then folders have wrong permissions on non root container
2022-07-08 17:39:42 -05:00
if cr . UID != 0 || cr . GID != 0 {
_ = cr . Exec ( [ ] string { "chown" , "-R" , fmt . Sprintf ( "%d:%d" , cr . UID , cr . GID ) , cr . input . WorkingDir } , nil , "0" , "" ) ( ctx )
}
2022-06-20 17:47:39 -05:00
return nil
} ,
2020-02-07 00:17:58 -06:00
) . IfNot ( common . Dryrun ) ,
)
}
2021-08-10 14:40:20 -05:00
2020-02-23 17:01:25 -06:00
func ( cr * containerReference ) Pull ( forcePull bool ) common . Executor {
2021-08-10 14:40:20 -05:00
return common .
NewInfoExecutor ( "%sdocker pull image=%s platform=%s username=%s forcePull=%t" , logPrefix , cr . input . Image , cr . input . Platform , cr . input . Username , forcePull ) .
Then (
NewDockerPullExecutor ( NewDockerPullExecutorInput {
Image : cr . input . Image ,
ForcePull : forcePull ,
Platform : cr . input . Platform ,
Username : cr . input . Username ,
Password : cr . input . Password ,
} ) ,
)
2020-02-23 17:01:25 -06:00
}
2021-05-05 18:11:43 -05:00
2020-02-23 17:01:25 -06:00
func ( cr * containerReference ) Copy ( destPath string , files ... * FileEntry ) common . Executor {
return common . NewPipelineExecutor (
cr . connect ( ) ,
cr . find ( ) ,
cr . copyContent ( destPath , files ... ) ,
) . IfNot ( common . Dryrun )
}
2021-05-03 09:37:20 -05:00
func ( cr * containerReference ) CopyDir ( destPath string , srcPath string , useGitIgnore bool ) common . Executor {
2020-02-24 18:38:49 -06:00
return common . NewPipelineExecutor (
2020-02-24 19:48:21 -06:00
common . NewInfoExecutor ( "%sdocker cp src=%s dst=%s" , logPrefix , srcPath , destPath ) ,
2021-05-03 09:37:20 -05:00
cr . copyDir ( destPath , srcPath , useGitIgnore ) ,
2022-06-20 17:47:39 -05:00
func ( ctx context . Context ) error {
// If this fails, then folders have wrong permissions on non root container
2022-07-08 17:39:42 -05:00
if cr . UID != 0 || cr . GID != 0 {
_ = cr . Exec ( [ ] string { "chown" , "-R" , fmt . Sprintf ( "%d:%d" , cr . UID , cr . GID ) , destPath } , nil , "0" , "" ) ( ctx )
}
2022-06-20 17:47:39 -05:00
return nil
} ,
2020-02-24 18:38:49 -06:00
) . IfNot ( common . Dryrun )
}
2021-08-03 12:39:56 -05:00
func ( cr * containerReference ) GetContainerArchive ( ctx context . Context , srcPath string ) ( io . ReadCloser , error ) {
2022-05-11 14:14:45 -05:00
if common . Dryrun ( ctx ) {
return nil , fmt . Errorf ( "DRYRUN is not supported in GetContainerArchive" )
}
2021-08-03 12:39:56 -05:00
a , _ , err := cr . cli . CopyFromContainer ( ctx , cr . id , srcPath )
return a , err
}
2021-05-05 18:11:43 -05:00
func ( cr * containerReference ) UpdateFromEnv ( srcPath string , env * map [ string ] string ) common . Executor {
return cr . extractEnv ( srcPath , env ) . IfNot ( common . Dryrun )
2021-01-12 00:39:43 -06:00
}
2021-09-27 14:01:14 -05:00
func ( cr * containerReference ) UpdateFromImageEnv ( env * map [ string ] string ) common . Executor {
return cr . extractFromImageEnv ( env ) . IfNot ( common . Dryrun )
}
2021-05-06 08:30:12 -05:00
func ( cr * containerReference ) UpdateFromPath ( env * map [ string ] string ) common . Executor {
return cr . extractPath ( env ) . IfNot ( common . Dryrun )
}
2021-08-10 14:40:20 -05:00
func ( cr * containerReference ) Exec ( command [ ] string , env map [ string ] string , user , workdir string ) common . Executor {
2020-02-23 17:01:25 -06:00
return common . NewPipelineExecutor (
2021-08-10 14:40:20 -05:00
common . NewInfoExecutor ( "%sdocker exec cmd=[%s] user=%s workdir=%s" , logPrefix , strings . Join ( command , " " ) , user , workdir ) ,
2020-02-23 17:01:25 -06:00
cr . connect ( ) ,
cr . find ( ) ,
2021-08-10 14:40:20 -05:00
cr . exec ( command , env , user , workdir ) ,
2020-02-23 17:01:25 -06:00
) . IfNot ( common . Dryrun )
}
2021-05-05 18:11:43 -05:00
2020-02-23 17:01:25 -06:00
func ( cr * containerReference ) Remove ( ) common . Executor {
return common . NewPipelineExecutor (
cr . connect ( ) ,
cr . find ( ) ,
) . Finally (
cr . remove ( ) ,
) . IfNot ( common . Dryrun )
}
2019-01-12 22:45:25 -06:00
2022-05-11 14:06:05 -05:00
func ( cr * containerReference ) ReplaceLogWriter ( stdout io . Writer , stderr io . Writer ) ( io . Writer , io . Writer ) {
out := cr . input . Stdout
err := cr . input . Stderr
cr . input . Stdout = stdout
cr . input . Stderr = stderr
return out , err
}
2020-02-07 00:17:58 -06:00
type containerReference struct {
2022-05-24 09:52:25 -05:00
cli client . APIClient
2020-02-07 00:17:58 -06:00
id string
2020-02-23 17:01:25 -06:00
input * NewContainerInput
2022-06-20 17:47:39 -05:00
UID int
GID int
2020-02-07 00:17:58 -06:00
}
2019-01-12 22:45:25 -06:00
2022-05-24 09:52:25 -05:00
func GetDockerClient ( ctx context . Context ) ( cli client . APIClient , err error ) {
2020-05-03 23:15:42 -05:00
// TODO: this should maybe need to be a global option, not hidden in here?
// though i'm not sure how that works out when there's another Executor :D
// I really would like something that works on OSX native for eg
dockerHost := os . Getenv ( "DOCKER_HOST" )
if strings . HasPrefix ( dockerHost , "ssh://" ) {
var helper * connhelper . ConnectionHelper
helper , err = connhelper . GetConnectionHelper ( dockerHost )
if err != nil {
return nil , err
}
cli , err = client . NewClientWithOpts (
client . WithHost ( helper . Host ) ,
client . WithDialContext ( helper . Dialer ) ,
)
} else {
cli , err = client . NewClientWithOpts ( client . FromEnv )
}
if err != nil {
2022-06-10 16:16:42 -05:00
return nil , fmt . Errorf ( "failed to connect to docker daemon: %w" , err )
2020-05-03 23:15:42 -05:00
}
cli . NegotiateAPIVersion ( ctx )
2022-06-10 16:16:42 -05:00
return cli , nil
2020-05-03 23:15:42 -05:00
}
2022-03-22 14:26:10 -05:00
func GetHostInfo ( ctx context . Context ) ( info types . Info , err error ) {
2022-05-24 09:52:25 -05:00
var cli client . APIClient
2022-03-22 14:26:10 -05:00
cli , err = GetDockerClient ( ctx )
if err != nil {
return info , err
}
defer cli . Close ( )
info , err = cli . Info ( ctx )
if err != nil {
return info , err
}
return info , nil
}
2022-08-29 10:39:31 -05:00
// Arch fetches values from docker info and translates architecture to
// GitHub actions compatible runner.arch values
// https://github.com/github/docs/blob/main/data/reusables/actions/runner-arch-description.md
func RunnerArch ( ctx context . Context ) string {
info , err := GetHostInfo ( ctx )
if err != nil {
return ""
}
archMapper := map [ string ] string {
"x86_64" : "X64" ,
"386" : "x86" ,
"aarch64" : "arm64" ,
}
if arch , ok := archMapper [ info . Architecture ] ; ok {
return arch
}
return info . Architecture
}
2020-02-07 00:17:58 -06:00
func ( cr * containerReference ) connect ( ) common . Executor {
return func ( ctx context . Context ) error {
2020-02-23 17:01:25 -06:00
if cr . cli != nil {
return nil
}
2020-05-03 23:15:42 -05:00
cli , err := GetDockerClient ( ctx )
2019-01-12 22:45:25 -06:00
if err != nil {
2020-05-03 23:15:42 -05:00
return err
2019-01-12 22:45:25 -06:00
}
2020-02-07 00:17:58 -06:00
cr . cli = cli
return nil
}
}
2019-01-12 22:45:25 -06:00
2021-10-24 11:50:43 -05:00
func ( cr * containerReference ) Close ( ) common . Executor {
return func ( ctx context . Context ) error {
if cr . cli != nil {
2022-06-10 16:16:42 -05:00
err := cr . cli . Close ( )
2021-10-24 11:50:43 -05:00
cr . cli = nil
2022-06-10 16:16:42 -05:00
if err != nil {
return fmt . Errorf ( "failed to close client: %w" , err )
}
2021-10-24 11:50:43 -05:00
}
return nil
}
}
2020-02-07 00:17:58 -06:00
func ( cr * containerReference ) find ( ) common . Executor {
return func ( ctx context . Context ) error {
2020-02-23 17:01:25 -06:00
if cr . id != "" {
return nil
}
2020-02-07 00:17:58 -06:00
containers , err := cr . cli . ContainerList ( ctx , types . ContainerListOptions {
All : true ,
} )
2019-01-12 22:45:25 -06:00
if err != nil {
2022-06-10 16:16:42 -05:00
return fmt . Errorf ( "failed to list containers: %w" , err )
2019-01-12 22:45:25 -06:00
}
2021-01-12 00:39:43 -06:00
for _ , c := range containers {
for _ , name := range c . Names {
2020-02-07 00:17:58 -06:00
if name [ 1 : ] == cr . input . Name {
2021-01-12 00:39:43 -06:00
cr . id = c . ID
2020-02-07 00:17:58 -06:00
return nil
}
2019-01-17 02:45:37 -06:00
}
2019-01-12 22:45:25 -06:00
}
2020-02-07 00:17:58 -06:00
cr . id = ""
return nil
2019-01-12 22:45:25 -06:00
}
}
2020-02-07 00:17:58 -06:00
func ( cr * containerReference ) remove ( ) common . Executor {
return func ( ctx context . Context ) error {
if cr . id == "" {
return nil
}
2019-01-12 22:45:25 -06:00
2020-02-07 00:17:58 -06:00
logger := common . Logger ( ctx )
2021-12-22 11:29:43 -06:00
err := cr . cli . ContainerRemove ( ctx , cr . id , types . ContainerRemoveOptions {
2020-02-07 00:17:58 -06:00
RemoveVolumes : true ,
Force : true ,
} )
if err != nil {
2022-06-10 16:16:42 -05:00
logger . Error ( fmt . Errorf ( "failed to remove container: %w" , err ) )
2019-01-12 22:45:25 -06:00
}
2020-02-07 00:17:58 -06:00
logger . Debugf ( "Removed container: %v" , cr . id )
2020-02-23 18:36:44 -06:00
cr . id = ""
2020-02-07 00:17:58 -06:00
return nil
2019-01-12 22:45:25 -06:00
}
}
2022-10-12 11:30:56 -05:00
func ( cr * containerReference ) mergeContainerConfigs ( ctx context . Context , config * container . Config , hostConfig * container . HostConfig ) ( * container . Config , * container . HostConfig , error ) {
logger := common . Logger ( ctx )
input := cr . input
if input . Options == "" {
return config , hostConfig , nil
}
// parse configuration from CLI container.options
flags := pflag . NewFlagSet ( "container_flags" , pflag . ContinueOnError )
copts := addFlags ( flags )
optionsArgs , err := shellquote . Split ( input . Options )
if err != nil {
return nil , nil , fmt . Errorf ( "Cannot split container options: '%s': '%w'" , input . Options , err )
}
err = flags . Parse ( optionsArgs )
if err != nil {
return nil , nil , fmt . Errorf ( "Cannot parse container options: '%s': '%w'" , input . Options , err )
}
containerConfig , err := parse ( flags , copts , "" )
if err != nil {
return nil , nil , fmt . Errorf ( "Cannot process container options: '%s': '%w'" , input . Options , err )
}
logger . Debugf ( "Custom container.Config from options ==> %+v" , containerConfig . Config )
err = mergo . Merge ( config , containerConfig . Config , mergo . WithOverride )
if err != nil {
return nil , nil , fmt . Errorf ( "Cannot merge container.Config options: '%s': '%w'" , input . Options , err )
}
logger . Debugf ( "Merged container.Config ==> %+v" , config )
logger . Debugf ( "Custom container.HostConfig from options ==> %+v" , containerConfig . HostConfig )
err = mergo . Merge ( hostConfig , containerConfig . HostConfig , mergo . WithOverride )
if err != nil {
return nil , nil , fmt . Errorf ( "Cannot merge container.HostConfig options: '%s': '%w'" , input . Options , err )
}
logger . Debugf ( "Merged container.HostConfig ==> %+v" , hostConfig )
return config , hostConfig , nil
}
2021-06-04 11:06:59 -05:00
func ( cr * containerReference ) create ( capAdd [ ] string , capDrop [ ] string ) common . Executor {
2020-02-07 00:17:58 -06:00
return func ( ctx context . Context ) error {
if cr . id != "" {
return nil
}
logger := common . Logger ( ctx )
2021-01-12 00:39:43 -06:00
isTerminal := term . IsTerminal ( int ( os . Stdout . Fd ( ) ) )
2020-02-07 00:17:58 -06:00
input := cr . input
2021-09-26 11:37:53 -05:00
2020-02-07 00:17:58 -06:00
config := & container . Config {
Image : input . Image ,
WorkingDir : input . WorkingDir ,
Env : input . Env ,
Tty : isTerminal ,
}
2022-10-06 17:09:43 -05:00
logger . Debugf ( "Common container.Config ==> %+v" , config )
2019-01-17 02:45:37 -06:00
2022-03-29 13:00:52 -05:00
if len ( input . Cmd ) != 0 {
config . Cmd = input . Cmd
}
if len ( input . Entrypoint ) != 0 {
config . Entrypoint = input . Entrypoint
}
2020-02-23 17:01:25 -06:00
mounts := make ( [ ] mount . Mount , 0 )
for mountSource , mountTarget := range input . Mounts {
mounts = append ( mounts , mount . Mount {
Type : mount . TypeVolume ,
Source : mountSource ,
Target : mountTarget ,
} )
2019-01-17 02:45:37 -06:00
}
2021-03-30 12:10:42 -05:00
var platSpecs * specs . Platform
2021-12-22 11:29:43 -06:00
if supportsContainerImagePlatform ( ctx , cr . cli ) && cr . input . Platform != "" {
2021-03-30 12:10:42 -05:00
desiredPlatform := strings . SplitN ( cr . input . Platform , ` / ` , 2 )
2021-03-28 23:08:40 -05:00
2021-03-30 12:10:42 -05:00
if len ( desiredPlatform ) != 2 {
2022-06-10 16:16:42 -05:00
return fmt . Errorf ( "incorrect container platform option '%s'" , cr . input . Platform )
2021-03-30 12:10:42 -05:00
}
2021-03-28 23:08:40 -05:00
2021-03-30 12:10:42 -05:00
platSpecs = & specs . Platform {
Architecture : desiredPlatform [ 1 ] ,
OS : desiredPlatform [ 0 ] ,
}
}
2022-10-06 17:09:43 -05:00
hostConfig := & container . HostConfig {
2021-06-04 11:06:59 -05:00
CapAdd : capAdd ,
CapDrop : capDrop ,
2020-03-09 19:43:24 -05:00
Binds : input . Binds ,
Mounts : mounts ,
NetworkMode : container . NetworkMode ( input . NetworkMode ) ,
2020-08-01 15:21:49 -05:00
Privileged : input . Privileged ,
2021-02-27 10:31:25 -06:00
UsernsMode : container . UsernsMode ( input . UsernsMode ) ,
2022-10-06 17:09:43 -05:00
}
logger . Debugf ( "Common container.HostConfig ==> %+v" , hostConfig )
2022-10-12 11:30:56 -05:00
config , hostConfig , err := cr . mergeContainerConfigs ( ctx , config , hostConfig )
2020-02-07 00:17:58 -06:00
if err != nil {
2022-10-12 11:30:56 -05:00
return err
2020-02-07 00:17:58 -06:00
}
2022-10-06 17:09:43 -05:00
resp , err := cr . cli . ContainerCreate ( ctx , config , hostConfig , nil , platSpecs , input . Name )
if err != nil {
return fmt . Errorf ( "failed to create container: '%w'" , err )
}
2021-03-28 23:08:40 -05:00
logger . Debugf ( "Created container name=%s id=%v from image %v (platform: %s)" , input . Name , resp . ID , input . Image , input . Platform )
2020-02-07 00:17:58 -06:00
logger . Debugf ( "ENV ==> %v" , input . Env )
2019-01-17 02:45:37 -06:00
2020-02-07 00:17:58 -06:00
cr . id = resp . ID
return nil
2019-01-12 22:45:25 -06:00
}
}
2022-06-10 16:16:42 -05:00
var singleLineEnvPattern , multiLineEnvPattern * regexp . Regexp
2021-01-12 00:39:43 -06:00
2021-05-05 18:11:43 -05:00
func ( cr * containerReference ) extractEnv ( srcPath string , env * map [ string ] string ) common . Executor {
2021-01-12 00:39:43 -06:00
if singleLineEnvPattern == nil {
2021-11-19 11:36:50 -06:00
// Single line pattern matches:
// SOME_VAR=data=moredata
// SOME_VAR=datamoredata
singleLineEnvPattern = regexp . MustCompile ( ` ^([^=]*)\=(.*)$ ` )
2022-10-18 17:27:28 -05:00
multiLineEnvPattern = regexp . MustCompile ( ` ^([^<]+)<<([\w-]+)$ ` )
2021-01-12 00:39:43 -06:00
}
localEnv := * env
return func ( ctx context . Context ) error {
2021-05-05 18:11:43 -05:00
envTar , _ , err := cr . cli . CopyFromContainer ( ctx , cr . id , srcPath )
2021-01-12 00:39:43 -06:00
if err != nil {
return nil
}
2021-05-06 08:30:12 -05:00
defer envTar . Close ( )
2022-06-10 16:16:42 -05:00
2021-05-05 18:11:43 -05:00
reader := tar . NewReader ( envTar )
2021-01-12 00:39:43 -06:00
_ , err = reader . Next ( )
if err != nil && err != io . EOF {
2022-06-10 16:16:42 -05:00
return fmt . Errorf ( "failed to read tar archive: %w" , err )
2021-01-12 00:39:43 -06:00
}
s := bufio . NewScanner ( reader )
multiLineEnvKey := ""
multiLineEnvDelimiter := ""
multiLineEnvContent := ""
for s . Scan ( ) {
line := s . Text ( )
if singleLineEnv := singleLineEnvPattern . FindStringSubmatch ( line ) ; singleLineEnv != nil {
localEnv [ singleLineEnv [ 1 ] ] = singleLineEnv [ 2 ]
}
if line == multiLineEnvDelimiter {
localEnv [ multiLineEnvKey ] = multiLineEnvContent
multiLineEnvKey , multiLineEnvDelimiter , multiLineEnvContent = "" , "" , ""
}
if multiLineEnvKey != "" && multiLineEnvDelimiter != "" {
if multiLineEnvContent != "" {
multiLineEnvContent += "\n"
}
multiLineEnvContent += line
}
2022-06-10 16:16:42 -05:00
if multiLineEnvStart := multiLineEnvPattern . FindStringSubmatch ( line ) ; multiLineEnvStart != nil {
multiLineEnvKey = multiLineEnvStart [ 1 ]
multiLineEnvDelimiter = multiLineEnvStart [ 2 ]
2021-01-12 00:39:43 -06:00
}
}
env = & localEnv
return nil
}
}
2021-09-27 14:01:14 -05:00
func ( cr * containerReference ) extractFromImageEnv ( env * map [ string ] string ) common . Executor {
envMap := * env
return func ( ctx context . Context ) error {
logger := common . Logger ( ctx )
inspect , _ , err := cr . cli . ImageInspectWithRaw ( ctx , cr . input . Image )
if err != nil {
logger . Error ( err )
}
imageEnv , err := godotenv . Unmarshal ( strings . Join ( inspect . Config . Env , "\n" ) )
if err != nil {
logger . Error ( err )
}
for k , v := range imageEnv {
if k == "PATH" {
if envMap [ k ] == "" {
envMap [ k ] = v
} else {
envMap [ k ] += ` : ` + v
}
} else if envMap [ k ] == "" {
envMap [ k ] = v
}
}
env = & envMap
return nil
}
}
2021-05-06 08:30:12 -05:00
func ( cr * containerReference ) extractPath ( env * map [ string ] string ) common . Executor {
localEnv := * env
return func ( ctx context . Context ) error {
pathTar , _ , err := cr . cli . CopyFromContainer ( ctx , cr . id , localEnv [ "GITHUB_PATH" ] )
if err != nil {
2022-06-10 16:16:42 -05:00
return fmt . Errorf ( "failed to copy from container: %w" , err )
2021-05-06 08:30:12 -05:00
}
defer pathTar . Close ( )
reader := tar . NewReader ( pathTar )
_ , err = reader . Next ( )
if err != nil && err != io . EOF {
2022-06-10 16:16:42 -05:00
return fmt . Errorf ( "failed to read tar archive: %w" , err )
2021-05-06 08:30:12 -05:00
}
s := bufio . NewScanner ( reader )
for s . Scan ( ) {
line := s . Text ( )
2021-05-18 08:22:39 -05:00
localEnv [ "PATH" ] = fmt . Sprintf ( "%s:%s" , line , localEnv [ "PATH" ] )
2021-05-06 08:30:12 -05:00
}
env = & localEnv
return nil
}
}
2021-08-10 14:40:20 -05:00
func ( cr * containerReference ) exec ( cmd [ ] string , env map [ string ] string , user , workdir string ) common . Executor {
2020-02-07 00:17:58 -06:00
return func ( ctx context . Context ) error {
logger := common . Logger ( ctx )
2021-01-12 00:41:35 -06:00
// Fix slashes when running on Windows
if runtime . GOOS == "windows" {
var newCmd [ ] string
for _ , v := range cmd {
newCmd = append ( newCmd , strings . ReplaceAll ( v , ` \ ` , ` / ` ) )
}
cmd = newCmd
}
2021-01-14 23:37:38 -06:00
2020-02-23 17:01:25 -06:00
logger . Debugf ( "Exec command '%s'" , cmd )
2021-01-12 00:39:43 -06:00
isTerminal := term . IsTerminal ( int ( os . Stdout . Fd ( ) ) )
2020-02-23 17:01:25 -06:00
envList := make ( [ ] string , 0 )
for k , v := range env {
envList = append ( envList , fmt . Sprintf ( "%s=%s" , k , v ) )
}
2021-08-10 14:40:20 -05:00
var wd string
if workdir != "" {
if strings . HasPrefix ( workdir , "/" ) {
wd = workdir
} else {
wd = fmt . Sprintf ( "%s/%s" , cr . input . WorkingDir , workdir )
}
} else {
wd = cr . input . WorkingDir
}
logger . Debugf ( "Working directory '%s'" , wd )
2020-02-23 17:01:25 -06:00
idResp , err := cr . cli . ContainerExecCreate ( ctx , cr . id , types . ExecConfig {
2021-05-24 12:09:03 -05:00
User : user ,
2020-02-23 17:01:25 -06:00
Cmd : cmd ,
2021-08-10 14:40:20 -05:00
WorkingDir : wd ,
2020-02-23 17:01:25 -06:00
Env : envList ,
Tty : isTerminal ,
AttachStderr : true ,
AttachStdout : true ,
} )
if err != nil {
2022-06-10 16:16:42 -05:00
return fmt . Errorf ( "failed to create exec: %w" , err )
2020-02-23 17:01:25 -06:00
}
resp , err := cr . cli . ContainerExecAttach ( ctx , idResp . ID , types . ExecStartCheck {
Tty : isTerminal ,
} )
if err != nil {
2022-06-10 16:16:42 -05:00
return fmt . Errorf ( "failed to attach to exec: %w" , err )
2020-02-23 17:01:25 -06:00
}
2021-05-06 08:30:12 -05:00
defer resp . Close ( )
2022-05-24 09:52:25 -05:00
err = cr . waitForCommand ( ctx , isTerminal , resp , idResp , user , workdir )
if err != nil {
return err
}
inspectResp , err := cr . cli . ContainerExecInspect ( ctx , idResp . ID )
if err != nil {
2022-06-10 16:16:42 -05:00
return fmt . Errorf ( "failed to inspect exec: %w" , err )
2022-05-24 09:52:25 -05:00
}
2022-06-10 16:16:42 -05:00
switch inspectResp . ExitCode {
case 0 :
2022-05-24 09:52:25 -05:00
return nil
2022-06-10 16:16:42 -05:00
case 127 :
return fmt . Errorf ( "exitcode '%d': command not found, please refer to https://github.com/nektos/act/issues/107 for more information" , inspectResp . ExitCode )
default :
return fmt . Errorf ( "exitcode '%d': failure" , inspectResp . ExitCode )
2022-05-24 09:52:25 -05:00
}
}
}
2022-06-20 17:47:39 -05:00
func ( cr * containerReference ) tryReadID ( opt string , cbk func ( id int ) ) common . Executor {
return func ( ctx context . Context ) error {
idResp , err := cr . cli . ContainerExecCreate ( ctx , cr . id , types . ExecConfig {
Cmd : [ ] string { "id" , opt } ,
AttachStdout : true ,
AttachStderr : true ,
} )
if err != nil {
return nil
}
resp , err := cr . cli . ContainerExecAttach ( ctx , idResp . ID , types . ExecStartCheck { } )
if err != nil {
return nil
}
defer resp . Close ( )
sid , err := resp . Reader . ReadString ( '\n' )
if err != nil {
return nil
}
exp := regexp . MustCompile ( ` \d+\n ` )
found := exp . FindString ( sid )
id , err := strconv . ParseInt ( found [ : len ( found ) - 1 ] , 10 , 32 )
if err != nil {
return nil
}
cbk ( int ( id ) )
return nil
}
}
func ( cr * containerReference ) tryReadUID ( ) common . Executor {
return cr . tryReadID ( "-u" , func ( id int ) { cr . UID = id } )
}
func ( cr * containerReference ) tryReadGID ( ) common . Executor {
return cr . tryReadID ( "-g" , func ( id int ) { cr . GID = id } )
}
2022-05-24 09:52:25 -05:00
func ( cr * containerReference ) waitForCommand ( ctx context . Context , isTerminal bool , resp types . HijackedResponse , idResp types . IDResponse , user string , workdir string ) error {
logger := common . Logger ( ctx )
cmdResponse := make ( chan error )
go func ( ) {
2020-02-23 17:01:25 -06:00
var outWriter io . Writer
outWriter = cr . input . Stdout
if outWriter == nil {
outWriter = os . Stdout
}
errWriter := cr . input . Stderr
if errWriter == nil {
errWriter = os . Stderr
}
2022-05-24 09:52:25 -05:00
var err error
2020-02-23 17:01:25 -06:00
if ! isTerminal || os . Getenv ( "NORAW" ) != "" {
_ , err = stdcopy . StdCopy ( outWriter , errWriter , resp . Reader )
} else {
_ , err = io . Copy ( outWriter , resp . Reader )
}
2022-05-24 09:52:25 -05:00
cmdResponse <- err
} ( )
2020-02-23 17:01:25 -06:00
2022-05-24 09:52:25 -05:00
select {
case <- ctx . Done ( ) :
// send ctrl + c
_ , err := resp . Conn . Write ( [ ] byte { 3 } )
2020-02-23 17:01:25 -06:00
if err != nil {
2022-05-24 09:52:25 -05:00
logger . Warnf ( "Failed to send CTRL+C: %+s" , err )
2020-02-23 17:01:25 -06:00
}
2022-05-24 09:52:25 -05:00
// we return the context canceled error to prevent other steps
// from executing
return ctx . Err ( )
case err := <- cmdResponse :
if err != nil {
logger . Error ( err )
2020-02-23 17:01:25 -06:00
}
2022-05-24 09:52:25 -05:00
return nil
2020-02-23 17:01:25 -06:00
}
}
2021-05-03 09:37:20 -05:00
func ( cr * containerReference ) copyDir ( dstPath string , srcPath string , useGitIgnore bool ) common . Executor {
2020-02-24 18:38:49 -06:00
return func ( ctx context . Context ) error {
logger := common . Logger ( ctx )
2022-10-29 12:15:38 -05:00
tarFile , err := os . CreateTemp ( "" , "act" )
2020-02-24 18:38:49 -06:00
if err != nil {
return err
}
2022-06-17 10:55:21 -05:00
logger . Debugf ( "Writing tarball %s from %s" , tarFile . Name ( ) , srcPath )
2022-04-04 10:27:00 -05:00
defer func ( tarFile * os . File ) {
name := tarFile . Name ( )
err := tarFile . Close ( )
2022-08-07 01:07:54 -05:00
if ! errors . Is ( err , os . ErrClosed ) {
2022-04-04 10:27:00 -05:00
logger . Error ( err )
}
err = os . Remove ( name )
if err != nil {
logger . Error ( err )
}
} ( tarFile )
2020-02-24 18:38:49 -06:00
tw := tar . NewWriter ( tarFile )
srcPrefix := filepath . Dir ( srcPath )
if ! strings . HasSuffix ( srcPrefix , string ( filepath . Separator ) ) {
srcPrefix += string ( filepath . Separator )
}
2022-06-17 10:55:21 -05:00
logger . Debugf ( "Stripping prefix:%s src:%s" , srcPrefix , srcPath )
2020-02-24 18:38:49 -06:00
2021-05-03 09:37:20 -05:00
var ignorer gitignore . Matcher
if useGitIgnore {
ps , err := gitignore . ReadPatterns ( polyfill . New ( osfs . New ( srcPath ) ) , nil )
if err != nil {
2022-06-17 10:55:21 -05:00
logger . Debugf ( "Error loading .gitignore: %v" , err )
2021-05-03 09:37:20 -05:00
}
2020-03-09 20:32:48 -05:00
2021-05-03 09:37:20 -05:00
ignorer = gitignore . NewMatcher ( ps )
}
2020-03-18 08:55:39 -05:00
2022-04-04 10:27:00 -05:00
fc := & fileCollector {
Fs : & defaultFs { } ,
Ignorer : ignorer ,
SrcPath : srcPath ,
SrcPrefix : srcPrefix ,
Handler : & tarCollector {
TarWriter : tw ,
2022-06-20 17:47:39 -05:00
UID : cr . UID ,
GID : cr . GID ,
DstDir : dstPath [ 1 : ] ,
2022-04-04 10:27:00 -05:00
} ,
}
2020-02-24 18:38:49 -06:00
2022-04-04 10:27:00 -05:00
err = filepath . Walk ( srcPath , fc . collectFiles ( ctx , [ ] string { } ) )
2020-02-24 18:38:49 -06:00
if err != nil {
return err
}
if err := tw . Close ( ) ; err != nil {
return err
}
logger . Debugf ( "Extracting content from '%s' to '%s'" , tarFile . Name ( ) , dstPath )
_ , err = tarFile . Seek ( 0 , 0 )
if err != nil {
2022-06-10 16:16:42 -05:00
return fmt . Errorf ( "failed to seek tar archive: %w" , err )
2020-02-24 18:38:49 -06:00
}
2022-06-20 17:47:39 -05:00
err = cr . cli . CopyToContainer ( ctx , cr . id , "/" , tarFile , types . CopyToContainerOptions { } )
2020-02-24 18:38:49 -06:00
if err != nil {
2022-06-10 16:16:42 -05:00
return fmt . Errorf ( "failed to copy content to container: %w" , err )
2020-02-24 18:38:49 -06:00
}
return nil
}
}
2020-02-23 17:01:25 -06:00
func ( cr * containerReference ) copyContent ( dstPath string , files ... * FileEntry ) common . Executor {
return func ( ctx context . Context ) error {
logger := common . Logger ( ctx )
var buf bytes . Buffer
tw := tar . NewWriter ( & buf )
for _ , file := range files {
2022-06-17 10:55:21 -05:00
logger . Debugf ( "Writing entry to tarball %s len:%d" , file . Name , len ( file . Body ) )
2020-02-23 17:01:25 -06:00
hdr := & tar . Header {
Name : file . Name ,
Mode : file . Mode ,
Size : int64 ( len ( file . Body ) ) ,
2022-06-20 17:47:39 -05:00
Uid : cr . UID ,
Gid : cr . GID ,
2020-02-23 17:01:25 -06:00
}
if err := tw . WriteHeader ( hdr ) ; err != nil {
return err
2020-02-07 00:17:58 -06:00
}
2020-02-23 17:01:25 -06:00
if _ , err := tw . Write ( [ ] byte ( file . Body ) ) ; err != nil {
return err
}
}
if err := tw . Close ( ) ; err != nil {
return err
}
logger . Debugf ( "Extracting content to '%s'" , dstPath )
err := cr . cli . CopyToContainer ( ctx , cr . id , dstPath , & buf , types . CopyToContainerOptions { } )
if err != nil {
2022-06-10 16:16:42 -05:00
return fmt . Errorf ( "failed to copy content to container: %w" , err )
2019-01-12 22:45:25 -06:00
}
2020-02-07 00:17:58 -06:00
return nil
2019-01-12 22:45:25 -06:00
}
}
2020-02-07 00:17:58 -06:00
func ( cr * containerReference ) attach ( ) common . Executor {
return func ( ctx context . Context ) error {
out , err := cr . cli . ContainerAttach ( ctx , cr . id , types . ContainerAttachOptions {
Stream : true ,
Stdout : true ,
Stderr : true ,
} )
if err != nil {
2022-06-10 16:16:42 -05:00
return fmt . Errorf ( "failed to attach to container: %w" , err )
2020-02-07 00:17:58 -06:00
}
2021-01-12 00:39:43 -06:00
isTerminal := term . IsTerminal ( int ( os . Stdout . Fd ( ) ) )
2020-02-11 11:10:35 -06:00
var outWriter io . Writer
outWriter = cr . input . Stdout
if outWriter == nil {
outWriter = os . Stdout
}
errWriter := cr . input . Stderr
if errWriter == nil {
errWriter = os . Stderr
2020-02-07 00:17:58 -06:00
}
2020-02-11 11:10:35 -06:00
go func ( ) {
if ! isTerminal || os . Getenv ( "NORAW" ) != "" {
_ , err = stdcopy . StdCopy ( outWriter , errWriter , out . Reader )
} else {
_ , err = io . Copy ( outWriter , out . Reader )
}
if err != nil {
common . Logger ( ctx ) . Error ( err )
}
} ( )
2020-02-07 00:17:58 -06:00
return nil
2019-01-12 22:45:25 -06:00
}
}
2020-02-07 00:17:58 -06:00
func ( cr * containerReference ) start ( ) common . Executor {
return func ( ctx context . Context ) error {
logger := common . Logger ( ctx )
2020-02-23 17:01:25 -06:00
logger . Debugf ( "Starting container: %v" , cr . id )
2019-01-12 22:45:25 -06:00
2020-02-07 00:17:58 -06:00
if err := cr . cli . ContainerStart ( ctx , cr . id , types . ContainerStartOptions { } ) ; err != nil {
2022-06-10 16:16:42 -05:00
return fmt . Errorf ( "failed to start container: %w" , err )
2020-02-07 00:17:58 -06:00
}
2019-01-12 22:45:25 -06:00
2020-02-07 00:17:58 -06:00
logger . Debugf ( "Started container: %v" , cr . id )
return nil
}
2019-01-12 22:45:25 -06:00
}
2020-02-07 00:17:58 -06:00
func ( cr * containerReference ) wait ( ) common . Executor {
return func ( ctx context . Context ) error {
logger := common . Logger ( ctx )
statusCh , errCh := cr . cli . ContainerWait ( ctx , cr . id , container . WaitConditionNotRunning )
var statusCode int64
select {
case err := <- errCh :
if err != nil {
2022-06-10 16:16:42 -05:00
return fmt . Errorf ( "failed to wait for container: %w" , err )
2020-02-07 00:17:58 -06:00
}
case status := <- statusCh :
statusCode = status . StatusCode
2019-01-12 22:45:25 -06:00
}
2020-02-07 00:17:58 -06:00
logger . Debugf ( "Return status: %v" , statusCode )
2019-01-12 22:45:25 -06:00
2020-02-07 00:17:58 -06:00
if statusCode == 0 {
return nil
}
2019-01-12 22:45:25 -06:00
2020-02-07 00:17:58 -06:00
return fmt . Errorf ( "exit with `FAILURE`: %v" , statusCode )
}
2019-01-12 22:45:25 -06:00
}