Skip to content

Draft: LXC support for self-hosted runners#1682

Closed
earl-warren wants to merge 6 commits intonektos:masterfrom
earl-warren:wip-github-lxc
Closed

Draft: LXC support for self-hosted runners#1682
earl-warren wants to merge 6 commits intonektos:masterfrom
earl-warren:wip-github-lxc

Conversation

@earl-warren
Copy link

@earl-warren earl-warren commented Mar 16, 2023

Description

The LXC support for self-hosted runners is used to run tests that do not fit the constraints imposed by the docker backend such as having a systemd capable environment.

It creates a container from scratch on every run. If the tests accidentally damage essential services such as the ssh server, it will not have any impact on the host running the LXC container. If the same accident happens without the isolation provided by the LXC container, the host itself will be damaged. The LXC support provides a robust isolation for each job in the workflows, which the self-hosted platform does not.

Implementation details

It is roughly the equivalent of doing the following:

  • Creating a LXC container (with lxc-create)
  • Running the commands in the container (with lxc-attach)
  • Destroying the LXC container (with lxc-destroy)

This is inherently insecure, in the same way the self-hosted platform is. Hardening LXC containers is possible but it makes them no more useful than docker containers.

FAQ

  • Why not run LXC inside a Docker container? Because it does not work. Although it could probably be hacked, that would not be more than that.
  • Why not run the LXC logic from the sef-hosted platform? Because it would require significant work and LXC knowledge from each ACT user. Instead they only have to care for their own tests, not to setup and teardown well isolated LXC containers.
  • Why not run the LXC containers as unprivileged to improve security? Because it does not work with most templates. Given the restrictions it imposes, the user is better of using Docker.
  • Why is apparmor disabled? Because it cannot be configured to allow essential operations such as mounting file systems

@earl-warren earl-warren requested a review from a team as a code owner March 16, 2023 11:37
@earl-warren
Copy link
Author

This is an early draft go gauge interest and I'm ready to do the needful if there is. It is developed and used in the context of the https://forgejo.org project.

Comment on lines +144 to +213
var startTemplate = template.Must(template.New("start").Parse(`#!/bin/sh -xe
lxc-create --name="{{.Name}}" --template={{.Template}} -- --release {{.Release}} $packages
tee -a /var/lib/lxc/{{.Name}}/config <<'EOF'
security.nesting = true
lxc.cap.drop =
lxc.apparmor.profile = unconfined
#
# /dev/net (docker won't work without /dev/net/tun)
#
lxc.cgroup2.devices.allow = c 10:200 rwm
lxc.mount.entry = /dev/net dev/net none bind,create=dir 0 0
#
# /dev/kvm (libvirt / kvm won't work without /dev/kvm)
#
lxc.cgroup2.devices.allow = c 10:232 rwm
lxc.mount.entry = /dev/kvm dev/kvm none bind,create=file 0 0
#
# /dev/loop
#
lxc.cgroup2.devices.allow = c 10:237 rwm
lxc.cgroup2.devices.allow = b 7:* rwm
lxc.mount.entry = /dev/loop-control dev/loop-control none bind,create=file 0 0
#
# /dev/mapper
#
lxc.cgroup2.devices.allow = c 10:236 rwm
lxc.mount.entry = /dev/mapper dev/mapper none bind,create=dir 0 0
#
# /dev/fuse
#
lxc.cgroup2.devices.allow = b 10:229 rwm
lxc.mount.entry = /dev/fuse dev/fuse none bind,create=file 0 0
EOF

mkdir -p /var/lib/lxc/{{.Name}}/rootfs/{{ .Root }}
mount --bind {{ .Root }} /var/lib/lxc/{{.Name}}/rootfs/{{ .Root }}

mkdir /var/lib/lxc/{{.Name}}/rootfs/tmpdir
mount --bind {{.TmpDir}} /var/lib/lxc/{{.Name}}/rootfs/tmpdir

lxc-start {{.Name}}
lxc-wait --name {{.Name}} --state RUNNING

#
# Wait for the network to come up
#
cat > /var/lib/lxc/{{.Name}}/rootfs/tmpdir/networking.sh <<'EOF'
#!/bin/sh -xe
for d in $(seq 60); do
getent hosts wikipedia.org > /dev/null && break
sleep 1
done
getent hosts wikipedia.org
EOF
chmod +x /var/lib/lxc/{{.Name}}/rootfs/tmpdir/networking.sh

lxc-attach --name {{.Name}} -- /tmpdir/networking.sh

cat > /var/lib/lxc/{{.Name}}/rootfs/tmpdir/node.sh <<'EOF'
#!/bin/sh -xe
# https://github.com/nodesource/distributions#debinstall
apt-get install -y curl git
curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
apt-get install -y nodejs
EOF
chmod +x /var/lib/lxc/{{.Name}}/rootfs/tmpdir/node.sh

lxc-attach --name {{.Name}} -- /tmpdir/node.sh

`))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be separate file an embedded into go

Comment on lines +215 to +222
var stopTemplate = template.Must(template.New("stop").Parse(`#!/bin/sh -x
lxc-ls -1 --filter="^{{.Name}}" | while read container ; do
lxc-stop --kill --name="$container"
umount "/var/lib/lxc/$container/rootfs/{{ .Root }}"
umount "/var/lib/lxc/$container/rootfs/tmpdir"
lxc-destroy --force --name="$container"
done
`))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

