Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
38 changes: 1 addition & 37 deletions core/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,7 @@

package config

import (
"fmt"
"strings"

"drassi.run/core/pkg/sandboxer"
"drassi.run/core/pkg/sandboxer/container"
"drassi.run/core/pkg/sandboxer/host"
"drassi.run/core/pkg/sandboxer/incus"
"github.com/pelletier/go-toml/v2"
"github.com/pelletier/go-toml/v2/unstable"
)
import "github.com/pelletier/go-toml/v2/unstable"

type Config[R any] struct {
Runner *R `toml:"runner" json:"runner"`
Expand Down Expand Up @@ -55,29 +45,3 @@ const (
ProviderHost = "host"
ProviderIncus = "incus"
)

func NewSandboxerEngine(config *Sandboxer) (sandboxer.Engine, error) {
provider := strings.ToLower(config.Provider)
switch provider {
case ProviderContainer:
cfg := container.DefaultConfig()
if err := toml.Unmarshal(config.Config, cfg); err != nil {
return nil, err
}
return container.New(cfg)
case ProviderHost:
cfg := host.DefaultConfig()
if err := toml.Unmarshal(config.Config, cfg); err != nil {
return nil, err
}
return host.New(cfg)
case ProviderIncus:
cfg := incus.DefaultConfig()
if err := toml.Unmarshal(config.Config, cfg); err != nil {
return nil, err
}
return incus.New(cfg)
default:
return nil, fmt.Errorf("unsupported sandboxer provider %q", config.Provider)
}
}
5 changes: 5 additions & 0 deletions core/pkg/container/cli/parse_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ func parseMount(m dockermount.Mount) *types.Mount {
mount.VolumeOptions.Options = dc.Options
}
}
if io := m.ImageOptions; io != nil {
mount.ImageOptions = &types.ImageOptions{
Subpath: io.Subpath,
}
}
if to := m.TmpfsOptions; to != nil {
mount.TmpfsOptions = &types.TmpfsOptions{
Size: to.SizeBytes,
Expand Down
11 changes: 11 additions & 0 deletions core/pkg/container/docker/config_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ func (cc *containerConfig) setMounts(volumes []*types.Mount) {
Subpath: volume.SubPath,
}
}
if image := v.ImageOptions; image != nil {
m.ImageOptions = &dockermount.ImageOptions{
Subpath: image.Subpath,
}
}
if tmpfs := v.TmpfsOptions; tmpfs != nil {
m.TmpfsOptions = &dockermount.TmpfsOptions{
SizeBytes: tmpfs.Size,
Expand Down Expand Up @@ -150,6 +155,12 @@ func (cs *containerSpec) setMounts(mounts []dockermount.Mount) {
opts.Options = driver.Options
}
}
case dockermount.TypeImage:
if image := m.ImageOptions; image != nil {
mount.ImageOptions = &types.ImageOptions{
Subpath: image.Subpath,
}
}
case dockermount.TypeTmpfs:
if tmpfs := m.TmpfsOptions; tmpfs != nil {
mount.TmpfsOptions = &types.TmpfsOptions{
Expand Down
10 changes: 9 additions & 1 deletion core/pkg/container/types/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ type ContainerStorage struct {
// - [github.com/moby/moby/api/types/container.MountPoint]
// - [github.com/compose-spec/compose-go/v2/types.ServiceVolumeConfig]
type Mount struct {
Type string // "bind", "volume", "tmpfs"
Type string // "bind", "volume", "image", "tmpfs"
Source string
Target string
ReadOnly bool

BindOptions *BindOptions
VolumeOptions *VolumeOptions
ImageOptions *ImageOptions
TmpfsOptions *TmpfsOptions
}

Expand Down Expand Up @@ -54,6 +55,13 @@ type VolumeOptions struct {
Options map[string]string
}

// ImageOptions represents the options for a mount of type "image".
// - [github.com/moby/moby/api/types/mount.ImageOptions]
// - [github.com/compose-spec/compose-go/v2/types.ServiceVolumeImage]
type ImageOptions struct {
Subpath string
}

// TmpfsOptions defines options specific to mounts of type "tmpfs".
// - [github.com/moby/moby/api/types/mount.TmpfsOptions]
// - [github.com/compose-spec/compose-go/v2/types.ServiceVolumeTmpfs]
Expand Down
15 changes: 15 additions & 0 deletions core/pkg/sandboxer/container/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strconv"
"strings"

"drassi.run/core/config"
"drassi.run/core/pkg/container"
"drassi.run/core/pkg/container/cli"
"drassi.run/core/pkg/container/docker"
Expand All @@ -23,9 +24,23 @@ import (
"drassi.run/core/pkg/stream"
"drassi.run/core/util/string"
dockerclient "github.com/moby/moby/client"
"github.com/pelletier/go-toml/v2"
"github.com/pelletier/go-toml/v2/unstable"
"golang.org/x/sync/errgroup"
)

func init() {
sandboxer.Register(config.ProviderContainer, func(raw unstable.RawMessage) (sandboxer.Engine, error) {
cfg := DefaultConfig()
if len(raw) > 0 {
if err := toml.Unmarshal(raw, cfg); err != nil {
return nil, err
}
}
return New(cfg)
})
}

type Bootstrapper interface {
Bootstrap(ctx context.Context, sb sandboxer.Sandbox, req *sandboxer.LaunchRequest) (*sandboxer.LaunchResponse, error)
}
Expand Down
71 changes: 71 additions & 0 deletions core/pkg/sandboxer/container/operation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: (c) 2024 The Drassi Authors
*
* SPDX-License-Identifier: Apache-2.0
*/

package container

import (
"fmt"
"path/filepath"

"drassi.run/core/pkg/container/types"
"drassi.run/core/pkg/runtime/provision"
)

type addBindMountOp struct {
provision.Noop
spec *types.ContainerSpec
}

func AddBindMount(spec *types.ContainerSpec) provision.Operation {
return &addBindMountOp{spec: spec}
}

func (op *addBindMountOp) Name() string { return "container/bind-mount" }

func (op *addBindMountOp) PreLaunch(pctx *provision.Context) error {
hostMountDir, ok := pctx.Get(provision.KeyHostMountDir)
if !ok {
return fmt.Errorf("host mount directory not set in context")
}

sourcePath := hostMountDir
if pctx.Config.Subpath != "" {
sourcePath = filepath.Join(sourcePath, pctx.Config.Subpath)
}

op.spec.Mounts = append(op.spec.Mounts, &types.Mount{
Type: "bind",
Source: sourcePath,
Target: pctx.TargetDir,
})
return nil
}

type addImageMountOp struct {
provision.Noop
spec *types.ContainerSpec
}

func AddImageMount(spec *types.ContainerSpec) provision.Operation {
return &addImageMountOp{spec: spec}
}

func (op *addImageMountOp) Name() string { return "container/image-mount" }

func (op *addImageMountOp) PreLaunch(pctx *provision.Context) error {
mount := &types.Mount{
Type: "image",
Source: pctx.Config.Image,
Target: pctx.TargetDir,
}
if pctx.Config.Subpath != "" {
mount.ImageOptions = &types.ImageOptions{
Subpath: pctx.Config.Subpath,
}
}
op.spec.Mounts = append(op.spec.Mounts, mount)
return nil
}
98 changes: 98 additions & 0 deletions core/pkg/sandboxer/container/operation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* SPDX-FileCopyrightText: (c) 2024 The Drassi Authors
*
* SPDX-License-Identifier: Apache-2.0
*/

package container

import (
"path/filepath"
"testing"

"drassi.run/core/config"
"drassi.run/core/pkg/container/types"
"drassi.run/core/pkg/runtime/provision"
"github.com/stretchr/testify/require"
)

func newTestSetup(t *testing.T, name string, cfg *config.Runtime) (*types.ContainerSpec, *provision.Context) {
t.Helper()
if cfg == nil {
cfg = new(config.Runtime)
}
spec := new(types.ContainerSpec)
pctx := provision.NewContext(t.Context(), name, cfg, "/opt/drassi/runtimes/"+name)
return spec, pctx
}

func TestContainerBindMount(t *testing.T) {
t.Run("basic bind mount", func(t *testing.T) {
spec, pctx := newTestSetup(t, "node", nil)
pctx.Set(provision.KeyHostMountDir, "/var/lib/drassi/storage/overlay/merged")

op := AddBindMount(spec)
require.Equal(t, "container/bind-mount", op.Name())

require.NoError(t, op.PreLaunch(pctx))
require.Len(t, spec.Mounts, 1)
require.Equal(t, "bind", spec.Mounts[0].Type)
require.Equal(t, "/var/lib/drassi/storage/overlay/merged", spec.Mounts[0].Source)
require.Equal(t, "/opt/drassi/runtimes/node", spec.Mounts[0].Target)
require.NoError(t, op.PostLaunch(pctx, nil))
})

t.Run("with subpath", func(t *testing.T) {
spec, pctx := newTestSetup(t, "python", &config.Runtime{Subpath: "opt/python"})
pctx.Set(provision.KeyHostMountDir, "/var/lib/drassi/storage/overlay/merged")

op := AddBindMount(spec)
require.NoError(t, op.PreLaunch(pctx))
require.Len(t, spec.Mounts, 1)
require.Equal(t, "bind", spec.Mounts[0].Type)
require.Equal(t, filepath.Join("/var/lib/drassi/storage/overlay/merged", "opt/python"), spec.Mounts[0].Source)
require.Equal(t, "/opt/drassi/runtimes/python", spec.Mounts[0].Target)
})

t.Run("missing host mount dir", func(t *testing.T) {
spec, pctx := newTestSetup(t, "node", nil)

op := AddBindMount(spec)
err := op.PreLaunch(pctx)
require.Error(t, err)
require.Contains(t, err.Error(), "host mount directory not set in context")
})
}

func TestContainerImageMount(t *testing.T) {
t.Run("basic image mount", func(t *testing.T) {
spec, pctx := newTestSetup(t, "node", &config.Runtime{Image: "drassi/node:24"})

op := AddImageMount(spec)
require.Equal(t, "container/image-mount", op.Name())

require.NoError(t, op.PreLaunch(pctx))
require.Len(t, spec.Mounts, 1)
require.Equal(t, "image", spec.Mounts[0].Type)
require.Equal(t, "drassi/node:24", spec.Mounts[0].Source)
require.Equal(t, "/opt/drassi/runtimes/node", spec.Mounts[0].Target)
require.Nil(t, spec.Mounts[0].ImageOptions)
require.NoError(t, op.PostLaunch(pctx, nil))
})

t.Run("with subpath", func(t *testing.T) {
spec, pctx := newTestSetup(t, "python", &config.Runtime{
Image: "drassi/python:3.12",
Subpath: "opt/python",
})

op := AddImageMount(spec)
require.NoError(t, op.PreLaunch(pctx))
require.Len(t, spec.Mounts, 1)
require.Equal(t, "image", spec.Mounts[0].Type)
require.Equal(t, "drassi/python:3.12", spec.Mounts[0].Source)
require.Equal(t, "/opt/drassi/runtimes/python", spec.Mounts[0].Target)
require.NotNil(t, spec.Mounts[0].ImageOptions)
require.Equal(t, "opt/python", spec.Mounts[0].ImageOptions.Subpath)
})
}
40 changes: 40 additions & 0 deletions core/pkg/sandboxer/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: (c) 2024 The Drassi Authors
*
* SPDX-License-Identifier: Apache-2.0
*/

package sandboxer

import (
"fmt"
"strings"
"sync"

"drassi.run/core/config"
"github.com/pelletier/go-toml/v2/unstable"
)

type Factory func(cfg unstable.RawMessage) (Engine, error)

var (
mu sync.RWMutex
factories = make(map[string]Factory)
)

func Register(provider string, factory Factory) {
mu.Lock()
defer mu.Unlock()
factories[strings.ToLower(provider)] = factory
}

func NewEngine(config *config.Sandboxer) (Engine, error) {
provider := strings.ToLower(config.Provider)
mu.RLock()
factory, ok := factories[provider]
mu.RUnlock()
if !ok {
return nil, fmt.Errorf("unsupported sandboxer provider %q", config.Provider)
}
return factory(config.Config)
}
15 changes: 15 additions & 0 deletions core/pkg/sandboxer/host/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,30 @@ import (
"path/filepath"
"strings"

"drassi.run/core/config"
c "drassi.run/core/pkg/container"
"drassi.run/core/pkg/container/docker"
"drassi.run/core/pkg/sandboxer"
"drassi.run/core/pkg/sandboxer/container"
"drassi.run/core/util/fs"
"drassi.run/core/util/path"
"drassi.run/core/util/string"
"github.com/pelletier/go-toml/v2"
"github.com/pelletier/go-toml/v2/unstable"
)

func init() {
sandboxer.Register(config.ProviderHost, func(raw unstable.RawMessage) (sandboxer.Engine, error) {
cfg := DefaultConfig()
if len(raw) > 0 {
if err := toml.Unmarshal(raw, cfg); err != nil {
return nil, err
}
}
return New(cfg)
})
}

type Config struct {
RootDir string `toml:"root_dir" json:"rootDir"`
RuntimeDir string `toml:"runtime_dir,omitempty" json:"runtimeDir,omitempty"`
Expand Down
Loading
Loading