Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plugincontainer: Support mlock #94

Merged
merged 6 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 42 additions & 8 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,23 @@ jobs:
run: |
(
set -e
ARCH=$(uname -m)
URL=https://storage.googleapis.com/gvisor/releases/release/latest/${ARCH}
wget --quiet ${URL}/runsc ${URL}/runsc.sha512 \
${URL}/containerd-shim-runsc-v1 ${URL}/containerd-shim-runsc-v1.sha512
ARCH="$(uname -m)"
URL="https://storage.googleapis.com/gvisor/releases/release/latest/${ARCH}"
wget --quiet "${URL}/runsc" "${URL}/runsc.sha512" \
"${URL}/containerd-shim-runsc-v1" "${URL}/containerd-shim-runsc-v1.sha512"
sha512sum -c runsc.sha512 \
-c containerd-shim-runsc-v1.sha512
rm -f *.sha512
rm -f -- *.sha512
chmod a+rx runsc containerd-shim-runsc-v1
sudo mv runsc containerd-shim-runsc-v1 /usr/local/bin
)
cat | sudo tee /etc/docker/daemon.json <<EOF
sudo tee /etc/docker/daemon.json <<EOF
{
"runtimes": {
"runsc": {
"path": "/usr/local/bin/runsc",
"runtimeArgs": [
"--host-uds=all",
"--host-fifo=open"
"--host-uds=all"
]
}
}
Expand All @@ -68,6 +67,41 @@ jobs:

sudo systemctl reload docker

- name: Install rootless docker
if: ${{ matrix.module == 'plugincontainer' }}
run: |
sudo apt-get install -y uidmap dbus-user-session
export FORCE_ROOTLESS_INSTALL=1
curl -fsSL https://get.docker.com/rootless | sh
mkdir -p ~/.config/docker/
tee ~/.config/docker/daemon.json <<EOF
{
"runtimes": {
"runsc": {
"path": "/usr/local/bin/runsc",
"runtimeArgs": [
"--host-uds=all",
"--ignore-cgroups"
]
}
}
}
EOF
systemctl --user restart docker

- name: Install rootless podman
if: ${{ matrix.module == 'plugincontainer' }}
run: |
sudo apt-get install -y podman slirp4netns fuse-overlayfs
mkdir -p ~/local/bin
RUNSC_SCRIPT=~/local/bin/runsc.podman
tee "${RUNSC_SCRIPT}" <<EOF
#!/bin/bash
/usr/local/bin/runsc --host-uds=all --ignore-cgroups "\$@"
EOF
chmod u+x "${RUNSC_SCRIPT}"
podman --runtime "${RUNSC_SCRIPT}" system service -t 0 &

- name: Test
run: cd ${{ matrix.module }} && go test ./...

Expand Down
151 changes: 151 additions & 0 deletions plugincontainer/compatibility_matrix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package plugincontainer_test

import (
"fmt"
"os"
"testing"

"github.com/hashicorp/go-secure-stdlib/plugincontainer"
)

const (
engineDocker = "docker"
enginePodman = "podman"
runtimeRunc = "runc"
runtimeRunsc = "runsc"
)

type matrixInput struct {
containerEngine string
containerRuntime string
rootlessEngine bool
rootlessUser bool
mlock bool
}

func (m matrixInput) String() string {
var s string
if m.rootlessEngine {
s = "rootless "
}
s += m.containerEngine
// Podman does not support configuring the runtime from the SDK.
if m.containerEngine != enginePodman {
s += ":" + m.containerRuntime
}
if m.rootlessUser {
s += ":" + "nonroot"
}
if m.mlock {
s += ":" + "mlock"
}
return s
}

func TestCompatibilityMatrix(t *testing.T) {
runCmd(t, "go", "build", "-o=examples/container/go-plugin-counter", "./examples/container/plugin-counter")
tomhjp marked this conversation as resolved.
Show resolved Hide resolved

for _, engine := range []string{engineDocker, enginePodman} {
tomhjp marked this conversation as resolved.
Show resolved Hide resolved
for _, runtime := range []string{runtimeRunc, runtimeRunsc} {
for _, rootlessEngine := range []bool{true, false} {
for _, rootlessUser := range []bool{true, false} {
for _, mlock := range []bool{true, false} {
if engine == enginePodman && runtime == runtimeRunsc {
// Podman does not support configuring the runtime from the SDK,
// so only run 1 of the set of runtime test cases against it.
// TODO: See if we can run two instances of podman to support one
// runtime each.
continue
}
i := matrixInput{
containerEngine: engine,
containerRuntime: runtime,
rootlessEngine: rootlessEngine,
rootlessUser: rootlessUser,
mlock: mlock,
}
t.Run(i.String(), func(t *testing.T) {
runExamplePlugin(t, i)
})
}
}
}
}
}
}

func skipIfUnsupported(t *testing.T, i matrixInput) {
switch {
case i.rootlessEngine && i.rootlessUser:
t.Skip("Unix socket permissions not yet working for rootless engine + nonroot container user")
case i.containerEngine == enginePodman && !i.rootlessEngine:
t.Skip("TODO: These tests would pass but CI doesn't have the environment set up yet")
case i.mlock && i.rootlessEngine:
if i.containerEngine == engineDocker && i.containerRuntime == runtimeRunsc {
// runsc works in rootless because it has its own implementation of mlockall(2)
} else {
t.Skip("TODO: These tests should work if the rootless engine is given the IPC_LOCK capability")
}
}
}

func setDockerHost(t *testing.T, containerEngine string, rootlessEngine bool) {
var socketFile string
switch {
case containerEngine == engineDocker && !rootlessEngine:
socketFile = "/var/run/docker.sock"
case containerEngine == engineDocker && rootlessEngine:
socketFile = fmt.Sprintf("/run/user/%d/docker.sock", os.Getuid())
case containerEngine == enginePodman && !rootlessEngine:
socketFile = "/var/run/podman/podman.sock"
case containerEngine == enginePodman && rootlessEngine:
socketFile = fmt.Sprintf("/run/user/%d/podman/podman.sock", os.Getuid())
default:
t.Fatalf("Unsupported combination: %s, %v", containerEngine, rootlessEngine)
}
if _, err := os.Stat(socketFile); err != nil {
t.Fatal("Did not find expected socket file:", err)
}
t.Setenv("DOCKER_HOST", "unix://"+socketFile)
}

func runExamplePlugin(t *testing.T, i matrixInput) {
skipIfUnsupported(t, i)
setDockerHost(t, i.containerEngine, i.rootlessEngine)

imageRef := goPluginCounterImage
var tag string
target := "root"
if i.rootlessUser {
tag = "nonroot"
imageRef += ":" + tag
if i.mlock {
target = "nonroot-mlock"
} else {
target = "nonroot"
}
}
runCmd(t, i.containerEngine, "build", "--tag="+imageRef, "--target="+target, "--file=examples/container/Dockerfile", "examples/container")

cfg := &plugincontainer.Config{
Image: goPluginCounterImage,
Tag: tag,
GroupAdd: os.Getgid(),
Debug: true,

CapIPCLock: i.mlock,
}
if i.mlock {
cfg.Env = append(cfg.Env, "MLOCK=true")
}
if i.rootlessUser {
cfg.Tag = "nonroot"
}
if i.containerEngine != enginePodman {
cfg.Runtime = i.containerRuntime
}
exerciseExamplePlugin(t, cfg)
}
3 changes: 2 additions & 1 deletion plugincontainer/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ type Config struct {
Labels map[string]string // Arbitrary metadata to facilitate querying containers.

// container.HostConfig options
Runtime string // OCI runtime.
Runtime string // OCI runtime. NOTE: Has no effect if using podman's system service API
CgroupParent string // Parent Cgroup for the container
NanoCpus int64 // CPU quota in billionths of a CPU core
Memory int64 // Memory quota in bytes
CapIPCLock bool // Whether to add the capability IPC_LOCK, to allow the mlockall(2) syscall

// network.NetworkConfig options
EndpointsConfig map[string]*network.EndpointSettings // Endpoint configs for each connecting network
Expand Down
25 changes: 16 additions & 9 deletions plugincontainer/container_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var (
_ runner.Runner = (*containerRunner)(nil)

errUnsupportedOS = errors.New("plugincontainer currently only supports Linux")
errSHA256Mismatch = errors.New("SHA256 mismatch")
ErrSHA256Mismatch = errors.New("SHA256 mismatch")
)

const pluginSocketDir = "/tmp/go-plugin-container"
Expand Down Expand Up @@ -83,15 +83,18 @@ func (cfg *Config) NewContainerRunner(logger hclog.Logger, cmd *exec.Cmd, hostSo

// Default to using the SHA256 for secure pinning of images, but allow users
// to omit the SHA256 as well.
var imageArg string
var imageRef string
if sha256 != "" {
imageArg = "sha256:" + sha256
imageRef = "sha256:" + sha256
} else {
imageArg = cfg.Image
imageRef = cfg.Image
if cfg.Tag != "" {
imageRef += ":" + cfg.Tag
}
}
// Container config.
containerConfig := &container.Config{
Image: imageArg,
Image: imageRef,
Env: cmd.Env,
NetworkDisabled: cfg.DisableNetwork,
Labels: cfg.Labels,
Expand Down Expand Up @@ -142,6 +145,10 @@ func (cfg *Config) NewContainerRunner(logger hclog.Logger, cmd *exec.Cmd, hostSo
hostConfig.GroupAdd = append(hostConfig.GroupAdd, strconv.Itoa(cfg.GroupAdd))
}

if cfg.CapIPCLock {
hostConfig.CapAdd = append(hostConfig.CapAdd, "IPC_LOCK")
}

// Network config.
networkConfig := &network.NetworkingConfig{
EndpointsConfig: cfg.EndpointsConfig,
Expand Down Expand Up @@ -186,19 +193,19 @@ func (c *containerRunner) Start(ctx context.Context) error {
}
}
if !imageFound {
return fmt.Errorf("could not find any locally available images named %s that match with the provided SHA256 hash %s: %w", ref, c.sha256, errSHA256Mismatch)
return fmt.Errorf("could not find any locally available images named %s that match with the provided SHA256 hash %s: %w", ref, c.sha256, ErrSHA256Mismatch)
}
}

resp, err := c.dockerClient.ContainerCreate(ctx, c.containerConfig, c.hostConfig, c.networkConfig, nil, "")
if err != nil {
return err
return fmt.Errorf("error creating container: %w", err)
}
c.id = resp.ID
c.logger.Trace("created container", "image", c.image, "id", c.id)

if err := c.dockerClient.ContainerStart(ctx, c.id, types.ContainerStartOptions{}); err != nil {
return err
return fmt.Errorf("error starting container: %w", err)
}

// ContainerLogs combines stdout and stderr.
Expand Down Expand Up @@ -296,7 +303,7 @@ func (c *containerRunner) Stderr() io.ReadCloser {
func (c *containerRunner) PluginToHost(pluginNet, pluginAddr string) (hostNet string, hostAddr string, err error) {
if path.Dir(pluginAddr) != pluginSocketDir {
return "", "", fmt.Errorf("expected address to be in directory %s, but was %s; "+
"the plugin may need to be recompiled with the latest go-plugin version", c.hostSocketDir, pluginAddr)
"the plugin may need to be recompiled with the latest go-plugin version", pluginSocketDir, pluginAddr)
}
return pluginNet, path.Join(c.hostSocketDir, path.Base(pluginAddr)), nil
}
Expand Down
Loading