Add tests and fix some issues that arised from testing (#74)

* Add tests and fix some issues that arised from testing

Mainly around the cmdargs and how many items it returns.
Also drop the iso target and jobs as its not necessary now

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

* Lint

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>

---------

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
This commit is contained in:
Itxaka 2023-03-02 16:46:25 +01:00 committed by GitHub
parent b0b326313b
commit fecfbf8e92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 673 additions and 220 deletions

View File

@ -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

View File

@ -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:

20
.golangci.yml Normal file
View File

@ -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

View File

@ -80,62 +80,3 @@ dracut-artifacts:
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

1
go.mod
View File

@ -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
)

3
go.sum
View File

@ -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=

View File

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

View File

@ -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
}

View File

@ -1,9 +1,10 @@
package utils
import (
"github.com/rs/zerolog"
"io"
"os"
"github.com/rs/zerolog"
)
var Log zerolog.Logger

View File

@ -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 != "" {

View File

@ -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(),

10
main.go
View File

@ -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(),

View File

@ -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
}

View File

@ -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

View File

@ -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 {

View File

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

View File

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

View File

@ -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)

173
tests/mocks/ghw_mock.go Normal file
View File

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

View File

@ -1,4 +1,4 @@
package mount_test
package tests
import (
"context"

View File

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

351
tests/utils_test.go Normal file
View File

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