diff --git a/.github/workflows/build-iso.yaml b/.github/workflows/build-iso.yaml deleted file mode 100644 index e64dc51..0000000 --- a/.github/workflows/build-iso.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: Iso build - -on: - push: - branches: - - master - -concurrency: - group: iso-build-${{ github.head_ref || github.ref }}-${{ github.repository }} - cancel-in-progress: true - -jobs: - iso-build: - runs-on: ubuntu-latest - strategy: - matrix: - include: - - flavor: "core-opensuse-leap" - steps: - - name: Release space from worker - run: | - sudo rm -rf /usr/local/lib/android # will release about 10 GB if you don't need Android - sudo rm -rf /usr/share/dotnet # will release about 20GB if you don't need .NET - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Install Go - uses: actions/setup-go@v3 - with: - go-version: '^1.18' - - name: Build iso - run: | - ./earthly.sh +iso --FLAVOR=${{ matrix.flavor }} - - uses: actions/upload-artifact@v3 - with: - name: ${{ matrix.flavor }}-immucore.iso.zip - path: | - build/*.iso - build/*.sha256 \ No newline at end of file diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 570c841..0061227 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -6,6 +6,16 @@ on: pull_request: jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Run Lint checks + run: | + ./earthly.sh +lint unit-tests: runs-on: ubuntu-latest steps: diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..885fb76 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,20 @@ +run: + timeout: 5m + tests: false +linters: + enable: + - revive # replacement for golint + - dupl # check duplicated code + - goconst # check strings that can turn into constants + - gofmt # check fmt + - goheader # Check license headers, only checks files in current year + - goimports # check imports + - gocyclo # check complexity + - govet + - gosimple + - ineffassign + - unused + - staticcheck + - typecheck + - godot + - misspell \ No newline at end of file diff --git a/Earthfile b/Earthfile index 696f424..5e5047d 100644 --- a/Earthfile +++ b/Earthfile @@ -79,63 +79,4 @@ dracut-artifacts: SAVE ARTIFACT 28immucore 28immucore SAVE ARTIFACT 02-kairos-setup-initramfs.conf 02-kairos-setup-initramfs.conf SAVE ARTIFACT 10-immucore.conf 10-immucore.conf - SAVE ARTIFACT 50-kairos-initrd.conf 50-kairos-initrd.conf - -build-dracut: - FROM $BASE_IMAGE - COPY +version/VERSION ./ - ARG VERSION=$(cat VERSION) - ARG REMOVE_COS_MODULE - COPY +build-immucore/immucore /usr/bin/immucore - COPY --dir dracut/28immucore /usr/lib/dracut/modules.d/ - COPY dracut/*.conf /etc/dracut.conf.d/ - RUN ls -ltra /etc/dracut.conf.d/ - # (START) Remove cos-immutable-rootfs module - RUN rm -Rf /usr/lib/dracut/modules.d/30cos-immutable-rootfs/ - RUN rm /etc/dracut.conf.d/02-cos-immutable-rootfs.conf - RUN rm /etc/dracut.conf.d/02-cos-setup-initramfs.conf - RUN rm /etc/dracut.conf.d/50-cos-initrd.conf - # (END) Remove cos-immutable-rootfs module - RUN kernel=$(ls /lib/modules | head -n1) && \ - dracut -f "/boot/initrd-${kernel}" "${kernel}" && \ - ln -sf "initrd-${kernel}" /boot/initrd - ARG INITRD=$(readlink -f /boot/initrd) - SAVE ARTIFACT $INITRD Initrd AS LOCAL build/initrd-$VERSION - -elemental: - FROM $OSBUILDER_IMAGE - SAVE ARTIFACT --keep-own /usr/bin/elemental elemental - -image: - FROM $BASE_IMAGE - COPY +version/VERSION ./ - ARG VERSION=$(cat VERSION) - ARG INITRD=$(readlink -f /boot/initrd) - COPY +build-dracut/Initrd $INITRD - # For initrd use - COPY +build-immucore/immucore /usr/bin/immucore - COPY +elemental/elemental /usr/bin/elemental - RUN ln -s /usr/lib/systemd/systemd /init - SAVE IMAGE $FLAVOR-immucore:$VERSION - -image-rootfs: - FROM +image - SAVE ARTIFACT --keep-own /. rootfs - -grub-files: - FROM alpine - RUN apk add wget - RUN wget https://raw.githubusercontent.com/c3os-io/c3os/master/overlay/files-iso/boot/grub2/grub.cfg -O grub.cfg - SAVE ARTIFACT --keep-own grub.cfg grub.cfg - -iso: - FROM $OSBUILDER_IMAGE - ARG ISO_NAME - COPY +version/VERSION ./ - ARG VERSION=$(cat VERSION) - WORKDIR /build - COPY --keep-own +grub-files/grub.cfg /build/files-iso/boot/grub2/grub.cfg - COPY --keep-own +image-rootfs/rootfs /build/rootfs - RUN /entrypoint.sh --name $ISO_NAME --debug build-iso --squash-no-compression --date=false --local --overlay-iso /build/files-iso --output /build/ dir:/build/rootfs - SAVE ARTIFACT /build/$ISO_NAME.iso iso AS LOCAL build/$ISO_NAME-$VERSION.iso - SAVE ARTIFACT /build/$ISO_NAME.iso.sha256 sha256 AS LOCAL build/$ISO_NAME-$VERSION.iso.sha256 \ No newline at end of file + SAVE ARTIFACT 50-kairos-initrd.conf 50-kairos-initrd.conf \ No newline at end of file diff --git a/go.mod b/go.mod index 854fe73..c3e6bd1 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/onsi/gomega v1.27.2 github.com/rs/zerolog v1.29.0 github.com/spectrocloud-labs/herd v0.4.2 + github.com/twpayne/go-vfs v1.7.2 github.com/urfave/cli/v2 v2.24.4 golang.org/x/sys v0.5.0 ) diff --git a/go.sum b/go.sum index ba45cb1..b38c182 100644 --- a/go.sum +++ b/go.sum @@ -723,6 +723,8 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/twpayne/go-vfs v1.7.2 h1:ZNYMAXcu2Av8c109USrSGYm8dIIIV0xPlG19I2088Kw= +github.com/twpayne/go-vfs v1.7.2/go.mod h1:1eni2ntkiiAHZG27xfLOO4CYvMR4Kw8V7rYiLeeolsQ= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -936,6 +938,7 @@ golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 4d816d9..28ed6c3 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -3,7 +3,7 @@ package constants import "errors" func DefaultRWPaths() []string { - // Default RW_PATHS to mount if not overriden by the cos-layout.env file + // Default RW_PATHS to mount if not override by the cos-layout.env file return []string{"/etc", "/root", "/home", "/opt", "/srv", "/usr/local", "/var"} } diff --git a/internal/utils/common.go b/internal/utils/common.go index c9a49d8..e133c1c 100644 --- a/internal/utils/common.go +++ b/internal/utils/common.go @@ -1,16 +1,18 @@ package utils import ( + "errors" "fmt" - "github.com/joho/godotenv" - "github.com/kairos-io/kairos/sdk/state" "os" "os/exec" "path/filepath" "strings" + + "github.com/joho/godotenv" + "github.com/kairos-io/kairos/sdk/state" ) -// BootStateToLabelDevice lets us know the device we need to mount sysroot on based on labels +// BootStateToLabelDevice lets us know the device we need to mount sysroot on based on labels. func BootStateToLabelDevice() string { runtime, err := state.NewRuntime() if err != nil { @@ -29,9 +31,9 @@ func BootStateToLabelDevice() string { } // GetRootDir returns the proper dir to mount all the stuff -// Useful if we want to move to a no-pivot boot +// Useful if we want to move to a no-pivot boot. func GetRootDir() string { - cmdline, _ := os.ReadFile("/proc/cmdline") + cmdline, _ := os.ReadFile(GetHostProcCmdline()) switch { case strings.Contains(string(cmdline), "rd.immucore.uki"): return "/" @@ -54,7 +56,7 @@ func UniqueSlice(slice []string) []string { return list } -// ReadEnv will reaed an env file (key=value) and return a nice map +// ReadEnv will read an env file (key=value) and return a nice map. func ReadEnv(file string) (map[string]string, error) { var envMap map[string]string var err error @@ -75,7 +77,7 @@ func ReadEnv(file string) (map[string]string, error) { return envMap, err } -// CreateIfNotExists will check if a path exists and create it if needed +// CreateIfNotExists will check if a path exists and create it if needed. func CreateIfNotExists(path string) error { if _, err := os.Stat(path); os.IsNotExist(err) { return os.MkdirAll(path, os.ModePerm) @@ -86,7 +88,7 @@ func CreateIfNotExists(path string) error { // CleanupSlice will clean a slice of strings of empty items // Typos can be made on writing the cos-layout.env file and that could introduce empty items -// In the lists that we need to go over, which causes bad stuff +// In the lists that we need to go over, which causes bad stuff. func CleanupSlice(slice []string) []string { var cleanSlice []string for _, item := range slice { @@ -98,37 +100,37 @@ func CleanupSlice(slice []string) []string { return cleanSlice } -// GetTarget gets the target image and device to mount in /sysroot -func GetTarget(dryRun bool) (string, string) { - var label string - - label = BootStateToLabelDevice() +// GetTarget gets the target image and device to mount in /sysroot. +func GetTarget(dryRun bool) (string, string, error) { + label := BootStateToLabelDevice() // If dry run, or we are disabled return whatever values, we won't go much further if dryRun || DisableImmucore() { - return "fake", label + return "fake", label, nil } - imgs := ReadCMDLineArg("cos-img/filename=") + imgs := CleanupSlice(ReadCMDLineArg("cos-img/filename=")) // If no image just panic here, we cannot longer continue if len(imgs) == 0 { if IsUKI() { imgs = []string{""} } else { - Log.Fatal().Msg("could not get the image name from cmdline (i.e. cos-img/filename=/cOS/active.img)") + msg := "could not get the image name from cmdline (i.e. cos-img/filename=/cOS/active.img)" + Log.Error().Msg(msg) + return "", "", errors.New(msg) } } Log.Debug().Str("what", imgs[0]).Msg("Target device") Log.Debug().Str("what", label).Msg("Target label") - return imgs[0], label + return imgs[0], label, nil } // DisableImmucore identifies if we need to be disabled -// We disable if we boot from CD, netboot, squashfs recovery or have the rd.cos.disable stanza in cmdline +// We disable if we boot from CD, netboot, squashfs recovery or have the rd.cos.disable stanza in cmdline. func DisableImmucore() bool { - cmdline, _ := os.ReadFile("/proc/cmdline") + cmdline, _ := os.ReadFile(GetHostProcCmdline()) cmdlineS := string(cmdline) return strings.Contains(cmdlineS, "live:LABEL") || strings.Contains(cmdlineS, "live:CDLABEL") || @@ -136,7 +138,7 @@ func DisableImmucore() bool { strings.Contains(cmdlineS, "rd.immucore.disable") } -// RootRW tells us if the mode to mount root +// RootRW tells us if the mode to mount root. func RootRW() string { if len(ReadCMDLineArg("rd.cos.debugrw")) > 0 || len(ReadCMDLineArg("rd.immucore.debugrw")) > 0 { Log.Warn().Msg("Mounting root as RW") @@ -146,7 +148,7 @@ func RootRW() string { } // GetState returns the disk-by-label of the state partition to mount -// This is only valid for either active/passive or normal recovery +// This is only valid for either active/passive or normal recovery. func GetState() string { var label string runtime, err := state.NewRuntime() @@ -164,14 +166,11 @@ func GetState() string { } func IsUKI() bool { - if len(ReadCMDLineArg("rd.immucore.uki")) > 0 { - return true - } - return false + return len(ReadCMDLineArg("rd.immucore.uki")) > 0 } // CommandWithPath runs a command adding the usual PATH to environment -// Useful under UKI as there is nothing setting the PATH +// Useful under UKI as there is nothing setting the PATH. func CommandWithPath(c string) (string, error) { cmd := exec.Command("/bin/sh", "-c", c) cmd.Env = os.Environ() @@ -188,3 +187,13 @@ func CommandWithPath(c string) (string, error) { o, err := cmd.CombinedOutput() return string(o), err } + +// GetHostProcCmdline returns the path to /proc/cmdline +// Mainly used to override the cmdline during testing. +func GetHostProcCmdline() string { + proc := os.Getenv("HOST_PROC_CMDLINE") + if proc == "" { + return "/proc/cmdline" + } + return proc +} diff --git a/internal/utils/log.go b/internal/utils/log.go index 5ba095a..49665f2 100644 --- a/internal/utils/log.go +++ b/internal/utils/log.go @@ -1,9 +1,10 @@ package utils import ( - "github.com/rs/zerolog" "io" "os" + + "github.com/rs/zerolog" ) var Log zerolog.Logger diff --git a/internal/utils/mounts.go b/internal/utils/mounts.go index b6cde84..a2b22fd 100644 --- a/internal/utils/mounts.go +++ b/internal/utils/mounts.go @@ -2,20 +2,21 @@ package utils import ( "fmt" - "github.com/containerd/containerd/mount" - "github.com/deniswernert/go-fstab" - "github.com/kairos-io/kairos/sdk/state" "os" "strconv" "strings" "syscall" + + "github.com/containerd/containerd/mount" + "github.com/deniswernert/go-fstab" + "github.com/kairos-io/kairos/sdk/state" ) // https://github.com/kairos-io/packages/blob/7c3581a8ba6371e5ce10c3a98bae54fde6a505af/packages/system/dracut/immutable-rootfs/30cos-immutable-rootfs/cos-mount-layout.sh#L58 // ParseMount will return a proper full disk path based on UUID or LABEL given // input: LABEL=FOO:/mount -// output: /dev/disk...:/mount +// output: /dev/disk...:/mount . func ParseMount(s string) string { switch { case strings.Contains(s, "UUID="): @@ -30,8 +31,9 @@ func ParseMount(s string) string { } // ReadCMDLineArg will return the pair of arg=value for a given arg if it was passed on the cmdline +// TODO: Split this into GetBool and GetValue to return decent defaults. func ReadCMDLineArg(arg string) []string { - cmdLine, err := os.ReadFile("/proc/cmdline") + cmdLine, err := os.ReadFile(GetHostProcCmdline()) if err != nil { return []string{} } @@ -43,7 +45,7 @@ func ReadCMDLineArg(arg string) []string { // For stanzas that have no value, we should return something better than an empty value // Otherwise anything can easily clean the value if dat[1] == "" { - res = append(res, "true") + res = append(res, "") } else { res = append(res, dat[1]) } @@ -52,7 +54,7 @@ func ReadCMDLineArg(arg string) []string { return res } -// IsMounted lets us know if the given device is currently mounted +// IsMounted lets us know if the given device is currently mounted. func IsMounted(dev string) bool { _, err := CommandWithPath(fmt.Sprintf("findmnt %s", dev)) return err == nil @@ -60,7 +62,7 @@ func IsMounted(dev string) bool { // DiskFSType will return the FS type for a given disk // Does NOT need to be mounted -// Needs full path so either /dev/sda1 or /dev/disk/by-{label,uuid}/{label,uuid} +// Needs full path so either /dev/sda1 or /dev/disk/by-{label,uuid}/{label,uuid} . func DiskFSType(s string) string { out, e := CommandWithPath(fmt.Sprintf("blkid %s -s TYPE -o value", s)) if e != nil { @@ -85,7 +87,7 @@ func AppendSlash(path string) string { return path } -// MountToFstab transforms a mount.Mount into a fstab.Mount so we can transform existing mounts into the fstab format +// MountToFstab transforms a mount.Mount into a fstab.Mount so we can transform existing mounts into the fstab format. func MountToFstab(m mount.Mount) *fstab.Mount { opts := map[string]string{} for _, o := range m.Options { @@ -112,7 +114,7 @@ func MountToFstab(m mount.Mount) *fstab.Mount { // As we mount on /sysroot during initramfs but the fstab file is for the real init process, we need to remove // Any mentions to /sysroot from the fstab lines, otherwise they won't work // Special care for the root (/sysroot) path as we can't just simple remove that path and call it a day -// as that will return an empty mountpoint which will break fstab mounting +// as that will return an empty mountpoint which will break fstab mounting. func CleanSysrootForFstab(path string) string { if IsUKI() { return path @@ -126,13 +128,13 @@ func CleanSysrootForFstab(path string) string { // Fsck will run fsck over the device // options are set on cmdline, but they are for systemd-fsck, -// so we need to interpret ourselves +// so we need to interpret ourselves. func Fsck(device string) error { if device == "tmpfs" { return nil } - mode := ReadCMDLineArg("fsck.mode=") - repair := ReadCMDLineArg("fsck.repair=") + mode := CleanupSlice(ReadCMDLineArg("fsck.mode=")) + repair := CleanupSlice(ReadCMDLineArg("fsck.repair=")) // Be safe with defaults if len(mode) == 0 { mode = []string{"auto"} @@ -176,7 +178,7 @@ func Fsck(device string) error { // MountProc will mount /proc // For now proc is needed to read the cmdline fully in uki mode -// in normal modes this should already be done by the initramfs process, so we can skip this +// in normal modes this should already be done by the initramfs process, so we can skip this. func MountProc() { _ = os.MkdirAll("/proc", 0755) if !IsMounted("/proc") { @@ -185,13 +187,13 @@ func MountProc() { } -// GetOemTimeout parses the cmdline to get the oem timeout to use. Defaults to 5 (converted into seconds afterwards) +// GetOemTimeout parses the cmdline to get the oem timeout to use. Defaults to 5 (converted into seconds afterwards). func GetOemTimeout() int { var time []string // Pick both stanzas until we deprecate the cos ones - timeCos := ReadCMDLineArg("rd.cos.oemtimeout=") - timeImmucore := ReadCMDLineArg("rd.immucore.oemtimeout=") + timeCos := CleanupSlice(ReadCMDLineArg("rd.cos.oemtimeout=")) + timeImmucore := CleanupSlice(ReadCMDLineArg("rd.immucore.oemtimeout=")) if len(timeCos) != 0 { time = timeCos @@ -203,7 +205,7 @@ func GetOemTimeout() int { if len(time) == 0 { return 5 } - converted, err := strconv.Atoi(time[1]) + converted, err := strconv.Atoi(time[0]) if err != nil { return 5 } @@ -212,13 +214,14 @@ func GetOemTimeout() int { // GetOverlayBase parses the cdmline and gets the overlay config // Format is rd.cos.overlay=tmpfs:20% or rd.cos.overlay=LABEL=$LABEL or rd.cos.overlay=UUID=$UUID -// Notice that this can be later override by the config coming from cos-layout.env +// Notice that this can be later override by the config coming from cos-layout.env . func GetOverlayBase() string { var overlayConfig []string // Pick both stanzas until we deprecate the cos ones - overlayConfigCos := ReadCMDLineArg("rd.cos.overlay=") - overlayConfigImmucore := ReadCMDLineArg("rd.immucore.overlay=") + // Clean up the slice in case the values are empty + overlayConfigCos := CleanupSlice(ReadCMDLineArg("rd.cos.overlay=")) + overlayConfigImmucore := CleanupSlice(ReadCMDLineArg("rd.immucore.overlay=")) if len(overlayConfigCos) != 0 { overlayConfig = overlayConfigCos @@ -231,22 +234,22 @@ func GetOverlayBase() string { return "tmpfs:20%" } - return overlayConfig[1] + return overlayConfig[0] } // GetOemLabel will ge the oem label to mount, first from the cmdline and if that fails, from the runtime -// This way users can override the oem label +// This way users can override the oem label. func GetOemLabel() string { var oemLabel string // Pick both stanzas until we deprecate the cos ones - oemLabelCos := ReadCMDLineArg("rd.cos.oemlabel=") - oemLabelImmucore := ReadCMDLineArg("rd.immucore.oemlabel=") + oemLabelCos := CleanupSlice(ReadCMDLineArg("rd.cos.oemlabel=")) + oemLabelImmucore := CleanupSlice(ReadCMDLineArg("rd.immucore.oemlabel=")) if len(oemLabelCos) != 0 { - oemLabel = oemLabelCos[1] + oemLabel = oemLabelCos[0] } if len(oemLabelImmucore) != 0 { - oemLabel = oemLabelImmucore[1] + oemLabel = oemLabelImmucore[0] } if oemLabel != "" { diff --git a/internal/version/version.go b/internal/version/version.go index c092273..04e9728 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -4,7 +4,7 @@ import "runtime" var ( version = "v0.0.1" - // gitCommit is the git sha1 + dirty if build from a dirty git + // gitCommit is the git sha1 + dirty if build from a dirty git. gitCommit = "none" ) @@ -22,7 +22,7 @@ type BuildInfo struct { GoVersion string `json:"go_version,omitempty"` } -// Get returns build info +// Get returns build info. func Get() BuildInfo { v := BuildInfo{ Version: GetVersion(), diff --git a/main.go b/main.go index fdb8b3c..2de5fb8 100644 --- a/main.go +++ b/main.go @@ -3,12 +3,13 @@ package main import ( "context" "fmt" + "os" + "github.com/kairos-io/immucore/internal/utils" "github.com/kairos-io/immucore/internal/version" "github.com/kairos-io/immucore/pkg/mount" "github.com/spectrocloud-labs/herd" "github.com/urfave/cli/v2" - "os" ) // Apply Immutability profiles. @@ -27,12 +28,15 @@ func main() { v := version.Get() utils.Log.Info().Str("commit", v.GitCommit).Str("compiled with", v.GoVersion).Str("version", v.Version).Msg("Immucore") - cmdline, _ := os.ReadFile("/proc/cmdline") + cmdline, _ := os.ReadFile(utils.GetHostProcCmdline()) utils.Log.Debug().Str("content", string(cmdline)).Msg("cmdline") g := herd.DAG(herd.EnableInit) // Get targets and state - targetImage, targetDevice = utils.GetTarget(c.Bool("dry-run")) + targetImage, targetDevice, err = utils.GetTarget(c.Bool("dry-run")) + if err != nil { + return err + } state = &mount.State{ Rootdir: utils.GetRootDir(), diff --git a/pkg/mount/dag_live_media.go b/pkg/mount/dag_live_media.go index 78ac6d7..263a6ce 100644 --- a/pkg/mount/dag_live_media.go +++ b/pkg/mount/dag_live_media.go @@ -5,10 +5,9 @@ import ( ) // RegisterLiveMedia registers the dag for booting from live media/netboot -// This sets the sentinel +// This sets the sentinel. func (s *State) RegisterLiveMedia(g *herd.Graph) error { - var err error // Maybe LogIfErrorAndPanic ? If no sentinel, a lot of config files are not going to run - err = s.LogIfErrorAndReturn(s.WriteSentinelDagStep(g), "write sentinel") + err := s.LogIfErrorAndReturn(s.WriteSentinelDagStep(g), "write sentinel") return err } diff --git a/pkg/mount/dag_normal_boot.go b/pkg/mount/dag_normal_boot.go index d164bcc..3840388 100644 --- a/pkg/mount/dag_normal_boot.go +++ b/pkg/mount/dag_normal_boot.go @@ -8,7 +8,7 @@ import ( // RegisterNormalBoot registers a dag for a normal boot, where we want to mount all the pieces that make up the // final system. This mounts root, oem, runs rootfs, loads the cos-layout.env file and mounts custom stuff from that file // and finally writes the fstab. -// This is all done on initramfs, very early, and ends up pivoting to the final system, usually under /sysroot +// This is all done on initramfs, very early, and ends up pivoting to the final system, usually under /sysroot. func (s *State) RegisterNormalBoot(g *herd.Graph) error { var err error diff --git a/pkg/mount/dag_steps.go b/pkg/mount/dag_steps.go index d4d4cf7..06683ac 100644 --- a/pkg/mount/dag_steps.go +++ b/pkg/mount/dag_steps.go @@ -4,6 +4,12 @@ import ( "context" "errors" "fmt" + "os" + "path/filepath" + "strings" + "syscall" + "time" + "github.com/hashicorp/go-multierror" cnst "github.com/kairos-io/immucore/internal/constants" internalUtils "github.com/kairos-io/immucore/internal/utils" @@ -12,14 +18,9 @@ import ( "github.com/mudler/go-kdetect" "github.com/spectrocloud-labs/herd" "golang.org/x/sys/unix" - "os" - "path/filepath" - "strings" - "syscall" - "time" ) -// MountTmpfsDagStep adds the step to mount /tmp +// MountTmpfsDagStep adds the step to mount /tmp . func (s *State) MountTmpfsDagStep(g *herd.Graph) error { return g.Add(cnst.OpMountTmpfs, herd.WithCallback(s.MountOP("tmpfs", "/tmp", "tmpfs", []string{"rw"}, 10*time.Second))) } @@ -27,7 +28,7 @@ func (s *State) MountTmpfsDagStep(g *herd.Graph) error { // MountRootDagStep will add the step to mount the Rootdir for the system // 1 - mount the state partition to find the images (active/passive/recovery) // 2 - mount the image as a loop device -// 3 - Mount the labels as /sysroot +// 3 - Mount the labels as /sysroot . func (s *State) MountRootDagStep(g *herd.Graph) error { var err error @@ -82,7 +83,7 @@ func (s *State) MountRootDagStep(g *herd.Graph) error { s.MountOP( s.TargetDevice, s.Rootdir, - "ext4", // TODO: Get this just in time? Currently if using DiskFSType is run immediately which is bad becuase its not mounted + "ext4", // TODO: Get this just in time? Currently if using DiskFSType is run immediately which is bad because its not mounted []string{ s.RootMountMode, "suid", @@ -110,7 +111,7 @@ func (s *State) InitramfsStageDagStep(g *herd.Graph, deps ...string) error { return g.Add(cnst.OpInitramfsHook, herd.WithDeps(deps...), herd.WeakDeps, herd.WithCallback(s.RunStageOp("initramfs"))) } -// LoadEnvLayoutDagStep will add the stage to load from cos-layout.env and fill the proper CustomMounts, OverlayDirs and BindMounts +// LoadEnvLayoutDagStep will add the stage to load from cos-layout.env and fill the proper CustomMounts, OverlayDirs and BindMounts. func (s *State) LoadEnvLayoutDagStep(g *herd.Graph, deps ...string) error { return g.Add(cnst.OpLoadConfig, herd.WithDeps(deps...), @@ -163,8 +164,8 @@ func (s *State) LoadEnvLayoutDagStep(g *herd.Graph, deps ...string) error { // Parse custom mounts also from cmdline (rd.immucore.mount=) // Parse custom mounts also from env file (VOLUMES) var mounts []string - mounts = internalUtils.ReadCMDLineArg("rd.cos.mount=") - mounts = append(mounts, internalUtils.ReadCMDLineArg("rd.immucore.mount=")...) + mounts = internalUtils.CleanupSlice(internalUtils.ReadCMDLineArg("rd.cos.mount=")) + mounts = append(mounts, internalUtils.CleanupSlice(internalUtils.ReadCMDLineArg("rd.immucore.mount="))...) mounts = append(mounts, env["VOLUMES"]) for _, v := range mounts { addLine(internalUtils.ParseMount(v)) @@ -174,7 +175,7 @@ func (s *State) LoadEnvLayoutDagStep(g *herd.Graph, deps ...string) error { })) } -// MountOemDagStep will add mounting COS_OEM partition under s.Rootdir + /oem +// MountOemDagStep will add mounting COS_OEM partition under s.Rootdir + /oem . func (s *State) MountOemDagStep(g *herd.Graph, deps ...string) error { return g.Add(cnst.OpMountOEM, herd.WithDeps(deps...), @@ -195,7 +196,7 @@ func (s *State) MountOemDagStep(g *herd.Graph, deps ...string) error { } // MountBaseOverlayDagStep will add mounting /run/overlay as an overlay dir -// Requires the config-load step because some parameters can come from there +// Requires the config-load step because some parameters can come from there. func (s *State) MountBaseOverlayDagStep(g *herd.Graph) error { return g.Add(cnst.OpMountBaseOverlay, herd.WithDeps(cnst.OpLoadConfig), @@ -225,7 +226,7 @@ func (s *State) MountBaseOverlayDagStep(g *herd.Graph) error { ) } -// MountCustomOverlayDagStep will add mounting s.OverlayDirs under /run/overlay +// MountCustomOverlayDagStep will add mounting s.OverlayDirs under /run/overlay . func (s *State) MountCustomOverlayDagStep(g *herd.Graph) error { return g.Add(cnst.OpOverlayMount, herd.WithDeps(cnst.OpLoadConfig, cnst.OpMountBaseOverlay), @@ -250,7 +251,7 @@ func (s *State) MountCustomOverlayDagStep(g *herd.Graph) error { ) } -// MountCustomMountsDagStep will add mounting s.CustomMounts +// MountCustomMountsDagStep will add mounting s.CustomMounts . func (s *State) MountCustomMountsDagStep(g *herd.Graph) error { return g.Add(cnst.OpCustomMounts, herd.WithDeps(cnst.OpLoadConfig), @@ -287,7 +288,7 @@ func (s *State) MountCustomMountsDagStep(g *herd.Graph) error { } // MountCustomBindsDagStep will add mounting s.BindMounts -// mount state is defined over a custom mount (/usr/local/.state for instance, needs to be mounted over a device) +// mount state is defined over a custom mount (/usr/local/.state for instance, needs to be mounted over a device). func (s *State) MountCustomBindsDagStep(g *herd.Graph) error { return g.Add(cnst.OpMountBind, herd.WithDeps(cnst.OpCustomMounts, cnst.OpLoadConfig), @@ -317,7 +318,7 @@ func (s *State) MountCustomBindsDagStep(g *herd.Graph) error { } // WriteFstabDagStep will add writing the final fstab file with all the mounts -// Depends on everything but weak, so it will still try to write +// Depends on everything but weak, so it will still try to write. func (s *State) WriteFstabDagStep(g *herd.Graph) error { return g.Add(cnst.OpWriteFstab, herd.WithDeps(cnst.OpMountRoot, cnst.OpDiscoverState, cnst.OpLoadConfig, cnst.OpMountOEM, cnst.OpCustomMounts, cnst.OpMountBind, cnst.OpOverlayMount), @@ -326,7 +327,7 @@ func (s *State) WriteFstabDagStep(g *herd.Graph) error { } // WriteSentinelDagStep sets the sentinel file to identify the boot mode. -// This is used by several things to know in which state they are, for example cloud configs +// This is used by several things to know in which state they are, for example cloud configs. func (s *State) WriteSentinelDagStep(g *herd.Graph) error { return g.Add(cnst.OpSentinel, herd.WithCallback(func(ctx context.Context) error { @@ -356,7 +357,7 @@ func (s *State) WriteSentinelDagStep(g *herd.Graph) error { // Workaround for runtime not detecting netboot/rd.cos.disable/rd.immucore.disable as live_mode // TODO: drop once the netboot/rd.cos.disable detection change is on the kairos sdk - cmdline, err := os.ReadFile("/proc/cmdline") + cmdline, _ := os.ReadFile(internalUtils.GetHostProcCmdline()) cmdlineS := string(cmdline) if strings.Contains(cmdlineS, "netboot") || len(internalUtils.ReadCMDLineArg("rd.cos.disable")) > 0 || len(internalUtils.ReadCMDLineArg("rd.immucore.disable")) > 0 { sentinel = "live_mode" @@ -382,7 +383,7 @@ func (s *State) WriteSentinelDagStep(g *herd.Graph) error { // UKIBootInitDagStep tries to launch /sbin/init in root and pass over the system // booting to the real init process -// Drops to emergency if not able to. Panic if it cant even launch emergency +// Drops to emergency if not able to. Panic if it cant even launch emergency. func (s *State) UKIBootInitDagStep(g *herd.Graph, deps ...string) error { return g.Add(cnst.OpUkiInit, herd.WithDeps(deps...), @@ -403,7 +404,7 @@ func (s *State) UKIBootInitDagStep(g *herd.Graph, deps ...string) error { })) } -// UKIRemountRootRODagStep remount root read only +// UKIRemountRootRODagStep remount root read only. func (s *State) UKIRemountRootRODagStep(g *herd.Graph, deps ...string) error { return g.Add(cnst.OpRemountRootRO, herd.WithDeps(deps...), @@ -450,7 +451,7 @@ func (s *State) UKIUdevDaemon(g *herd.Graph) error { // LoadKernelModules loads kernel modules needed during uki boot to load the disks for. // Mainly block devices and net devices -// probably others down the line +// probably others down the line. func (s *State) LoadKernelModules(g *herd.Graph) error { return g.Add("kernel-modules", herd.WithCallback(func(ctx context.Context) error { diff --git a/pkg/mount/dag_uki_boot.go b/pkg/mount/dag_uki_boot.go index fe0a320..47f610b 100644 --- a/pkg/mount/dag_uki_boot.go +++ b/pkg/mount/dag_uki_boot.go @@ -5,7 +5,7 @@ import ( "github.com/spectrocloud-labs/herd" ) -// RegisterUKI registers the dag for booting from UKI +// RegisterUKI registers the dag for booting from UKI. func (s *State) RegisterUKI(g *herd.Graph) error { // Write sentinel s.LogIfError(s.WriteSentinelDagStep(g), "sentinel") diff --git a/pkg/mount/mounts_test.go b/pkg/mount/mounts_test.go deleted file mode 100644 index a2da9ed..0000000 --- a/pkg/mount/mounts_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package mount - -import ( - "github.com/kairos-io/immucore/internal/utils" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("mount utils", func() { - BeforeEach(func() { - }) - - Context("ReadCMDLineArg", func() { - It("splits arguments from cmdline", func() { - Skip("No way of overriding the cmdline yet") - Expect(len(utils.ReadCMDLineArg("testvalue/key="))).To(Equal(1)) - }) - It("returns properly for stanzas without value", func() { - Skip("No way of overriding the cmdline yet") - Expect(len(utils.ReadCMDLineArg("singlevalue"))).To(Equal(1)) - }) - }) -}) diff --git a/pkg/mount/state.go b/pkg/mount/state.go index f3551e2..195ec27 100644 --- a/pkg/mount/state.go +++ b/pkg/mount/state.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "github.com/kairos-io/kairos/pkg/utils" "os" "path/filepath" "time" @@ -13,6 +12,7 @@ import ( "github.com/deniswernert/go-fstab" "github.com/kairos-io/immucore/internal/constants" internalUtils "github.com/kairos-io/immucore/internal/utils" + "github.com/kairos-io/kairos/pkg/utils" "github.com/rs/zerolog" "github.com/spectrocloud-labs/herd" ) @@ -62,7 +62,7 @@ func (s *State) WriteFstab(fstabFile string) func(context.Context) error { } // 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 +// 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(ctx context.Context) error { if stage == "rootfs" && !internalUtils.IsUKI() { @@ -91,7 +91,7 @@ func (s *State) RunStageOp(stage string) func(context.Context) error { } } -// MountOP creates and executes a mount operation +// MountOP creates and executes a mount operation. func (s *State) MountOP(what, where, t string, options []string, timeout time.Duration) func(context.Context) error { internalUtils.Log.With().Str("what", what).Str("where", where).Str("type", t).Strs("options", options).Logger() @@ -149,7 +149,7 @@ func (s *State) MountOP(what, where, t string, options []string, timeout time.Du } } -// WriteDAG writes the dag +// 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) @@ -165,7 +165,7 @@ func (s *State) WriteDAG(g *herd.Graph) (out string) { } // LogIfError will log if there is an error with the given context as message -// Context can be empty +// Context can be empty. func (s *State) LogIfError(e error, msgContext string) { if e != nil { internalUtils.Log.Err(e).Msg(msgContext) @@ -174,7 +174,7 @@ func (s *State) LogIfError(e error, msgContext string) { // LogIfErrorAndReturn will log if there is an error with the given context as message // Context can be empty -// Will also return the error +// Will also return the error. func (s *State) LogIfErrorAndReturn(e error, msgContext string) error { if e != nil { internalUtils.Log.Err(e).Msg(msgContext) @@ -184,7 +184,7 @@ func (s *State) LogIfErrorAndReturn(e error, msgContext string) error { // LogIfErrorAndPanic will log if there is an error with the given context as message // Context can be empty -// Will also panic +// Will also panic. func (s *State) LogIfErrorAndPanic(e error, msgContext string) { if e != nil { internalUtils.Log.Err(e).Msg(msgContext) diff --git a/tests/mocks/ghw_mock.go b/tests/mocks/ghw_mock.go new file mode 100644 index 0000000..4f7382e --- /dev/null +++ b/tests/mocks/ghw_mock.go @@ -0,0 +1,173 @@ +/* +Copyright © 2022 SUSE LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mocks + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/jaypipes/ghw/pkg/block" + "github.com/jaypipes/ghw/pkg/context" + "github.com/jaypipes/ghw/pkg/linuxpath" +) + +// GhwMock is used to construct a fake disk to present to ghw when scanning block devices +// The way this works is ghw will use the existing files in the system to determine the different disks, partitions and +// mountpoints. It uses /sys/block, /proc/self/mounts and /run/udev/data to gather everything +// It also has an entrypoint to overwrite the root dir from which the paths are constructed so that allows us to override +// it easily and make it read from a different location. +// This mock is used to construct a fake FS with all its needed files on a different chroot and just add a Disk with its +// partitions and let the struct do its thing creating files and mountpoints and such +// You can even just pass no disks to simulate a system in which there is no disk/no cos partitions. +type GhwMock struct { + chroot string + paths *linuxpath.Paths + disks []block.Disk + mounts []string +} + +// AddDisk adds a disk to GhwMock. +func (g *GhwMock) AddDisk(disk block.Disk) { + g.disks = append(g.disks, disk) +} + +// AddPartitionToDisk will add a partition to the given disk and call Clean+CreateDevices, so we recreate all files +// It makes no effort checking if the disk exists. +func (g *GhwMock) AddPartitionToDisk(diskName string, partition *block.Partition) { + for _, disk := range g.disks { + if disk.Name == diskName { + disk.Partitions = append(disk.Partitions, partition) + g.Clean() + g.CreateDevices() + } + } +} + +// CreateDevices will create a new context and paths for ghw using the Chroot value as base, then set the env var GHW_ROOT so the +// ghw library picks that up and then iterate over the disks and partitions and create the necessary files. +func (g *GhwMock) CreateDevices() { + d, _ := os.MkdirTemp("", "ghwmock") + g.chroot = d + ctx := context.New() + ctx.Chroot = d + g.paths = linuxpath.New(ctx) + _ = os.Setenv("GHW_CHROOT", g.chroot) + // Create the /sys/block dir + _ = os.MkdirAll(g.paths.SysBlock, 0755) + // Create the /run/udev/data dir + _ = os.MkdirAll(g.paths.RunUdevData, 0755) + // Create only the /proc/self dir, we add the mounts file afterwards + procDir, _ := filepath.Split(g.paths.ProcMounts) + _ = os.MkdirAll(procDir, 0755) + + for indexDisk, disk := range g.disks { + // For each dir we create the /sys/block/DISK_NAME + diskPath := filepath.Join(g.paths.SysBlock, disk.Name) + _ = os.Mkdir(diskPath, 0755) + for indexPart, partition := range disk.Partitions { + // For each partition we create the /sys/block/DISK_NAME/PARTITION_NAME + _ = os.Mkdir(filepath.Join(diskPath, partition.Name), 0755) + // Create the /sys/block/DISK_NAME/PARTITION_NAME/dev file which contains the major:minor of the partition + _ = os.WriteFile(filepath.Join(diskPath, partition.Name, "dev"), []byte(fmt.Sprintf("%d:6%d\n", indexDisk, indexPart)), 0644) + // Create the /run/udev/data/bMAJOR:MINOR file with the data inside to mimic the udev database + data := []string{fmt.Sprintf("E:ID_FS_LABEL=%s\n", partition.Label)} + if partition.Type != "" { + data = append(data, fmt.Sprintf("E:ID_FS_TYPE=%s\n", partition.Type)) + } + _ = os.WriteFile(filepath.Join(g.paths.RunUdevData, fmt.Sprintf("b%d:6%d", indexDisk, indexPart)), []byte(strings.Join(data, "")), 0644) + // If we got a mountpoint, add it to our fake /proc/self/mounts + if partition.MountPoint != "" { + // Check if the partition has a fs, otherwise default to ext4 + if partition.Type == "" { + partition.Type = "ext4" + } + // Prepare the g.mounts with all the mount lines + g.mounts = append( + g.mounts, + fmt.Sprintf("%s %s %s ro,relatime 0 0\n", filepath.Join("/dev", partition.Name), partition.MountPoint, partition.Type)) + } + } + } + // Finally, write all the mounts + _ = os.WriteFile(g.paths.ProcMounts, []byte(strings.Join(g.mounts, "")), 0644) +} + +// RemoveDisk will remove the files for a disk. It makes no effort to check if the disk exists or not. +func (g *GhwMock) RemoveDisk(disk string) { + // This could be simpler I think, just removing the /sys/block/DEVICE should make ghw not find anything and not search + // for partitions, but just in case do it properly + var newMounts []string + diskPath := filepath.Join(g.paths.SysBlock, disk) + _ = os.RemoveAll(diskPath) + + // Try to find any mounts that match the disk given and remove them from the mounts + for _, mount := range g.mounts { + fields := strings.Fields(mount) + // If first field does not contain the /dev/DEVICE, add it to the newmounts + if !strings.Contains(fields[0], filepath.Join("/dev", disk)) { + newMounts = append(newMounts, mount) + } + } + g.mounts = newMounts + // Write the mounts again + _ = os.WriteFile(g.paths.ProcMounts, []byte(strings.Join(g.mounts, "")), 0644) +} + +// RemovePartitionFromDisk will remove the files for a partition +// It makes no effort checking if the disk/partition/files exist. +func (g *GhwMock) RemovePartitionFromDisk(diskName string, partitionName string) { + var newMounts []string + diskPath := filepath.Join(g.paths.SysBlock, diskName) + // Read the dev major:minor + devName, _ := os.ReadFile(filepath.Join(diskPath, partitionName, "dev")) + // Remove the MAJOR:MINOR file from the udev database + _ = os.RemoveAll(filepath.Join(g.paths.RunUdevData, fmt.Sprintf("b%s", devName))) + // Remove the /sys/block/DISK/PARTITION dir + _ = os.RemoveAll(filepath.Join(diskPath, partitionName)) + + // Try to find any mounts that match the partition given and remove them from the mounts + for _, mount := range g.mounts { + fields := strings.Fields(mount) + // If first field does not contain the /dev/PARTITION, add it to the newmounts + if !strings.Contains(fields[0], filepath.Join("/dev", partitionName)) { + newMounts = append(newMounts, mount) + } + } + g.mounts = newMounts + // Write the mounts again + _ = os.WriteFile(g.paths.ProcMounts, []byte(strings.Join(g.mounts, "")), 0644) + // Remove it from the partitions list + for index, disk := range g.disks { + if disk.Name == diskName { + var newPartitions []*block.Partition + for _, partition := range disk.Partitions { + if partition.Name != partitionName { + newPartitions = append(newPartitions, partition) + } + } + g.disks[index].Partitions = newPartitions + } + } +} + +// Clean will remove the chroot dir and unset the env var. +func (g *GhwMock) Clean() { + _ = os.Unsetenv("GHW_CHROOT") + _ = os.RemoveAll(g.chroot) +} diff --git a/pkg/mount/state_test.go b/tests/state_test.go similarity index 99% rename from pkg/mount/state_test.go rename to tests/state_test.go index 9c7a939..f928845 100644 --- a/pkg/mount/state_test.go +++ b/tests/state_test.go @@ -1,4 +1,4 @@ -package mount_test +package tests import ( "context" diff --git a/pkg/mount/state_suite_test.go b/tests/suite_test.go similarity index 73% rename from pkg/mount/state_suite_test.go rename to tests/suite_test.go index 7acb880..1ce3286 100644 --- a/pkg/mount/state_suite_test.go +++ b/tests/suite_test.go @@ -1,4 +1,4 @@ -package mount_test +package tests import ( "testing" @@ -9,5 +9,5 @@ import ( func TestSuite(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "mount test Suite") + RunSpecs(t, "Test Suite") } diff --git a/tests/utils_test.go b/tests/utils_test.go new file mode 100644 index 0000000..af7af55 --- /dev/null +++ b/tests/utils_test.go @@ -0,0 +1,351 @@ +package tests + +import ( + "github.com/containerd/containerd/mount" + "github.com/jaypipes/ghw/pkg/block" + "github.com/kairos-io/immucore/internal/utils" + "github.com/kairos-io/immucore/tests/mocks" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/twpayne/go-vfs" + "github.com/twpayne/go-vfs/vfst" + "os" + "path/filepath" +) + +var _ = Describe("mount utils", func() { + var fs vfs.FS + var cleanup func() + + BeforeEach(func() { + fs, cleanup, _ = vfst.NewTestFS(map[string]interface{}{ + "/proc/cmdline": "", + }) + _, err := fs.Stat("/proc/cmdline") + Expect(err).ToNot(HaveOccurred()) + fakeCmdline, _ := fs.RawPath("/proc/cmdline") + err = os.Setenv("HOST_PROC_CMDLINE", fakeCmdline) + Expect(err).ToNot(HaveOccurred()) + }) + AfterEach(func() { + cleanup() + }) + + Context("ReadCMDLineArg", func() { + BeforeEach(func() { + err := fs.WriteFile("/proc/cmdline", []byte("test/key=value1 rd.immucore.debug rd.immucore.uki rd.cos.oemlabel=FAKE_LABEL empty=\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + }) + It("splits arguments from cmdline", func() { + value := utils.ReadCMDLineArg("test/key=") + Expect(len(value)).To(Equal(1)) + Expect(value[0]).To(Equal("value1")) + value = utils.ReadCMDLineArg("rd.cos.oemlabel=") + Expect(len(value)).To(Equal(1)) + Expect(value[0]).To(Equal("FAKE_LABEL")) + // This is mostly wrong, it should return and empty value, not a []string of 1 empty value + // Requires refactoring + value = utils.ReadCMDLineArg("empty=") + Expect(len(value)).To(Equal(1)) + Expect(value[0]).To(Equal("")) + + }) + It("returns properly for stanzas without value", func() { + Expect(len(utils.ReadCMDLineArg("rd.immucore.debug"))).To(Equal(1)) + Expect(len(utils.ReadCMDLineArg("rd.immucore.uki"))).To(Equal(1)) + }) + }) + Context("GetRootDir", func() { + It("Returns / for uki", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.immucore.uki"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetRootDir()).To(Equal("/")) + }) + It("Returns /sysroot", func() { + Expect(utils.GetRootDir()).To(Equal("/sysroot")) + }) + }) + Context("UniqueSlice", func() { + It("Removes duplicates", func() { + dups := []string{"a", "b", "c", "d", "b", "a"} + dupsRemoved := utils.UniqueSlice(dups) + Expect(len(dupsRemoved)).To(Equal(4)) + }) + }) + Context("ReadEnv", func() { + It("Parses correctly an env file", func() { + tmpDir, err := os.MkdirTemp("", "") + Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(tmpDir) + err = os.WriteFile(filepath.Join(tmpDir, "layout.env"), []byte("OVERLAY=\"tmpfs:25%\"\nPERSISTENT_STATE_BIND=\"true\"\nPERSISTENT_STATE_PATHS=\"/home /opt /root\"\nRW_PATHS=\"/var /etc /srv\"\nVOLUMES=\"LABEL=COS_OEM:/oem LABEL=COS_PERSISTENT:/usr/local\""), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + env, err := utils.ReadEnv(filepath.Join(tmpDir, "layout.env")) + Expect(err).ToNot(HaveOccurred()) + Expect(env).To(HaveKey("OVERLAY")) + Expect(env).To(HaveKey("PERSISTENT_STATE_BIND")) + Expect(env).To(HaveKey("PERSISTENT_STATE_PATHS")) + Expect(env).To(HaveKey("RW_PATHS")) + Expect(env).To(HaveKey("VOLUMES")) + Expect(env["OVERLAY"]).To(Equal("tmpfs:25%")) + Expect(env["PERSISTENT_STATE_BIND"]).To(Equal("true")) + Expect(env["PERSISTENT_STATE_PATHS"]).To(Equal("/home /opt /root")) + Expect(env["RW_PATHS"]).To(Equal("/var /etc /srv")) + Expect(env["VOLUMES"]).To(Equal("LABEL=COS_OEM:/oem LABEL=COS_PERSISTENT:/usr/local")) + }) + }) + Context("CleanupSlice", func() { + It("Cleans up the slice of empty values", func() { + slice := []string{"", " "} + sliceCleaned := utils.CleanupSlice(slice) + Expect(len(sliceCleaned)).To(Equal(0)) + }) + }) + Context("GetTarget", func() { + It("Returns a fake target if called with dry run", func() { + target, label, err := utils.GetTarget(true) + Expect(err).ToNot(HaveOccurred()) + Expect(target).To(Equal("fake")) + // We cant manipulate runtime, so it will return an empty label as it cant identify where are we + Expect(label).To(Equal("")) + }) + It("Returns a fake target if immucore is disabled", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.immucore.disabled\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + target, label, err := utils.GetTarget(false) + Expect(err).ToNot(HaveOccurred()) + Expect(target).To(Equal("fake")) + // We cant manipulate runtime, so it will return an empty label as it cant identify where are we + Expect(label).To(Equal("")) + }) + It("Returns the proper target from cmdline", func() { + err := fs.WriteFile("/proc/cmdline", []byte("cos-img/filename=active.img\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + target, label, err := utils.GetTarget(false) + Expect(err).ToNot(HaveOccurred()) + Expect(target).To(Equal("active.img")) + // We cant manipulate runtime, so it will return an empty label as it cant identify where are we + Expect(label).To(Equal("")) + }) + It("Returns an empty target if we are on UKI", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.immucore.uki\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + target, label, err := utils.GetTarget(false) + Expect(err).ToNot(HaveOccurred()) + Expect(target).To(Equal("")) + // We cant manipulate runtime, so it will return an empty label as it cant identify where are we + Expect(label).To(Equal("")) + }) + It("Returns an error if we dont have the target in the cmdline", func() { + target, label, err := utils.GetTarget(false) + Expect(err).To(HaveOccurred()) + Expect(target).To(Equal("")) + Expect(label).To(Equal("")) + }) + }) + Context("DisableImmucore", func() { + It("Disables immucore if cmdline contains live:LABEL", func() { + err := fs.WriteFile("/proc/cmdline", []byte("root=live:LABEL=COS_LIVE\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.DisableImmucore()).To(BeTrue()) + }) + It("Disables immucore if cmdline contains live:CDLABEL", func() { + err := fs.WriteFile("/proc/cmdline", []byte("root=live:CDLABEL=COS_LIVE\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.DisableImmucore()).To(BeTrue()) + }) + It("Disables immucore if cmdline contains netboot", func() { + err := fs.WriteFile("/proc/cmdline", []byte("netboot\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.DisableImmucore()).To(BeTrue()) + }) + It("Disables immucore if cmdline contains rd.cos.disable", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.cos.disable\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.DisableImmucore()).To(BeTrue()) + }) + It("Disables immucore if cmdline contains rd.immucore.disable", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.immucore.disable\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.DisableImmucore()).To(BeTrue()) + }) + It("Enables immucore by default", func() { + Expect(utils.DisableImmucore()).To(BeFalse()) + }) + }) + Context("RootRW", func() { + It("Defaults to RO", func() { + Expect(utils.RootRW()).To(Equal("ro")) + }) + It("Sets RW if set via cmdline with rd.cos.debugrw", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.cos.debugrw\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.RootRW()).To(Equal("rw")) + }) + It("Sets RW if set via cmdline with rd.immucore.debugrw", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.immucore.debugrw\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.RootRW()).To(Equal("rw")) + }) + It("Sets RW if set via cmdline with both rd.cos.debugrw and rd.immucore.debugrw at the same time", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.cos.debugrw rd.immucore.debugrw\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.RootRW()).To(Equal("rw")) + }) + }) + Context("IsUKI", func() { + It("Returns false in a normal boot", func() { + Expect(utils.IsUKI()).To(BeFalse()) + }) + It("Returns true if set via cmdline with rd.immucore.uki", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.immucore.uki\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.IsUKI()).To(BeTrue()) + }) + }) + Context("ParseMount", func() { + It("Returns disk path by LABEL", func() { + Expect(utils.ParseMount("LABEL=MY_LABEL")).To(Equal("/dev/disk/by-label/MY_LABEL")) + }) + It("Returns disk path by UUID", func() { + Expect(utils.ParseMount("UUID=9999")).To(Equal("/dev/disk/by-uuid/9999")) + }) + }) + Context("AppendSlash", func() { + It("Appends a slash if it doesnt have one", func() { + noSlash := "/noslash" + Expect(utils.AppendSlash(noSlash)).To(Equal("/noslash/")) + }) + It("Does not append a slash if it already has one", func() { + slash := "/yesslash/" + Expect(utils.AppendSlash(slash)).To(Equal("/yesslash/")) + }) + }) + Context("MountToFstab", func() { + It("Generates teh proper fstab config", func() { + m := mount.Mount{ + Type: "fakefs", + Source: "/dev/fake", + Options: []string{"option1", "option=2"}, + } + fstab := utils.MountToFstab(m) + fstab.File = "/mnt/fake" + // Options can be shown in whatever order, so regexp that + Expect(fstab.String()).To(MatchRegexp("/dev/fake /mnt/fake fakefs (option1|option=2),(option=2|option1) 0 0")) + Expect(fstab.Spec).To(Equal("/dev/fake")) + Expect(fstab.VfsType).To(Equal("fakefs")) + Expect(fstab.MntOps).To(HaveKey("option1")) + Expect(fstab.MntOps).To(HaveKey("option")) + Expect(fstab.MntOps["option1"]).To(Equal("")) + Expect(fstab.MntOps["option"]).To(Equal("2")) + }) + }) + Context("CleanSysrootForFstab", func() { + It("Removes /sysroot", func() { + Expect(utils.CleanSysrootForFstab("/sysroot/dev")).To(Equal("/dev")) + Expect(utils.CleanSysrootForFstab("/sysroot/sysroot/dev")).To(Equal("/dev")) + Expect(utils.CleanSysrootForFstab("sysroot/dev")).To(Equal("sysroot/dev")) + Expect(utils.CleanSysrootForFstab("/dev/sysroot/dev")).To(Equal("/dev/dev")) + Expect(utils.CleanSysrootForFstab("/dev/")).To(Equal("/dev/")) + Expect(utils.CleanSysrootForFstab("/dev")).To(Equal("/dev")) + Expect(utils.CleanSysrootForFstab("//sysroot/dev")).To(Equal("//dev")) + Expect(utils.CleanSysrootForFstab("/sysroot//dev")).To(Equal("//dev")) + }) + }) + Context("GetOemTimeout", func() { + It("Gets timeout from rd.cos.oemtimeout", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.cos.oemtimeout=100\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOemTimeout()).To(Equal(100)) + }) + It("Gets timeout from rd.immucore.oemtimeout", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.immucore.oemtimeout=200\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOemTimeout()).To(Equal(200)) + }) + It("Gets timeout from both rd.cos.oemtimeout and rd.immucore.oemtimeout(immucore has precedence)", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.cos.oemtimeout=100 rd.immucore.oemtimeout=200\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOemTimeout()).To(Equal(200)) + }) + It("Fails to parse from cmdline and gets default", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.immucore.oemtimeout=really\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOemTimeout()).To(Equal(5)) + err = fs.WriteFile("/proc/cmdline", []byte("rd.immucore.oemtimeout=\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOemTimeout()).To(Equal(5)) + }) + It("Gets default timeout", func() { + Expect(utils.GetOemTimeout()).To(Equal(5)) + }) + }) + Context("GetOverlayBase", func() { + It("Gets overlay from rd.cos.overlay", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.cos.overlay=tmpfs:100%\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOverlayBase()).To(Equal("tmpfs:100%")) + }) + It("Gets overlay from rd.immucore.overlay", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.immucore.overlay=tmpfs:200%\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOverlayBase()).To(Equal("tmpfs:200%")) + }) + It("Gets overlay from both rd.cos.overlay and rd.immucore.overlay(immucore has precedence)", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.cos.overlay=tmpfs:100% rd.immucore.overlay=tmpfs:200%\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOverlayBase()).To(Equal("tmpfs:200%")) + }) + It("Fails to parse from cmdline and gets default", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.immucore.overlay=\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOverlayBase()).To(Equal("tmpfs:20%")) + err = fs.WriteFile("/proc/cmdline", []byte("rd.immucore.overlay=\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOverlayBase()).To(Equal("tmpfs:20%")) + }) + It("Gets default overlay", func() { + Expect(utils.GetOverlayBase()).To(Equal("tmpfs:20%")) + }) + }) + Context("GetOemLabel", func() { + It("Gets label from rd.cos.oemlabel", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.cos.oemlabel=COS_LABEL\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOemLabel()).To(Equal("COS_LABEL")) + }) + It("Gets label from rd.immucore.oemlabel", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.immucore.oemlabel=IMMUCORE_LABEL\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOemLabel()).To(Equal("IMMUCORE_LABEL")) + }) + It("Gets label from both rd.cos.oemlabel and rd.immucore.oemlabel(immucore has precedence)", func() { + err := fs.WriteFile("/proc/cmdline", []byte("rd.cos.oemlabel=COS_LABEL rd.immucore.oemlabel=IMMUCORE_LABEL\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOemLabel()).To(Equal("IMMUCORE_LABEL")) + }) + It("Fails to parse from cmdline and gets default from runtime", func() { + mainDisk := block.Disk{ + Name: "device", + Partitions: []*block.Partition{ + { + Name: "device2", + Label: "COS_OEM", + Type: "ext4", + MountPoint: "/oem", + }, + }, + } + ghwTest := mocks.GhwMock{} + ghwTest.AddDisk(mainDisk) + ghwTest.CreateDevices() + defer ghwTest.Clean() + + err := fs.WriteFile("/proc/cmdline", []byte("rd.cos.oemlabel=\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOemLabel()).To(Equal("COS_OEM")) + err = fs.WriteFile("/proc/cmdline", []byte("rd.immucore.oemlabel=\n"), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + Expect(utils.GetOemLabel()).To(Equal("COS_OEM")) + }) + }) +})