[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
This commit is contained in:
Earl Warren 2023-03-12 14:26:24 +01:00
parent 9ede3e638c
commit c2eaf440f5
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
4 changed files with 177 additions and 9 deletions

View file

@ -5,6 +5,8 @@ import "context"
type ExecutionsEnvironment interface { type ExecutionsEnvironment interface {
Container Container
ToContainerPath(string) string ToContainerPath(string) string
GetName() string
GetRoot() string
GetActPath() string GetActPath() string
GetPathVariableName() string GetPathVariableName() string
DefaultPathVariable() string DefaultPathVariable() string

View file

@ -25,16 +25,18 @@ import (
) )
type HostEnvironment struct { type HostEnvironment struct {
Name string
Path string Path string
TmpDir string TmpDir string
ToolCache string ToolCache string
Workdir string Workdir string
ActPath string ActPath string
Root string
CleanUp func() CleanUp func()
StdOut io.Writer StdOut io.Writer
} }
func (e *HostEnvironment) Create(capAdd []string, capDrop []string) common.Executor { func (e *HostEnvironment) Create(capAdd, capDrop []string) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
return nil return nil
} }
@ -66,7 +68,7 @@ func (e *HostEnvironment) Copy(destPath string, files ...*FileEntry) common.Exec
} }
} }
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 { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
srcPrefix := filepath.Dir(srcPath) srcPrefix := filepath.Dir(srcPath)
@ -260,7 +262,7 @@ func getEnvListFromMap(env map[string]string) []string {
return envList return envList
} }
func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline string, env map[string]string, user, 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) envList := getEnvListFromMap(env)
var wd string var wd string
if workdir != "" { if workdir != "" {
@ -272,6 +274,19 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
} else { } else {
wd = e.Path 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) f, err := lookupPathHost(command[0], env, e.StdOut)
if err != nil { if err != nil {
return err return err
@ -314,7 +329,7 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
} }
err = cmd.Run() err = cmd.Run()
if err != nil { if err != nil {
return err return fmt.Errorf("RUN %w", err)
} }
if tty != nil { if tty != nil {
writer.AutoStop = true writer.AutoStop = true
@ -367,6 +382,14 @@ func (e *HostEnvironment) ToContainerPath(path string) string {
return path return path
} }
func (e *HostEnvironment) GetName() string {
return e.Name
}
func (e *HostEnvironment) GetRoot() string {
return e.Root
}
func (e *HostEnvironment) GetActPath() string { func (e *HostEnvironment) GetActPath() string {
actPath := e.ActPath actPath := e.ActPath
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@ -426,7 +449,7 @@ func (e *HostEnvironment) GetRunnerContext(ctx context.Context) map[string]inter
} }
} }
func (e *HostEnvironment) ReplaceLogWriter(stdout io.Writer, stderr io.Writer) (io.Writer, io.Writer) { func (e *HostEnvironment) ReplaceLogWriter(stdout, stderr io.Writer) (io.Writer, io.Writer) {
org := e.StdOut org := e.StdOut
e.StdOut = stdout e.StdOut = stdout
return org, org return org, org

View file

@ -10,8 +10,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
type LinuxContainerEnvironmentExtensions struct { type LinuxContainerEnvironmentExtensions struct{}
}
// Resolves the equivalent host path inside the container // 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 // 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 return result
} }
func (*LinuxContainerEnvironmentExtensions) GetName() string {
return "NAME"
}
func (*LinuxContainerEnvironmentExtensions) GetRoot() string {
return "/var/run"
}
func (*LinuxContainerEnvironmentExtensions) GetActPath() string { func (*LinuxContainerEnvironmentExtensions) GetActPath() string {
return "/var/run/act" return "/var/run/act"
} }

View file

@ -3,9 +3,11 @@ package runner
import ( import (
"archive/tar" "archive/tar"
"bufio" "bufio"
"bytes"
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
_ "embed"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
@ -16,6 +18,7 @@ import (
"regexp" "regexp"
"runtime" "runtime"
"strings" "strings"
"text/template"
"time" "time"
"github.com/opencontainers/selinux/go-selinux" "github.com/opencontainers/selinux/go-selinux"
@ -178,6 +181,89 @@ func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) {
return binds, mounts 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 -xe
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 -xe
# https://github.com/nodesource/distributions#debinstall
apt-get install -qq -y curl git
curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
apt-get install -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 -x
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 { func (rc *RunContext) startHostEnvironment() common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
@ -193,7 +279,8 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
cacheDir := rc.ActionCacheDir() cacheDir := rc.ActionCacheDir()
randBytes := make([]byte, 8) randBytes := make([]byte, 8)
_, _ = rand.Read(randBytes) _, _ = rand.Read(randBytes)
miscpath := filepath.Join(cacheDir, hex.EncodeToString(randBytes)) randName := hex.EncodeToString(randBytes)
miscpath := filepath.Join(cacheDir, randName)
actPath := filepath.Join(miscpath, "act") actPath := filepath.Join(miscpath, "act")
if err := os.MkdirAll(actPath, 0o777); err != nil { if err := os.MkdirAll(actPath, 0o777); err != nil {
return err return err
@ -208,6 +295,8 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
} }
toolCache := filepath.Join(cacheDir, "tool_cache") toolCache := filepath.Join(cacheDir, "tool_cache")
rc.JobContainer = &container.HostEnvironment{ rc.JobContainer = &container.HostEnvironment{
Name: randName,
Root: miscpath,
Path: path, Path: path,
TmpDir: runnerTmp, TmpDir: runnerTmp,
ToolCache: toolCache, ToolCache: toolCache,
@ -233,7 +322,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( 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{ rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
Name: "workflow/event.json", Name: "workflow/event.json",
Mode: 0o644, Mode: 0o644,
@ -546,12 +672,22 @@ func (rc *RunContext) IsHostEnv(ctx context.Context) bool {
} }
func (rc *RunContext) stopContainer() common.Executor { 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 { func (rc *RunContext) closeContainer() common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
if rc.JobContainer != nil { if rc.JobContainer != nil {
image := rc.platformImage(ctx)
if strings.EqualFold(image, "-self-hosted") {
return rc.stopHostEnvironment()(ctx)
}
return rc.JobContainer.Close()(ctx) return rc.JobContainer.Close()(ctx)
} }
return nil return nil