Files
immucore/pkg/mount/mount.go

409 lines
9.6 KiB
Go
Raw Normal View History

2023-01-12 19:10:10 +01:00
package mount
import (
2023-02-01 18:01:58 +01:00
"context"
2023-01-12 19:10:10 +01:00
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
2023-02-01 18:01:58 +01:00
"time"
2023-01-12 19:10:10 +01:00
"github.com/containerd/containerd/mount"
"github.com/deniswernert/go-fstab"
"github.com/kairos-io/immucore/pkg/profile"
2023-02-01 18:01:58 +01:00
"github.com/kairos-io/kairos/pkg/utils"
2023-01-12 19:10:10 +01:00
"github.com/moby/sys/mountinfo"
2023-02-01 18:01:58 +01:00
"github.com/spectrocloud-labs/herd"
2023-01-12 19:10:10 +01:00
)
2023-01-12 19:29:49 +01:00
type MountOperation struct {
FstabEntry fstab.Mount
MountOption mount.Mount
Target string
PrepareCallback func() error
}
func (m MountOperation) Run() error {
if m.PrepareCallback != nil {
if err := m.PrepareCallback(); err != nil {
return err
}
}
return mount.All([]mount.Mount{m.MountOption}, m.Target)
}
2023-01-12 19:10:10 +01:00
// https://github.com/kairos-io/packages/blob/94aa3bef3d1330cb6c6905ae164f5004b6a58b8c/packages/system/dracut/immutable-rootfs/30cos-immutable-rootfs/cos-mount-layout.sh#L129
2023-01-12 19:29:49 +01:00
func BaseOverlay(overlay profile.Overlay) (MountOperation, error) {
2023-01-12 19:10:10 +01:00
if err := os.MkdirAll(overlay.Base, 0700); err != nil {
2023-01-12 19:29:49 +01:00
return MountOperation{}, err
2023-01-12 19:10:10 +01:00
}
dat := strings.Split(overlay.BackingBase, ":")
if len(dat) != 2 {
2023-01-12 19:29:49 +01:00
return MountOperation{}, fmt.Errorf("invalid backing base. must be a tmpfs with a size or a block device. e.g. tmpfs:30%%, block:/dev/sda1. Input: %s", overlay.BackingBase)
2023-01-12 19:10:10 +01:00
}
t := dat[0]
switch t {
case "tmpfs":
tmpMount := mount.Mount{Type: "tmpfs", Source: "tmpfs", Options: []string{"defaults", fmt.Sprintf("size=%s", dat[1])}}
err := mount.All([]mount.Mount{tmpMount}, overlay.Base)
fstab := mountToStab(tmpMount)
fstab.File = overlay.BackingBase
2023-01-12 19:29:49 +01:00
return MountOperation{
MountOption: tmpMount,
FstabEntry: *fstab,
Target: overlay.Base,
}, err
2023-01-12 19:10:10 +01:00
case "block":
blockMount := mount.Mount{Type: "auto", Source: dat[1]}
err := mount.All([]mount.Mount{blockMount}, overlay.Base)
fstab := mountToStab(blockMount)
fstab.File = overlay.BackingBase
fstab.MntOps["default"] = ""
2023-01-12 19:29:49 +01:00
return MountOperation{
MountOption: blockMount,
FstabEntry: *fstab,
Target: overlay.Base,
}, err
2023-01-12 19:10:10 +01:00
default:
2023-01-12 19:29:49 +01:00
return MountOperation{}, fmt.Errorf("invalid overlay backing base type")
2023-01-12 19:10:10 +01:00
}
}
func mountToStab(m mount.Mount) *fstab.Mount {
opts := map[string]string{}
for _, o := range m.Options {
if strings.Contains(o, "=") {
dat := strings.Split(o, "=")
key := dat[0]
value := dat[1]
opts[key] = value
} else {
opts[o] = ""
}
}
return &fstab.Mount{
Spec: m.Source,
VfsType: m.Type,
MntOps: opts,
Freq: 0,
PassNo: 0,
}
}
func MountEphemeral(path []string) {
}
func MountPeristentPaths() {
}
func createIfNotExists(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return os.MkdirAll(path, os.ModePerm)
}
return nil
}
func appendSlash(path string) string {
if !strings.HasSuffix(path, "/") {
return fmt.Sprintf("%s/", path)
}
return path
}
// https://github.com/kairos-io/packages/blob/94aa3bef3d1330cb6c6905ae164f5004b6a58b8c/packages/system/dracut/immutable-rootfs/30cos-immutable-rootfs/cos-mount-layout.sh#L183
2023-01-12 19:29:49 +01:00
func mountBind(mountpoint, root, stateTarget string) (MountOperation, error) {
2023-01-12 19:10:10 +01:00
mountpoint = strings.TrimLeft(mountpoint, "/") // normalize, remove / upfront as we are going to re-use it in subdirs
rootMount := filepath.Join(root, mountpoint)
bindMountPath := strings.ReplaceAll(mountpoint, "/", "-")
stateDir := filepath.Join(root, stateTarget, fmt.Sprintf("%s.bind", bindMountPath))
if mounted, _ := mountinfo.Mounted(rootMount); !mounted {
tmpMount := mount.Mount{
Type: "overlay",
Source: stateDir,
Options: []string{
"defaults",
"bind",
},
}
fstab := mountToStab(tmpMount)
fstab.File = fmt.Sprintf("/%s", mountpoint)
fstab.Spec = strings.ReplaceAll(fstab.Spec, root, "")
2023-01-12 19:29:49 +01:00
return MountOperation{
MountOption: tmpMount,
FstabEntry: *fstab,
Target: rootMount,
PrepareCallback: func() error {
if err := createIfNotExists(rootMount); err != nil {
return err
}
if err := createIfNotExists(stateDir); err != nil {
return err
}
return syncState(appendSlash(rootMount), appendSlash(stateDir))
},
}, nil
2023-01-12 19:10:10 +01:00
}
2023-01-12 19:29:49 +01:00
return MountOperation{}, fmt.Errorf("already mounted")
2023-01-12 19:10:10 +01:00
}
func syncState(src, dst string) error {
return exec.Command("rsync", "-aqAX", src, dst).Run()
}
// https://github.com/kairos-io/packages/blob/94aa3bef3d1330cb6c6905ae164f5004b6a58b8c/packages/system/dracut/immutable-rootfs/30cos-immutable-rootfs/cos-mount-layout.sh#L145
2023-01-12 19:29:49 +01:00
func mountWithBaseOverlay(mountpoint, root, base string) (MountOperation, error) {
2023-01-12 19:10:10 +01:00
mountpoint = strings.TrimLeft(mountpoint, "/") // normalize, remove / upfront as we are going to re-use it in subdirs
rootMount := filepath.Join(root, mountpoint)
bindMountPath := strings.ReplaceAll(mountpoint, "/", "-")
createIfNotExists(rootMount)
if mounted, _ := mountinfo.Mounted(rootMount); !mounted {
upperdir := filepath.Join(base, bindMountPath, ".overlay", "upper")
workdir := filepath.Join(base, bindMountPath, ".overlay", "work")
tmpMount := mount.Mount{
Type: "overlay",
Source: "overlay",
Options: []string{
"defaults",
fmt.Sprintf("lowerdir=%s", rootMount),
fmt.Sprintf("upperdir=%s", upperdir),
fmt.Sprintf("workdir=%s", workdir),
},
}
fstab := mountToStab(tmpMount)
fstab.File = rootMount
// TODO: update fstab with x-systemd info
// https://github.com/kairos-io/packages/blob/94aa3bef3d1330cb6c6905ae164f5004b6a58b8c/packages/system/dracut/immutable-rootfs/30cos-immutable-rootfs/cos-mount-layout.sh#L170
2023-01-12 19:29:49 +01:00
return MountOperation{
MountOption: tmpMount,
FstabEntry: *fstab,
Target: rootMount,
PrepareCallback: func() error {
// Make sure workdir and/or upper exists
os.MkdirAll(upperdir, os.ModePerm)
os.MkdirAll(workdir, os.ModePerm)
return nil
},
}, nil
2023-01-12 19:10:10 +01:00
}
2023-01-12 19:29:49 +01:00
return MountOperation{}, fmt.Errorf("already mounted")
2023-01-12 19:10:10 +01:00
}
2023-02-01 18:01:58 +01:00
type State struct {
2023-02-01 19:06:51 +01:00
Rootdir string
TargetImage string // e.g. /cOS/active.img
OverlayDir []string // e.g. /var
BindMounts []string // e.g. /etc/kubernetes
CustomMounts map[string]string // e.g. diskid : mountpoint
fstabs []*fstab.Mount
2023-02-01 18:01:58 +01:00
}
func (s *State) Register(g *herd.Graph) error {
g.Add("discover-mount",
herd.WithDeps("mount-cos-state"),
herd.WithCallback(
func(ctx context.Context) error {
2023-02-01 19:06:51 +01:00
_, err := utils.SH(fmt.Sprintf("losetup --show -f /run/initramfs/cos-state%s", s.TargetImage))
return err
2023-02-01 18:01:58 +01:00
},
))
g.Add("mount-cos-state",
herd.WithCallback(
s.MountOP(
"/dev/disk/by-label/COS_STATE",
s.path("/run/initramfs/cos-state"),
"auto",
[]string{
"ro", // or rw
}, 60*time.Second),
),
)
2023-02-01 19:06:51 +01:00
g.Add("mount-overlay-base",
herd.WithCallback(
func(ctx context.Context) error {
op, err := BaseOverlay(profile.Overlay{
Base: "/run/overlay",
BackingBase: "tmpfs:20%",
})
if err != nil {
return err
}
s.fstabs = append(s.fstabs, &op.FstabEntry)
return op.Run()
},
),
)
// TODO: Add fsck
// mount overlay
for _, p := range s.OverlayDir {
g.Add("mount-overlays-base",
herd.WithCallback(
func(ctx context.Context) error {
op, err := mountWithBaseOverlay(p, s.Rootdir, "/run/overlay")
if err != nil {
return err
}
s.fstabs = append(s.fstabs, &op.FstabEntry)
return op.Run()
},
),
)
}
// custom mounts TODO: disk/path
for id, mountpoint := range s.CustomMounts {
g.Add("mount-custom",
herd.WithCallback(
s.MountOP(
id,
s.path(mountpoint),
"auto",
[]string{
"ro", // or rw
}, 60*time.Second),
),
)
}
// mount state
for _, p := range s.BindMounts {
g.Add("mount-state",
herd.WithCallback(
func(ctx context.Context) error {
op, err := mountBind(p, s.Rootdir, "/usr/local/.state")
if err != nil {
return err
}
s.fstabs = append(s.fstabs, &op.FstabEntry)
return op.Run()
},
),
)
}
2023-02-01 18:01:58 +01:00
g.Add("mount-sysroot",
herd.WithCallback(
s.MountOP(
"/dev/disk/by-label/COS_ACTIVE",
s.path("/sysroot"),
"auto",
[]string{
"ro", // or rw
"suid",
"dev",
"exec",
"auto",
"nouser",
"async",
}, 60*time.Second),
),
)
g.Add("mount-oem",
herd.WithCallback(
s.MountOP(
"/dev/disk/by-label/COS_OEM",
"/oem",
"auto",
[]string{
"rw",
"suid",
"dev",
"exec",
"noauto",
"nouser",
"async",
}, 60*time.Second),
),
)
g.Add("write-fstab", herd.WithCallback(s.WriteFstab("foo")))
return nil
}
func (s *State) path(p ...string) string {
return filepath.Join(append([]string{s.Rootdir}, p...)...)
}
func (s *State) WriteFstab(fstabFile string) func(context.Context) error {
return func(ctx context.Context) error {
for _, fst := range s.fstabs {
select {
case <-ctx.Done():
default:
f, err := os.OpenFile(fstabFile,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
if _, err := f.WriteString(fmt.Sprintf("%s\n", fst.String())); err != nil {
return err
}
}
}
return nil
}
}
func (s *State) MountOP(what, where, t string, options []string, timeout time.Duration) func(context.Context) error {
return func(c context.Context) error {
for {
select {
default:
time.Sleep(1 * time.Second)
mountPoint := mount.Mount{
Type: t,
Source: what,
Options: options,
}
fstab := mountToStab(mountPoint)
fstab.File = where
op := MountOperation{
MountOption: mountPoint,
FstabEntry: *fstab,
Target: where,
}
err := op.Run()
if err != nil {
continue
}
s.fstabs = append(s.fstabs, fstab)
return nil
case <-c.Done():
return fmt.Errorf("context canceled")
case <-time.After(timeout):
return fmt.Errorf("timeout exhausted")
}
}
}
}