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" )
}
}
}
}