immucore/pkg/mount/state.go
2024-03-18 14:38:57 +01:00

290 lines
8.8 KiB
Go

package mount
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/containerd/containerd/mount"
"github.com/deniswernert/go-fstab"
"github.com/kairos-io/immucore/internal/constants"
internalUtils "github.com/kairos-io/immucore/internal/utils"
"github.com/rs/zerolog"
"github.com/spectrocloud-labs/herd"
)
type State struct {
Rootdir string // where to mount the root partition e.g. /sysroot inside initrd with pivot, / with nopivot
TargetImage string // image from the state partition to mount as loop device e.g. /cOS/active.img
TargetDevice string // e.g. /dev/disk/by-label/COS_ACTIVE
RootMountMode string // How to mount the root partition e.g. ro or rw
// /run/cos-layout.env (different!)
OverlayDirs []string // e.g. /var
BindMounts []string // e.g. /etc/kubernetes
CustomMounts map[string]string // e.g. diskid : mountpoint
OverlayBase string // Overlay config, defaults to tmpfs:20%
StateDir string // e.g. "/usr/local/.state"
fstabs []*fstab.Mount
}
// SortedBindMounts returns the nodes with less depth first and in alphabetical order.
func (s *State) SortedBindMounts() []string {
bindMountsCopy := s.BindMounts
sort.Slice(bindMountsCopy, func(i, j int) bool {
iAry := strings.Split(bindMountsCopy[i], "/")
jAry := strings.Split(bindMountsCopy[j], "/")
iSize := len(iAry)
jSize := len(jAry)
if iSize == jSize {
return strings.Compare(iAry[len(iAry)-1], jAry[len(jAry)-1]) == -1
}
return iSize < jSize
})
return bindMountsCopy
}
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 {
// Create the file first, override if something is there, we don't care, we are on initramfs
f, err := os.Create(fstabFile)
if err != nil {
return err
}
f.Close()
for _, fst := range s.fstabs {
internalUtils.Log.Debug().Str("what", fst.String()).Msg("Adding line to fstab")
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
}
if _, err := f.WriteString(fmt.Sprintf("%s\n", fst.String())); err != nil {
_ = f.Close()
return err
}
_ = f.Close()
}
}
return nil
}
}
// RunStageOp runs elemental run-stage stage. If its rootfs its special as it needs som symlinks
// If its uki we don't symlink as we already have everything in the sysroot.
func (s *State) RunStageOp(stage string) func(context.Context) error {
return func(_ context.Context) error {
switch stage {
case "rootfs":
if !internalUtils.IsUKI() {
if _, err := os.Stat("/system"); os.IsNotExist(err) {
err = os.Symlink("/sysroot/system", "/system")
if err != nil {
internalUtils.Log.Err(err).Msg("creating symlink")
}
}
if _, err := os.Stat("/oem"); os.IsNotExist(err) {
err = os.Symlink("/sysroot/oem", "/oem")
if err != nil {
internalUtils.Log.Err(err).Msg("creating symlink")
}
}
}
internalUtils.Log.Info().Msg("Running rootfs stage")
output, _ := internalUtils.RunStage("rootfs")
internalUtils.Log.Debug().Msg(output.String())
err := internalUtils.CreateIfNotExists(constants.LogDir)
if err != nil {
return err
}
e := os.WriteFile(filepath.Join(constants.LogDir, "rootfs_stage.log"), output.Bytes(), os.ModePerm)
if e != nil {
internalUtils.Log.Err(e).Msg("Writing log for rootfs stage")
}
return err
case "initramfs":
// Not sure if it will work under UKI where the s.Rootdir is the current root already
internalUtils.Log.Info().Msg("Running initramfs stage")
if internalUtils.IsUKI() {
output, _ := internalUtils.RunStage("initramfs")
internalUtils.Log.Debug().Msg(output.String())
err := internalUtils.CreateIfNotExists(constants.LogDir)
if err != nil {
return err
}
e := os.WriteFile(filepath.Join(constants.LogDir, "initramfs_stage.log"), output.Bytes(), os.ModePerm)
if e != nil {
internalUtils.Log.Err(e).Msg("Writing log for initramfs stage")
}
return err
} else {
chroot := internalUtils.NewChroot(s.Rootdir)
return chroot.RunCallback(func() error {
output, _ := internalUtils.RunStage("initramfs")
internalUtils.Log.Debug().Msg(output.String())
err := internalUtils.CreateIfNotExists(constants.LogDir)
if err != nil {
return err
}
e := os.WriteFile(filepath.Join(constants.LogDir, "initramfs_stage.log"), output.Bytes(), os.ModePerm)
if e != nil {
internalUtils.Log.Err(e).Msg("Writing log for initramfs stage")
}
return err
})
}
default:
return errors.New("no stage that we know off")
}
}
}
// MountOP creates and executes a mount operation.
func (s *State) MountOP(what, where, t string, options []string, timeout time.Duration) func(context.Context) error {
l := internalUtils.Log.With().Str("what", what).Str("where", where).Str("type", t).Strs("options", options).Logger()
// Not sure why this defaults to debuglevel when creating a sublogger, so make sure we set it properly
debug := len(internalUtils.ReadCMDLineArg("rd.immucore.debug")) > 0
if debug {
l = l.Level(zerolog.DebugLevel)
}
return func(c context.Context) error {
cc := time.After(timeout)
for {
select {
default:
// check fs type just-in-time before running the OP
if t != "tmpfs" {
fsType := internalUtils.DiskFSType(what)
// If not empty and it does not match
if fsType != "" && t != fsType {
t = fsType
}
}
err := internalUtils.CreateIfNotExists(where)
if err != nil {
l.Err(err).Msg("Creating dir")
continue
}
time.Sleep(1 * time.Second)
mountPoint := mount.Mount{
Type: t,
Source: what,
Options: options,
}
tmpFstab := internalUtils.MountToFstab(mountPoint)
tmpFstab.File = internalUtils.CleanSysrootForFstab(where)
op := mountOperation{
MountOption: mountPoint,
FstabEntry: *tmpFstab,
Target: where,
PrepareCallback: func() error {
_ = internalUtils.Fsck(what)
return nil
},
}
err = op.run()
// If no error on mounting or error is already mounted, as that affects the sysroot
// for some reason it reports that its already mounted (systemd is mounting it behind our back!).
if err == nil || err != nil && errors.Is(err, constants.ErrAlreadyMounted) {
s.AddToFstab(tmpFstab)
} else {
l.Debug().Err(err).Msg("Mount not added to fstab")
}
// only continue the loop if it's an error and not an already mounted error
if err != nil && !errors.Is(err, constants.ErrAlreadyMounted) {
l.Warn().Err(err).Send()
continue
}
l.Info().Msg("mount done")
return nil
case <-c.Done():
e := fmt.Errorf("context canceled")
l.Err(e).Msg("mount canceled")
return e
case <-cc:
e := fmt.Errorf("timeout exhausted")
l.Err(e).Msg("Mount timeout")
return e
}
}
}
}
// WriteDAG writes the dag.
func (s *State) WriteDAG(g *herd.Graph) (out string) {
for i, layer := range g.Analyze() {
out += fmt.Sprintf("%d.\n", i+1)
for _, op := range layer {
if op.Error != nil {
out += fmt.Sprintf(" <%s> (error: %s) (background: %t) (weak: %t) (run: %t)\n", op.Name, op.Error.Error(), op.Background, op.WeakDeps, op.Executed)
} else {
out += fmt.Sprintf(" <%s> (background: %t) (weak: %t) (run: %t)\n", op.Name, op.Background, op.WeakDeps, op.Executed)
}
}
}
return
}
// LogIfError will log if there is an error with the given context as message
// Context can be empty.
func (s *State) LogIfError(e error, msgContext string) {
if e != nil {
internalUtils.Log.Err(e).Msg(msgContext)
}
}
// LogIfErrorAndReturn will log if there is an error with the given context as message
// Context can be empty
// Will also return the error.
func (s *State) LogIfErrorAndReturn(e error, msgContext string) error {
if e != nil {
internalUtils.Log.Err(e).Msg(msgContext)
}
return e
}
// LogIfErrorAndPanic will log if there is an error with the given context as message
// Context can be empty
// Will also panic.
func (s *State) LogIfErrorAndPanic(e error, msgContext string) {
if e != nil {
internalUtils.Log.Err(e).Msg(msgContext)
internalUtils.Log.Fatal().Msg(e.Error())
}
}
// AddToFstab will try to add an entry to the fstab list
// Will check if the entry exists before adding it to avoid duplicates.
func (s *State) AddToFstab(tmpFstab *fstab.Mount) {
found := false
for _, f := range s.fstabs {
if f.Spec == tmpFstab.Spec {
internalUtils.Log.Debug().Interface("existing", f).Interface("duplicated", tmpFstab).Msg("Duplicated fstab entry found, not adding")
found = true
}
}
if !found {
s.fstabs = append(s.fstabs, tmpFstab)
}
}