From d5915243add8fa8f5179aca9cb5c2ae1be95228a Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sun, 12 Mar 2023 14:26:24 +0100 Subject: [PATCH] [FORGEJO] wrap self-hosted platform steps in an LXC container act PR https://github.com/nektos/act/pull/1682 * shell script to start the LXC container * create and destroy a LXC container * run commands with lxc-attach * expose additional devices for docker & libvirt to work * install node 16 & git for checkout to work [FORGEJO] start/stop lxc working directory is /tmp [FORGEJO] use lxc-helpers to create/destroy containers [FORGEJO] do not setup LXC (cherry picked from commit c2eaf440f591cc8800ff59fef9e155d2904e0e37) Conflicts: pkg/container/host_environment.go Conflicts: pkg/container/host_environment.go [FORGJEO] upgrade to node20 --- pkg/container/executions_environment.go | 2 + pkg/container/host_environment.go | 33 +++- .../linux_container_environment_extensions.go | 11 +- pkg/runner/run_context.go | 147 +++++++++++++++++- 4 files changed, 183 insertions(+), 10 deletions(-) diff --git a/pkg/container/executions_environment.go b/pkg/container/executions_environment.go index 41e3b57..87e652b 100644 --- a/pkg/container/executions_environment.go +++ b/pkg/container/executions_environment.go @@ -5,6 +5,8 @@ import "context" type ExecutionsEnvironment interface { Container ToContainerPath(string) string + GetName() string + GetRoot() string GetActPath() string GetPathVariableName() string DefaultPathVariable() string diff --git a/pkg/container/host_environment.go b/pkg/container/host_environment.go index bdda16c..7093c06 100644 --- a/pkg/container/host_environment.go +++ b/pkg/container/host_environment.go @@ -26,16 +26,18 @@ import ( ) type HostEnvironment struct { + Name string Path string TmpDir string ToolCache string Workdir string ActPath string + Root string CleanUp func() StdOut io.Writer } -func (e *HostEnvironment) Create(_ []string, _ []string) common.Executor { +func (e *HostEnvironment) Create(_, _ []string) common.Executor { return func(ctx context.Context) error { return nil } @@ -94,7 +96,7 @@ func (e *HostEnvironment) CopyTarStream(ctx context.Context, destPath string, ta } } -func (e *HostEnvironment) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor { +func (e *HostEnvironment) CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor { return func(ctx context.Context) error { logger := common.Logger(ctx) srcPrefix := filepath.Dir(srcPath) @@ -288,7 +290,7 @@ func getEnvListFromMap(env map[string]string) []string { return envList } -func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline string, env map[string]string, _, workdir string) error { +func (e *HostEnvironment) exec(ctx context.Context, commandparam []string, cmdline string, env map[string]string, user, workdir string) error { envList := getEnvListFromMap(env) var wd string if workdir != "" { @@ -300,6 +302,19 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st } else { wd = e.Path } + if _, err := os.Stat(wd); err != nil { + common.Logger(ctx).Debugf("Failed to stat working directory %s %v\n", wd, err.Error()) + } + + command := make([]string, len(commandparam)) + copy(command, commandparam) + if user == "root" { + command = append([]string{"/usr/bin/sudo"}, command...) + } else { + common.Logger(ctx).Debugf("lxc-attach --name %v %v", e.Name, command) + command = append([]string{"/usr/bin/sudo", "--preserve-env", "--preserve-env=PATH", "/usr/bin/lxc-attach", "--keep-env", "--name", e.Name, "--"}, command...) + } + f, err := lookupPathHost(command[0], env, e.StdOut) if err != nil { return err @@ -342,7 +357,7 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st } err = cmd.Run() if err != nil { - return err + return fmt.Errorf("RUN %w", err) } if tty != nil { writer.AutoStop = true @@ -399,6 +414,14 @@ func (e *HostEnvironment) ToContainerPath(path string) string { return path } +func (e *HostEnvironment) GetName() string { + return e.Name +} + +func (e *HostEnvironment) GetRoot() string { + return e.Root +} + func (e *HostEnvironment) GetActPath() string { actPath := e.ActPath if runtime.GOOS == "windows" { @@ -458,7 +481,7 @@ func (e *HostEnvironment) GetRunnerContext(_ context.Context) map[string]interfa } } -func (e *HostEnvironment) ReplaceLogWriter(stdout io.Writer, _ io.Writer) (io.Writer, io.Writer) { +func (e *HostEnvironment) ReplaceLogWriter(stdout, _ io.Writer) (io.Writer, io.Writer) { org := e.StdOut e.StdOut = stdout return org, org diff --git a/pkg/container/linux_container_environment_extensions.go b/pkg/container/linux_container_environment_extensions.go index d673451..3518554 100644 --- a/pkg/container/linux_container_environment_extensions.go +++ b/pkg/container/linux_container_environment_extensions.go @@ -10,8 +10,7 @@ import ( log "github.com/sirupsen/logrus" ) -type LinuxContainerEnvironmentExtensions struct { -} +type LinuxContainerEnvironmentExtensions struct{} // Resolves the equivalent host path inside the container // This is required for windows and WSL 2 to translate things like C:\Users\Myproject to /mnt/users/Myproject @@ -47,6 +46,14 @@ func (*LinuxContainerEnvironmentExtensions) ToContainerPath(path string) string return result } +func (*LinuxContainerEnvironmentExtensions) GetName() string { + return "NAME" +} + +func (*LinuxContainerEnvironmentExtensions) GetRoot() string { + return "/var/run" +} + func (*LinuxContainerEnvironmentExtensions) GetActPath() string { return "/var/run/act" } diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 5c8187a..daa8b33 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -3,9 +3,11 @@ package runner import ( "archive/tar" "bufio" + "bytes" "context" "crypto/rand" "crypto/sha256" + _ "embed" "encoding/hex" "encoding/json" "errors" @@ -16,6 +18,7 @@ import ( "regexp" "runtime" "strings" + "text/template" "github.com/docker/go-connections/nat" "github.com/nektos/act/pkg/common" @@ -183,6 +186,94 @@ func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) { return binds, mounts } +//go:embed lxc-helpers-lib.sh +var lxcHelpersLib string + +//go:embed lxc-helpers.sh +var lxcHelpers string + +var startTemplate = template.Must(template.New("start").Parse(`#!/bin/bash -e +source $(dirname $0)/lxc-helpers-lib.sh + +LXC_CONTAINER_RELEASE="{{.Release}}" + +function template_act() { + echo $(lxc_template_release)-act +} + +function install_nodejs() { + local name="$1" + + local script=/usr/local/bin/lxc-helpers-install-node.sh + + cat > $(lxc_root $name)/$script <<'EOF' +#!/bin/sh -e +# https://github.com/nodesource/distributions#debinstall +export DEBIAN_FRONTEND=noninteractive +apt-get install -qq -y ca-certificates curl gnupg git +mkdir -p /etc/apt/keyrings +curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg +NODE_MAJOR=20 +echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list +apt-get update -qq +apt-get install -qq -y nodejs +EOF + lxc_container_run_script $name $script +} + +function build_template_act() { + local name="$(template_act)" + + if lxc_exists $name ; then + return + fi + + lxc_build_template $(lxc_template_release) $name + lxc_container_start $name + install_nodejs $name + lxc_container_stop $name +} + +lxc_prepare_environment +build_template_act +lxc_build_template $(template_act) "{{.Name}}" +lxc_container_mount "{{.Name}}" "{{ .Root }}" +lxc_container_start "{{.Name}}" +`)) + +var stopTemplate = template.Must(template.New("stop").Parse(`#!/bin/bash +source $(dirname $0)/lxc-helpers-lib.sh + +lxc_container_destroy "{{.Name}}" +`)) + +func (rc *RunContext) stopHostEnvironment() common.Executor { + return func(ctx context.Context) error { + logger := common.Logger(ctx) + logger.Debugf("stopHostEnvironment") + + var stopScript bytes.Buffer + if err := stopTemplate.Execute(&stopScript, struct { + Name string + Root string + }{ + Name: rc.JobContainer.GetName(), + Root: rc.JobContainer.GetRoot(), + }); err != nil { + return err + } + + return common.NewPipelineExecutor( + rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{ + Name: "workflow/stop-lxc.sh", + Mode: 0755, + Body: stopScript.String(), + }), + rc.JobContainer.Exec([]string{rc.JobContainer.GetActPath() + "/workflow/stop-lxc.sh"}, map[string]string{}, "root", "/tmp"), + )(ctx) + } +} + func (rc *RunContext) startHostEnvironment() common.Executor { return func(ctx context.Context) error { logger := common.Logger(ctx) @@ -198,7 +289,8 @@ func (rc *RunContext) startHostEnvironment() common.Executor { cacheDir := rc.ActionCacheDir() randBytes := make([]byte, 8) _, _ = rand.Read(randBytes) - miscpath := filepath.Join(cacheDir, hex.EncodeToString(randBytes)) + randName := hex.EncodeToString(randBytes) + miscpath := filepath.Join(cacheDir, randName) actPath := filepath.Join(miscpath, "act") if err := os.MkdirAll(actPath, 0o777); err != nil { return err @@ -213,6 +305,8 @@ func (rc *RunContext) startHostEnvironment() common.Executor { } toolCache := filepath.Join(cacheDir, "tool_cache") rc.JobContainer = &container.HostEnvironment{ + Name: randName, + Root: miscpath, Path: path, TmpDir: runnerTmp, ToolCache: toolCache, @@ -238,7 +332,44 @@ func (rc *RunContext) startHostEnvironment() common.Executor { } } + var startScript bytes.Buffer + if err := startTemplate.Execute(&startScript, struct { + Name string + Template string + Release string + Repo string + Root string + TmpDir string + Script string + }{ + Name: rc.JobContainer.GetName(), + Template: "debian", + Release: "bullseye", + Repo: "", // step.Environment["CI_REPO"], + Root: rc.JobContainer.GetRoot(), + TmpDir: runnerTmp, + Script: "", // "commands-" + step.Name, + }); err != nil { + return err + } + return common.NewPipelineExecutor( + rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{ + Name: "workflow/lxc-helpers-lib.sh", + Mode: 0755, + Body: lxcHelpersLib, + }), + rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{ + Name: "workflow/lxc-helpers.sh", + Mode: 0755, + Body: lxcHelpers, + }), + rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{ + Name: "workflow/start-lxc.sh", + Mode: 0755, + Body: startScript.String(), + }), + rc.JobContainer.Exec([]string{rc.JobContainer.GetActPath() + "/workflow/start-lxc.sh"}, map[string]string{}, "root", "/tmp"), rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{ Name: "workflow/event.json", Mode: 0o644, @@ -601,12 +732,22 @@ func (rc *RunContext) IsHostEnv(ctx context.Context) bool { } func (rc *RunContext) stopContainer() common.Executor { - return rc.stopJobContainer() + return func(ctx context.Context) error { + image := rc.platformImage(ctx) + if strings.EqualFold(image, "-self-hosted") { + return rc.stopHostEnvironment()(ctx) + } + return rc.stopJobContainer()(ctx) + } } func (rc *RunContext) closeContainer() common.Executor { return func(ctx context.Context) error { if rc.JobContainer != nil { + image := rc.platformImage(ctx) + if strings.EqualFold(image, "-self-hosted") { + return rc.stopHostEnvironment()(ctx) + } return rc.JobContainer.Close()(ctx) } return nil @@ -628,7 +769,7 @@ func (rc *RunContext) steps() []*model.Step { // Executor returns a pipeline executor for all the steps in the job func (rc *RunContext) Executor() (common.Executor, error) { var executor common.Executor - var jobType, err = rc.Run.Job().Type() + jobType, err := rc.Run.Job().Type() switch jobType { case model.JobTypeDefault: