aboutsummaryrefslogtreecommitdiffhomepage
path: root/hst/fs.go
blob: 3f0f601736ffe2be5fc51bd73870535be9956f7d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package hst

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"reflect"

	"hakurei.app/container/check"
)

// FilesystemConfig is an abstract representation of a mount point.
type FilesystemConfig interface {
	// Valid returns whether the configuration is valid.
	Valid() bool
	// Path returns the target path in the container.
	Path() *check.Absolute
	// Host returns a slice of all host paths used by this operation.
	Host() []*check.Absolute
	// Apply appends the [container.Op] implementing this operation.
	Apply(z *ApplyState)

	fmt.Stringer
}

// The Ops interface enables [FilesystemConfig] to queue container ops without
// depending on the container package.
type Ops interface {
	// Tmpfs appends an op that mounts tmpfs on a container path.
	Tmpfs(target *check.Absolute, size int, perm os.FileMode) Ops
	// Readonly appends an op that mounts read-only tmpfs on a container path.
	Readonly(target *check.Absolute, perm os.FileMode) Ops

	// Bind appends an op that bind mounts a host path on a container path.
	Bind(source, target *check.Absolute, flags int) Ops
	// Overlay appends an op that mounts the overlay pseudo filesystem.
	Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) Ops
	// OverlayReadonly appends an op that mounts the overlay pseudo filesystem readonly.
	OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) Ops

	// Link appends an op that creates a symlink in the container filesystem.
	Link(target *check.Absolute, linkName string, dereference bool) Ops

	// Root appends an op that expands a directory into a toplevel bind mount
	// mirror on container root.
	Root(host *check.Absolute, flags int) Ops
	// Etc appends an op that expands host /etc into a toplevel symlink mirror
	// with /etc semantics.
	Etc(host *check.Absolute, prefix string) Ops

	// Daemon appends an op that starts a daemon in the container and blocks
	// until target appears.
	Daemon(target, path *check.Absolute, args ...string) Ops
}

// ApplyState holds the address of [Ops] and any relevant application state.
type ApplyState struct {
	// AutoEtcPrefix is the prefix for [FSBind] in autoetc [FSBind.Special] condition.
	AutoEtcPrefix string

	Ops
}

// ErrFSNull is returned by [json] on encountering a null [FilesystemConfig] value.
var ErrFSNull = errors.New("unexpected null in mount point")

// FSTypeError is returned when [ContainerConfig.Filesystem] contains an entry
// with invalid type.
type FSTypeError string

func (f FSTypeError) Error() string { return fmt.Sprintf("invalid filesystem type %q", string(f)) }

// FSImplError is returned for unsupported implementations of [FilesystemConfig].
type FSImplError struct{ Value FilesystemConfig }

func (f FSImplError) Error() string {
	implType := reflect.TypeOf(f.Value)
	var name string
	for implType != nil && implType.Kind() == reflect.Ptr {
		name += "*"
		implType = implType.Elem()
	}
	if implType != nil {
		name += implType.Name()
	} else {
		name += "nil"
	}
	return fmt.Sprintf("implementation %s not supported", name)
}

// FilesystemConfigJSON is the [json] adapter for [FilesystemConfig].
type FilesystemConfigJSON struct{ FilesystemConfig }

// Valid returns whether the [FilesystemConfigJSON] is valid.
func (f *FilesystemConfigJSON) Valid() bool {
	return f != nil && f.FilesystemConfig != nil && f.FilesystemConfig.Valid()
}

// fsType holds the string representation of the concrete type of [FilesystemConfig].
type fsType struct {
	Type string `json:"type"`
}

func (f *FilesystemConfigJSON) MarshalJSON() ([]byte, error) {
	if f == nil || f.FilesystemConfig == nil {
		return nil, ErrFSNull
	}
	var v any
	switch cv := f.FilesystemConfig.(type) {
	case *FSBind:
		v = &struct {
			fsType
			*FSBind
		}{fsType{FilesystemBind}, cv}

	case *FSEphemeral:
		v = &struct {
			fsType
			*FSEphemeral
		}{fsType{FilesystemEphemeral}, cv}

	case *FSOverlay:
		v = &struct {
			fsType
			*FSOverlay
		}{fsType{FilesystemOverlay}, cv}

	case *FSLink:
		v = &struct {
			fsType
			*FSLink
		}{fsType{FilesystemLink}, cv}

	case *FSDaemon:
		v = &struct {
			fsType
			*FSDaemon
		}{fsType{FilesystemDaemon}, cv}

	default:
		return nil, FSImplError{f.FilesystemConfig}
	}

	return json.Marshal(v)
}

func (f *FilesystemConfigJSON) UnmarshalJSON(data []byte) error {
	t := new(fsType)
	if err := json.Unmarshal(data, &t); err != nil {
		return err
	}
	if t == nil {
		return ErrFSNull
	}
	switch t.Type {
	case FilesystemBind:
		*f = FilesystemConfigJSON{new(FSBind)}

	case FilesystemEphemeral:
		*f = FilesystemConfigJSON{new(FSEphemeral)}

	case FilesystemOverlay:
		*f = FilesystemConfigJSON{new(FSOverlay)}

	case FilesystemLink:
		*f = FilesystemConfigJSON{new(FSLink)}

	case FilesystemDaemon:
		*f = FilesystemConfigJSON{new(FSDaemon)}

	default:
		return FSTypeError(t.Type)
	}

	return json.Unmarshal(data, f.FilesystemConfig)
}