command := make([]string, len(commandparam))
copy(command, commandparam)
if user == "root" {
command = append([]string{"/usr/bin/sudo"}, command...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hardcoded sudo

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...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@panekj
Copy link
Contributor

panekj commented Mar 16, 2023

This should be separate backend from host

@KnisterPeter
Copy link
Member

@earl-warren This sounds very interesting. I haven't read through the code but I would like to see it integrated into act. I guess we need to discuss about the structure and interfaces a bit more. It's not always clearly separated currently.

@ChristopherHX might have some ideas and expectations here as well.

@earl-warren
Copy link
Author

earl-warren commented Mar 23, 2023

Great to hear 🎉

It could also be an optional feature of the self-hosted platform. If LXC is available and the option is set, wrap it into a LXC container for better isolation. Otherwise not. It would be entirely transparent to the workflows being run.

@ChristopherHX is is something you think to be a valuable addition to the self-hosted platform you implemented? Or would you prefer to go in another direction?

@ChristopherHX
Copy link
Contributor

ChristopherHX commented Mar 23, 2023

@ChristopherHX is is something you think to be a valuable addition to the self-hosted platform you implemented? Or would you prefer to go in another direction?

I primary made my self-hosted for platforms like freebsd, openbsd, plan9 etc. where docker is not a thing (and GitHub Actions doesn't support), so this should be a different implemention of the ExecutionsEnvironment interface.

I prefer that you create a new struct, which overrides (in golang it is probably a different wording, using interfaces it should work) some HostEnvironment functions. Then instantiate it via a custom identifier from the runcontext, maybe -lxc:<baseimage> instead of -self-hosted ( needs design consent, it took long for my PR to land ).

I don't think we need to add Root() and Name() to the interface. You can override the Close() executor inside the struct with access to custom fields. The start method also have access to the fields, if you create a local var with the specfic type and later assign it to jobcontainer.

@earl-warren
Copy link
Author

Thanks for the feedback @ChristopherHX

I prefer that you create a new struct, which overrides (in golang it is probably a different wording, using interfaces it should work) some HostEnvironment functions. Then instantiate it via a custom identifier from the runcontext, maybe -lxc: instead of -self-hosted ( needs design consent, it took long for my PR to land ).

I'm happy to go in this direction. The current implementation is more of a proof of concept / hack than anything else and reworking it entirely is what I was expecting anyways.

If anyone think it should go differently please speak up. Otherwise I'll start working as soon as time permits, which could be as soon as in two weeks from now.

@mergify
Copy link
Contributor

mergify bot commented Jul 17, 2023

@earl-warren this pull request is now in conflict 😩

@mergify mergify bot added the conflict PR has conflicts label Jul 17, 2023
@earl-warren
Copy link
Author

I'm still motivated, it just takes a little longer than expected.

@ChristopherHX
Copy link
Contributor

The HostEnvironment took me ca. 1,5 years (1 year in my fork, 1/2 years in review) to get in here. You can still be faster than me 😅.

@github-actions
Copy link
Contributor

PR is stale and will be closed in 14 days unless there is new activity

@github-actions
Copy link
Contributor

PR is stale and will be closed in 14 days unless there is new activity

@github-actions github-actions bot added the stale label Jul 14, 2024
@github-actions github-actions bot closed this Jul 29, 2024
@mergify mergify bot removed the conflict PR has conflicts label Jul 29, 2024
@ChristopherHX
Copy link
Contributor

......https://github.com/nektos/act/tree/act-lxc

Maybe I continue here someday, some stuff from here has been ported. (This is based on my macOS tart addon)

However the sudo and script gen stuff doesn't make it straight forward.

Tbh, I need more disk space / do that on a raspberry pi to actually make that run like the macOS tart.

@YOU54F
Copy link

YOU54F commented Sep 5, 2024

I was watching this, as it looks really interesting.

I was using lxd for ephemeral gh runners

Leveraging macpine which is a macos tool in go that creates alpine vms with qemu (similar to lima)

https://beringresearch.github.io/macpine/lxd_macpine/

Tbh, I need more disk space / do that on a raspberry pi to actually make that run like the macOS tart.

☝🏾 this! I ended up buying a 4tb nvme and external tb4 dock to satisfy chonki vm's and my desire to create many of them 😅

@ChristopherHX
Copy link
Contributor

ChristopherHX commented Sep 7, 2024

I consider to not use the lxc-* commands and instead use lxd api.

Found this https://pkg.go.dev/github.com/lxc/lxd/client

Additionally I plan to not use bind mounts for act, instead make use of sftp to copy data

hmm lxd has been forked because of canonical changing the license..

project-mirrors-bot-tu bot pushed a commit to project-mirrors/forgejo-runner-as-gitea-act-runner-fork that referenced this pull request Jul 28, 2025
act PR nektos/act#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 5b94ff3226848791b93e72d2e0f0ee4bba29a989)

Conflicts:
	pkg/container/host_environment.go

Conflicts:
	pkg/container/host_environment.go

[FORGJEO] upgrade to node20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants