Add iso builder

This adds a new package for the iso builder run directly on go.
This is extracted from the original elemental-cli and then from the now
build-less kairos-agent

This uses no deps on elemental, only deps are on kairos-agent for the
config stuff mainly.

Signed-off-by: Itxaka <itxaka.garcia@spectrocloud.com>
This commit is contained in:
Itxaka 2023-07-18 12:46:50 +02:00
parent e5f563c4db
commit 2be9cfce66
15 changed files with 2766 additions and 3 deletions

View File

@ -3,12 +3,27 @@ ARG LEAP_VERSION=15.4
ARG LUET_VERSION=0.33.0
FROM quay.io/luet/base:$LUET_VERSION AS luet
FROM golang:1.20 as enki
ENV CGO_ENABLED=0
COPY ./enki /src/enki
WORKDIR /src/enki
RUN go mod download
# Set arg/env after go mod download, otherwise we invalidate the cached layers due to the commit changing easily
ARG ENKI_VERSION=0.0.1
ARG ENKI_COMMIT=""
ENV ENKI_VERSION=${ENKI_VERSION}
ENV ENKI_COMMIT=${ENKI_COMMIT}
RUN go build \
-ldflags "-w -s \
-X github.com/kairos-io/enki/internal/version.version=$ENKI_VERSION \
-X github.com/kairos-io/enki/internal/version.gitCommit=$ENKI_COMMIT" \
-o /usr/bin/enki
FROM opensuse/leap:$LEAP_VERSION as luet-install
COPY --from=luet /usr/bin/luet /usr/bin/luet
ENV LUET_NOLOCK=true
ENV TMPDIR=/tmp
COPY luet.yaml /etc/luet/luet.yaml
RUN luet install -y system/elemental-cli
RUN luet install -y livecd/grub2 --system-target /grub2
RUN luet install -y livecd/grub2-efi-image --system-target /efi
@ -42,7 +57,6 @@ FROM quay.io/kairos/packages:grub-config-static-0.1 AS grub-raw-config
FROM quay.io/kairos/packages:grub-artifacts-static-0.1 AS grub-raw-artifacts
FROM opensuse/leap:$LEAP_VERSION
COPY --from=luet-install /usr/bin/elemental /usr/bin/elemental
COPY --from=luet /usr/bin/luet /usr/bin/luet
# ISO files
@ -101,5 +115,6 @@ COPY ./netboot.sh /netboot.sh
COPY defaults.yaml /defaults.yaml
COPY --from=enki /usr/bin/enki /usr/bin/enki
ENTRYPOINT [ "/entrypoint.sh" ]

View File

@ -0,0 +1,126 @@
package cmd
import (
"fmt"
"os/exec"
"github.com/kairos-io/enki/pkg/action"
"github.com/kairos-io/enki/pkg/config"
"github.com/kairos-io/enki/pkg/utils"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"k8s.io/mount-utils"
)
// NewBuildISO returns a new instance of the buid-iso subcommand and appends it to
// the root command. requireRoot is to initiate it with or without the CheckRoot
// pre-run check. This method is mostly used for testing purposes.
func NewBuildISO(root *cobra.Command, addCheckRoot bool) *cobra.Command {
c := &cobra.Command{
Use: "build-iso SOURCE",
Short: "Build bootable installation media ISOs",
Long: "Build bootable installation media ISOs\n\n" +
"SOURCE - should be provided as uri in following format <sourceType>:<sourceName>\n" +
" * <sourceType> - might be [\"dir\", \"file\", \"oci\", \"docker\"], as default is \"docker\"\n" +
" * <sourceName> - is path to file or directory, image name with tag version",
Args: cobra.MaximumNArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
if addCheckRoot {
return CheckRoot()
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
path, err := exec.LookPath("mount")
if err != nil {
return err
}
mounter := mount.New(path)
cfg, err := config.ReadConfigBuild(viper.GetString("config-dir"), cmd.Flags(), mounter)
if err != nil {
cfg.Logger.Errorf("Error reading config: %s\n", err)
}
flags := cmd.Flags()
// Set this after parsing of the flags, so it fails on parsing and prints usage properly
cmd.SilenceUsage = true
cmd.SilenceErrors = true // Do not propagate errors down the line, we control them
spec, err := config.ReadBuildISO(cfg, flags)
if err != nil {
cfg.Logger.Errorf("invalid install command setup %v", err)
return err
}
if len(args) == 1 {
imgSource, err := v1.NewSrcFromURI(args[0])
if err != nil {
cfg.Logger.Errorf("not a valid rootfs source image argument: %s", args[0])
return err
}
spec.RootFS = []*v1.ImageSource{imgSource}
} else if len(spec.RootFS) == 0 {
errmsg := "rootfs source image for building ISO was not provided"
cfg.Logger.Errorf(errmsg)
return fmt.Errorf(errmsg)
}
// Repos and overlays can't be unmarshaled directly as they require
// to be merged on top and flags do not match any config value key
oRootfs, _ := flags.GetString("overlay-rootfs")
oUEFI, _ := flags.GetString("overlay-uefi")
oISO, _ := flags.GetString("overlay-iso")
if oRootfs != "" {
if ok, err := utils.Exists(cfg.Fs, oRootfs); ok {
spec.RootFS = append(spec.RootFS, v1.NewDirSrc(oRootfs))
} else {
cfg.Logger.Errorf("Invalid value for overlay-rootfs")
return fmt.Errorf("Invalid path '%s': %v", oRootfs, err)
}
}
if oUEFI != "" {
if ok, err := utils.Exists(cfg.Fs, oUEFI); ok {
spec.UEFI = append(spec.UEFI, v1.NewDirSrc(oUEFI))
} else {
cfg.Logger.Errorf("Invalid value for overlay-uefi")
return fmt.Errorf("Invalid path '%s': %v", oUEFI, err)
}
}
if oISO != "" {
if ok, err := utils.Exists(cfg.Fs, oISO); ok {
spec.Image = append(spec.Image, v1.NewDirSrc(oISO))
} else {
cfg.Logger.Errorf("Invalid value for overlay-iso")
return fmt.Errorf("Invalid path '%s': %v", oISO, err)
}
}
buildISO := action.NewBuildISOAction(cfg, spec)
err = buildISO.ISORun()
if err != nil {
cfg.Logger.Errorf(err.Error())
return err
}
return nil
},
}
root.AddCommand(c)
c.Flags().StringP("name", "n", "", "Basename of the generated ISO file")
c.Flags().StringP("output", "o", "", "Output directory (defaults to current directory)")
c.Flags().Bool("date", false, "Adds a date suffix into the generated ISO file")
c.Flags().String("overlay-rootfs", "", "Path of the overlayed rootfs data")
c.Flags().String("overlay-uefi", "", "Path of the overlayed uefi data")
c.Flags().String("overlay-iso", "", "Path of the overlayed iso data")
c.Flags().String("label", "", "Label of the ISO volume")
archType := newEnumFlag([]string{"x86_64", "arm64"}, "x86_64")
c.Flags().Bool("squash-no-compression", true, "Disable squashfs compression.")
c.Flags().VarP(archType, "arch", "a", "Arch to build the image for")
return c
}
// register the subcommand into rootCmd
var _ = NewBuildISO(rootCmd, true)

View File

@ -0,0 +1,90 @@
package cmd
import (
"errors"
"fmt"
"os"
"strings"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func NewRootCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "enki",
Short: "enki",
}
cmd.PersistentFlags().Bool("debug", false, "Enable debug output")
cmd.PersistentFlags().String("config-dir", "/etc/elemental", "Set config dir (default is /etc/elemental)")
cmd.PersistentFlags().String("logfile", "", "Set logfile")
cmd.PersistentFlags().Bool("quiet", false, "Do not output to stdout")
_ = viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug"))
_ = viper.BindPFlag("config-dir", cmd.PersistentFlags().Lookup("config-dir"))
_ = viper.BindPFlag("logfile", cmd.PersistentFlags().Lookup("logfile"))
_ = viper.BindPFlag("quiet", cmd.PersistentFlags().Lookup("quiet"))
if viper.GetBool("debug") {
logrus.SetLevel(logrus.DebugLevel)
}
return cmd
}
// rootCmd represents the base command when called without any subcommands
var rootCmd = NewRootCmd()
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
// CheckRoot is a helper to return on PreRunE, so we can add it to commands that require root
func CheckRoot() error {
if os.Geteuid() != 0 {
return errors.New("this command requires root privileges")
}
return nil
}
type enum struct {
Allowed []string
Value string
}
func (a enum) String() string {
return a.Value
}
func (a *enum) Set(p string) error {
isIncluded := func(opts []string, val string) bool {
for _, opt := range opts {
if val == opt {
return true
}
}
return false
}
if !isIncluded(a.Allowed, p) {
return fmt.Errorf("%s is not included in %s", p, strings.Join(a.Allowed, ","))
}
a.Value = p
return nil
}
func (a *enum) Type() string {
return "string"
}
// newEnum give a list of allowed flag parameters, where the second argument is the default
func newEnumFlag(allowed []string, d string) *enum {
return &enum{
Allowed: allowed,
Value: d,
}
}

149
tools-image/enki/go.mod Normal file
View File

@ -0,0 +1,149 @@
module github.com/kairos-io/enki
go 1.20
require (
github.com/hashicorp/go-multierror v1.1.1
github.com/kairos-io/kairos-agent/v2 v2.1.11-0.20230713071318-9a16b94e2af6
github.com/mitchellh/mapstructure v1.5.0
github.com/sanity-io/litter v1.5.5
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0
github.com/twpayne/go-vfs v1.7.2
k8s.io/mount-utils v0.27.3
)
require (
atomicgo.dev/cursor v0.1.1 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.0.2 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.10.0-rc.8 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230117203413-a47887b8f098 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect
github.com/cavaliergopher/grab v2.0.0+incompatible // indirect
github.com/cavaliergopher/grab/v3 v3.0.1 // indirect
github.com/cloudflare/circl v1.3.1 // indirect
github.com/containerd/cgroups v1.1.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/containerd/containerd v1.7.1 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/denisbrodbeck/machineid v1.0.1 // indirect
github.com/diskfs/go-diskfs v1.3.0 // indirect
github.com/distribution/distribution v2.8.2+incompatible // indirect
github.com/docker/cli v23.0.5+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v23.0.6+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/go-git/go-git/v5 v5.4.2 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-containerregistry v0.15.2 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gookit/color v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/itchyny/gojq v0.12.12 // indirect
github.com/itchyny/timefmt-go v0.1.5 // indirect
github.com/jaypipes/ghw v0.10.0 // indirect
github.com/jaypipes/pcidb v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/kairos-io/kairos-sdk v0.0.9-0.20230620064343-df990bf49a07 // indirect
github.com/kendru/darwin/go/depgraph v0.0.0-20221105232959-877d6a81060c // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/moby v23.0.4+incompatible // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/mudler/entities v0.0.0-20220905203055-68348bae0f49 // indirect
github.com/mudler/yip v1.3.1-0.20230704124832-e5812d0f5890 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
github.com/packethost/packngo v0.29.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/xattr v0.4.9 // indirect
github.com/pterm/pterm v0.12.62 // indirect
github.com/qeesung/image2ascii v1.0.1 // indirect
github.com/rancher-sandbox/linuxkit v1.0.1-0.20230517173613-432a87ba3e09 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/samber/lo v1.37.0 // indirect
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spectrocloud-labs/herd v0.4.2 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/tredoe/osutil/v2 v2.0.0-rc.16 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/vbatts/tar-split v0.11.3 // indirect
github.com/vishvananda/netlink v1.2.1-beta.2 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/vmware/vmw-guestinfo v0.0.0-20220317130741-510905f0efa3 // indirect
github.com/wayneashleyberry/terminal-dimensions v1.1.0 // indirect
github.com/willdonnelly/passwd v0.0.0-20141013001024-7935dab3074c // indirect
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zcalusic/sysinfo v0.9.5 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/term v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.9.1 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/djherbis/times.v1 v1.3.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
pault.ag/go/modprobe v0.1.2 // indirect
pault.ag/go/topsort v0.1.1 // indirect
)

1002
tools-image/enki/go.sum Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
package version
import (
"runtime"
)
var (
version = "v0.0.1"
// gitCommit is the git sha1
gitCommit = ""
)
// BuildInfo describes the compile time information.
type BuildInfo struct {
// Version is the current semver.
Version string `json:"version,omitempty"`
// GitCommit is the git sha1.
GitCommit string `json:"git_commit,omitempty"`
// GoVersion is the version of the Go compiler used.
GoVersion string `json:"go_version,omitempty"`
}
func GetVersion() string {
return version
}
// Get returns build info
func Get() BuildInfo {
v := BuildInfo{
Version: GetVersion(),
GitCommit: gitCommit,
GoVersion: runtime.Version(),
}
return v
}

7
tools-image/enki/main.go Normal file
View File

@ -0,0 +1,7 @@
package main
import "github.com/kairos-io/enki/cmd"
func main() {
cmd.Execute()
}

View File

@ -0,0 +1,373 @@
package action
import (
"fmt"
"path/filepath"
"strings"
"time"
"github.com/kairos-io/enki/pkg/constants"
"github.com/kairos-io/enki/pkg/utils"
"github.com/kairos-io/kairos-agent/v2/pkg/elemental"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
)
type BuildISOAction struct {
cfg *v1.BuildConfig
spec *v1.LiveISO
e *elemental.Elemental
}
type BuildISOActionOption func(a *BuildISOAction)
func NewBuildISOAction(cfg *v1.BuildConfig, spec *v1.LiveISO, opts ...BuildISOActionOption) *BuildISOAction {
b := &BuildISOAction{
cfg: cfg,
e: elemental.NewElemental(&cfg.Config),
spec: spec,
}
for _, opt := range opts {
opt(b)
}
return b
}
// ISORun will install the system from a given configuration
func (b *BuildISOAction) ISORun() (err error) {
cleanup := utils.NewCleanStack()
defer func() { err = cleanup.Cleanup(err) }()
isoTmpDir, err := utils.TempDir(b.cfg.Fs, "", "elemental-iso")
if err != nil {
return err
}
cleanup.Push(func() error { return b.cfg.Fs.RemoveAll(isoTmpDir) })
rootDir := filepath.Join(isoTmpDir, "rootfs")
err = utils.MkdirAll(b.cfg.Fs, rootDir, constants.DirPerm)
if err != nil {
return err
}
uefiDir := filepath.Join(isoTmpDir, "uefi")
err = utils.MkdirAll(b.cfg.Fs, uefiDir, constants.DirPerm)
if err != nil {
return err
}
isoDir := filepath.Join(isoTmpDir, "iso")
err = utils.MkdirAll(b.cfg.Fs, isoDir, constants.DirPerm)
if err != nil {
return err
}
if b.cfg.OutDir != "" {
err = utils.MkdirAll(b.cfg.Fs, b.cfg.OutDir, constants.DirPerm)
if err != nil {
b.cfg.Logger.Errorf("Failed creating output folder: %s", b.cfg.OutDir)
return err
}
}
b.cfg.Logger.Infof("Preparing squashfs root...")
err = b.applySources(rootDir, b.spec.RootFS...)
if err != nil {
b.cfg.Logger.Errorf("Failed installing OS packages: %v", err)
return err
}
err = utils.CreateDirStructure(b.cfg.Fs, rootDir)
if err != nil {
b.cfg.Logger.Errorf("Failed creating root directory structure: %v", err)
return err
}
b.cfg.Logger.Infof("Preparing EFI image...")
if b.spec.BootloaderInRootFs {
err = b.PrepareEFI(rootDir, uefiDir)
if err != nil {
b.cfg.Logger.Errorf("Failed fetching EFI data: %v", err)
return err
}
}
err = b.applySources(uefiDir, b.spec.UEFI...)
if err != nil {
b.cfg.Logger.Errorf("Failed installing EFI packages: %v", err)
return err
}
b.cfg.Logger.Infof("Preparing ISO image root tree...")
if b.spec.BootloaderInRootFs {
err = b.PrepareISO(rootDir, isoDir)
if err != nil {
b.cfg.Logger.Errorf("Failed fetching bootloader binaries: %v", err)
return err
}
}
err = b.applySources(isoDir, b.spec.Image...)
if err != nil {
b.cfg.Logger.Errorf("Failed installing ISO image packages: %v", err)
return err
}
err = b.prepareISORoot(isoDir, rootDir, uefiDir)
if err != nil {
b.cfg.Logger.Errorf("Failed preparing ISO's root tree: %v", err)
return err
}
b.cfg.Logger.Infof("Creating ISO image...")
err = b.burnISO(isoDir)
if err != nil {
b.cfg.Logger.Errorf("Failed preparing ISO's root tree: %v", err)
return err
}
return err
}
func (b BuildISOAction) prepareISORoot(isoDir string, rootDir string, uefiDir string) error {
kernel, initrd, err := b.e.FindKernelInitrd(rootDir)
if err != nil {
b.cfg.Logger.Error("Could not find kernel and/or initrd")
return err
}
err = utils.MkdirAll(b.cfg.Fs, filepath.Join(isoDir, "boot"), constants.DirPerm)
if err != nil {
return err
}
//TODO document boot/kernel and boot/initrd expectation in bootloader config
b.cfg.Logger.Debugf("Copying Kernel file %s to iso root tree", kernel)
err = utils.CopyFile(b.cfg.Fs, kernel, filepath.Join(isoDir, constants.IsoKernelPath))
if err != nil {
return err
}
b.cfg.Logger.Debugf("Copying initrd file %s to iso root tree", initrd)
err = utils.CopyFile(b.cfg.Fs, initrd, filepath.Join(isoDir, constants.IsoInitrdPath))
if err != nil {
return err
}
b.cfg.Logger.Info("Creating squashfs...")
err = utils.CreateSquashFS(b.cfg.Runner, b.cfg.Logger, rootDir, filepath.Join(isoDir, constants.IsoRootFile), constants.GetDefaultSquashfsOptions())
if err != nil {
return err
}
b.cfg.Logger.Info("Creating EFI image...")
err = b.createEFI(uefiDir, filepath.Join(isoDir, constants.IsoEFIPath))
if err != nil {
return err
}
return nil
}
func (b BuildISOAction) createEFI(root string, img string) error {
efiSize, err := utils.DirSize(b.cfg.Fs, root)
if err != nil {
return err
}
// align efiSize to the next 4MB slot
align := int64(4 * 1024 * 1024)
efiSizeMB := (efiSize/align*align + align) / (1024 * 1024)
err = b.e.CreateFileSystemImage(&v1.Image{
File: img,
Size: uint(efiSizeMB),
FS: constants.EfiFs,
Label: constants.EfiLabel,
})
if err != nil {
return err
}
files, err := b.cfg.Fs.ReadDir(root)
if err != nil {
return err
}
for _, f := range files {
_, err = b.cfg.Runner.Run("mcopy", "-s", "-i", img, filepath.Join(root, f.Name()), "::")
if err != nil {
return err
}
}
return nil
}
func (b BuildISOAction) burnISO(root string) error {
cmd := "xorriso"
var outputFile string
var isoFileName string
if b.cfg.Date {
currTime := time.Now()
isoFileName = fmt.Sprintf("%s.%s.iso", b.cfg.Name, currTime.Format("20060102"))
} else {
isoFileName = fmt.Sprintf("%s.iso", b.cfg.Name)
}
outputFile = isoFileName
if b.cfg.OutDir != "" {
outputFile = filepath.Join(b.cfg.OutDir, outputFile)
}
if exists, _ := utils.Exists(b.cfg.Fs, outputFile); exists {
b.cfg.Logger.Warnf("Overwriting already existing %s", outputFile)
err := b.cfg.Fs.Remove(outputFile)
if err != nil {
return err
}
}
args := []string{
"-volid", b.spec.Label, "-joliet", "on", "-padding", "0",
"-outdev", outputFile, "-map", root, "/", "-chmod", "0755", "--",
}
args = append(args, constants.GetXorrisoBooloaderArgs(root)...)
out, err := b.cfg.Runner.Run(cmd, args...)
b.cfg.Logger.Debugf("Xorriso: %s", string(out))
if err != nil {
return err
}
checksum, err := utils.CalcFileChecksum(b.cfg.Fs, outputFile)
if err != nil {
return fmt.Errorf("checksum computation failed: %w", err)
}
err = b.cfg.Fs.WriteFile(fmt.Sprintf("%s.sha256", outputFile), []byte(fmt.Sprintf("%s %s\n", checksum, isoFileName)), 0644)
if err != nil {
return fmt.Errorf("cannot write checksum file: %w", err)
}
return nil
}
func (b BuildISOAction) applySources(target string, sources ...*v1.ImageSource) error {
for _, src := range sources {
_, err := b.e.DumpSource(target, src)
if err != nil {
return err
}
}
return nil
}
func (g *BuildISOAction) PrepareEFI(rootDir, uefiDir string) error {
err := utils.MkdirAll(g.cfg.Fs, filepath.Join(uefiDir, constants.EfiBootPath), constants.DirPerm)
if err != nil {
return err
}
switch g.cfg.Arch {
case constants.ArchAmd64, constants.Archx86:
err = utils.CopyFile(
g.cfg.Fs,
filepath.Join(rootDir, constants.GrubEfiImagex86),
filepath.Join(uefiDir, constants.GrubEfiImagex86Dest),
)
case constants.ArchArm64:
err = utils.CopyFile(
g.cfg.Fs,
filepath.Join(rootDir, constants.GrubEfiImageArm64),
filepath.Join(uefiDir, constants.GrubEfiImageArm64Dest),
)
default:
err = fmt.Errorf("Not supported architecture: %v", g.cfg.Arch)
}
if err != nil {
return err
}
return g.cfg.Fs.WriteFile(filepath.Join(uefiDir, constants.EfiBootPath, constants.GrubCfg), []byte(constants.GrubEfiCfg), constants.FilePerm)
}
func (g *BuildISOAction) PrepareISO(rootDir, imageDir string) error {
err := utils.MkdirAll(g.cfg.Fs, filepath.Join(imageDir, constants.GrubPrefixDir), constants.DirPerm)
if err != nil {
return err
}
switch g.cfg.Arch {
case constants.ArchAmd64, constants.Archx86:
// Create eltorito image
eltorito, err := g.BuildEltoritoImg(rootDir)
if err != nil {
return err
}
// Inlude loaders in expected paths
loaderDir := filepath.Join(imageDir, constants.IsoLoaderPath)
err = utils.MkdirAll(g.cfg.Fs, loaderDir, constants.DirPerm)
if err != nil {
return err
}
loaderFiles := []string{eltorito, constants.GrubBootHybridImg}
loaderFiles = append(loaderFiles, strings.Split(constants.SyslinuxFiles, " ")...)
for _, f := range loaderFiles {
err = utils.CopyFile(g.cfg.Fs, filepath.Join(rootDir, f), loaderDir)
if err != nil {
return err
}
}
fontsDir := filepath.Join(loaderDir, "/grub2/fonts")
err = utils.MkdirAll(g.cfg.Fs, fontsDir, constants.DirPerm)
if err != nil {
return err
}
err = utils.CopyFile(g.cfg.Fs, filepath.Join(rootDir, constants.GrubFont), fontsDir)
if err != nil {
return err
}
case constants.ArchArm64:
// TBC
default:
return fmt.Errorf("Not supported architecture: %v", g.cfg.Arch)
}
// Write grub.cfg file
err = g.cfg.Fs.WriteFile(
filepath.Join(imageDir, constants.GrubPrefixDir, constants.GrubCfg),
[]byte(fmt.Sprintf(constants.GrubCfgTemplate, g.spec.GrubEntry, g.spec.Label)),
constants.FilePerm,
)
if err != nil {
return err
}
// Include EFI contents in iso root too
return g.PrepareEFI(rootDir, imageDir)
}
func (g *BuildISOAction) BuildEltoritoImg(rootDir string) (string, error) {
var args []string
args = append(args, "-O", constants.GrubBiosTarget)
args = append(args, "-o", constants.GrubBiosImg)
args = append(args, "-p", constants.GrubPrefixDir)
args = append(args, "-d", constants.GrubI386BinDir)
args = append(args, strings.Split(constants.GrubModules, " ")...)
chRoot := utils.NewChroot(rootDir, &g.cfg.Config)
out, err := chRoot.Run("grub2-mkimage", args...)
if err != nil {
g.cfg.Logger.Errorf("grub2-mkimage failed: %s", string(out))
g.cfg.Logger.Errorf("Error: %v", err)
return "", err
}
concatFiles := func() error {
return utils.ConcatFiles(
g.cfg.Fs, []string{constants.GrubBiosCDBoot, constants.GrubBiosImg},
constants.GrubEltoritoImg,
)
}
err = chRoot.RunCallback(concatFiles)
if err != nil {
return "", err
}
return constants.GrubEltoritoImg, nil
}

View File

@ -0,0 +1,295 @@
package config
import (
"fmt"
"io"
"io/fs"
"os"
"reflect"
"runtime"
"strings"
"github.com/kairos-io/enki/internal/version"
"github.com/kairos-io/enki/pkg/constants"
"github.com/kairos-io/enki/pkg/utils"
"github.com/kairos-io/kairos-agent/v2/pkg/cloudinit"
"github.com/kairos-io/kairos-agent/v2/pkg/http"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
"github.com/mitchellh/mapstructure"
"github.com/sanity-io/litter"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/twpayne/go-vfs"
"k8s.io/mount-utils"
)
var decodeHook = viper.DecodeHook(
mapstructure.ComposeDecodeHookFunc(
UnmarshalerHook(),
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
),
)
func WithLogger(logger v1.Logger) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Logger = logger
return nil
}
}
func WithMounter(mounter mount.Interface) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Mounter = mounter
return nil
}
}
type GenericOptions func(a *v1.Config) error
func ReadConfigBuild(configDir string, flags *pflag.FlagSet, mounter mount.Interface) (*v1.BuildConfig, error) {
logger := v1.NewLogger()
if configDir == "" {
configDir = "."
}
cfg := NewBuildConfig(
WithLogger(logger),
WithMounter(mounter),
)
configLogger(cfg.Logger, cfg.Fs)
viper.AddConfigPath(configDir)
viper.SetConfigType("yaml")
viper.SetConfigName("manifest.yaml")
// If a config file is found, read it in.
_ = viper.MergeInConfig()
// Bind buildconfig flags
bindGivenFlags(viper.GetViper(), flags)
// merge environment variables on top for rootCmd
viperReadEnv(viper.GetViper(), "BUILD", constants.GetBuildKeyEnvMap())
// unmarshal all the vars into the config object
err := viper.Unmarshal(cfg, setDecoder, decodeHook)
if err != nil {
cfg.Logger.Warnf("error unmarshalling config: %s", err)
}
err = cfg.Sanitize()
cfg.Logger.Debugf("Full config loaded: %s", litter.Sdump(cfg))
return cfg, err
}
func ReadBuildISO(b *v1.BuildConfig, flags *pflag.FlagSet) (*v1.LiveISO, error) {
iso := NewISO()
vp := viper.Sub("iso")
if vp == nil {
vp = viper.New()
}
// Bind build-iso cmd flags
bindGivenFlags(vp, flags)
// Bind build-iso env vars
viperReadEnv(vp, "ISO", constants.GetISOKeyEnvMap())
err := vp.Unmarshal(iso, setDecoder, decodeHook)
if err != nil {
b.Logger.Warnf("error unmarshalling LiveISO: %s", err)
}
err = iso.Sanitize()
b.Logger.Debugf("Loaded LiveISO: %s", litter.Sdump(iso))
return iso, err
}
func NewISO() *v1.LiveISO {
return &v1.LiveISO{
Label: constants.ISOLabel,
GrubEntry: constants.GrubDefEntry,
UEFI: []*v1.ImageSource{},
Image: []*v1.ImageSource{},
}
}
func NewBuildConfig(opts ...GenericOptions) *v1.BuildConfig {
b := &v1.BuildConfig{
Config: *NewConfig(opts...),
Name: constants.BuildImgName,
}
if len(b.Repos) == 0 {
repo := constants.LuetDefaultRepoURI
if b.Arch != constants.Archx86 {
repo = fmt.Sprintf("%s-%s", constants.LuetDefaultRepoURI, b.Arch)
}
b.Repos = []v1.Repository{{
Name: "cos",
Type: "docker",
URI: repo,
Arch: b.Arch,
Priority: constants.LuetDefaultRepoPrio,
}}
}
return b
}
func NewConfig(opts ...GenericOptions) *v1.Config {
log := v1.NewLogger()
arch, err := utils.GolangArchToArch(runtime.GOARCH)
if err != nil {
log.Errorf("invalid arch: %s", err.Error())
return nil
}
c := &v1.Config{
Fs: vfs.OSFS,
Logger: log,
Syscall: &v1.RealSyscall{},
Client: http.NewClient(),
Repos: []v1.Repository{},
Arch: arch,
SquashFsNoCompression: true,
}
for _, o := range opts {
err := o(c)
if err != nil {
log.Errorf("error applying config option: %s", err.Error())
return nil
}
}
// delay runner creation after we have run over the options in case we use WithRunner
if c.Runner == nil {
c.Runner = &v1.RealRunner{Logger: c.Logger}
}
// Now check if the runner has a logger inside, otherwise point our logger into it
// This can happen if we set the WithRunner option as that doesn't set a logger
if c.Runner.GetLogger() == nil {
c.Runner.SetLogger(c.Logger)
}
// Delay the yip runner creation, so we set the proper logger instead of blindly setting it to the logger we create
// at the start of NewRunConfig, as WithLogger can be passed on init, and that would result in 2 different logger
// instances, on the config.Logger and the other on config.CloudInitRunner
if c.CloudInitRunner == nil {
c.CloudInitRunner = cloudinit.NewYipCloudInitRunner(c.Logger, c.Runner, vfs.OSFS)
}
if c.Mounter == nil {
c.Mounter = mount.New(constants.MountBinary)
}
return c
}
func configLogger(log v1.Logger, vfs v1.FS) {
// Set debug level
if viper.GetBool("debug") {
log.SetLevel(v1.DebugLevel())
}
// Set formatter so both file and stdout format are equal
log.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
DisableColors: false,
DisableTimestamp: false,
FullTimestamp: true,
})
// Logfile
logfile := viper.GetString("logfile")
if logfile != "" {
o, err := vfs.OpenFile(logfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, fs.ModePerm)
if err != nil {
log.Errorf("Could not open %s for logging to file: %s", logfile, err.Error())
}
// else set it to both stdout and the file
mw := io.MultiWriter(os.Stdout, o)
log.SetOutput(mw)
} else { // no logfile
if viper.GetBool("quiet") { // quiet is enabled so discard all logging
log.SetOutput(io.Discard)
} else { // default to stdout
log.SetOutput(os.Stdout)
}
}
v := version.Get()
if log.GetLevel() == logrus.DebugLevel {
log.Debugf("Starting enki version %s on commit %s", v.Version, v.GitCommit)
} else {
log.Infof("Starting enki version %s", v.Version)
}
}
// BindGivenFlags binds to viper only passed flags, ignoring any non provided flag
func bindGivenFlags(vp *viper.Viper, flagSet *pflag.FlagSet) {
if flagSet != nil {
flagSet.VisitAll(func(f *pflag.Flag) {
if f.Changed {
_ = vp.BindPFlag(f.Name, f)
}
})
}
}
// setDecoder sets ZeroFields mastructure attribute to true
func setDecoder(config *mapstructure.DecoderConfig) {
// Make sure we zero fields before applying them, this is relevant for slices
// so we do not merge with any already present value and directly apply whatever
// we got form configs.
config.ZeroFields = true
}
type Unmarshaler interface {
CustomUnmarshal(interface{}) (bool, error)
}
func UnmarshalerHook() mapstructure.DecodeHookFunc {
return func(from reflect.Value, to reflect.Value) (interface{}, error) {
// get the destination object address if it is not passed by reference
if to.CanAddr() {
to = to.Addr()
}
// If the destination implements the unmarshaling interface
u, ok := to.Interface().(Unmarshaler)
if !ok {
return from.Interface(), nil
}
// If it is nil and a pointer, create and assign the target value first
if to.IsNil() && to.Type().Kind() == reflect.Ptr {
to.Set(reflect.New(to.Type().Elem()))
u = to.Interface().(Unmarshaler)
}
// Call the custom unmarshaling method
cont, err := u.CustomUnmarshal(from.Interface())
if cont {
// Continue with the decoding stack
return from.Interface(), err
}
// Decoding finalized
return to.Interface(), err
}
}
func viperReadEnv(vp *viper.Viper, prefix string, keyMap map[string]string) {
// If we expect to override complex keys in the config, i.e. configs
// that are nested, we probably need to manually do the env stuff
// ourselves, as this will only match keys in the config root
replacer := strings.NewReplacer("-", "_")
vp.SetEnvKeyReplacer(replacer)
if prefix == "" {
prefix = "ELEMENTAL"
} else {
prefix = fmt.Sprintf("ELEMENTAL_%s", prefix)
}
// Manually bind keys to env variable if custom names are needed.
for k, v := range keyMap {
_ = vp.BindEnv(k, fmt.Sprintf("%s_%s", prefix, v))
}
}

View File

@ -0,0 +1,139 @@
package constants
import (
"fmt"
"os"
"path/filepath"
)
const (
GrubDefEntry = "Kairos"
EfiLabel = "COS_GRUB"
ISOLabel = "COS_LIVE"
MountBinary = "/usr/bin/mount"
EfiFs = "vfat"
IsoRootFile = "rootfs.squashfs"
IsoEFIPath = "/boot/uefi.img"
LuetDefaultRepoURI = "quay.io/costoolkit/releases-green"
LuetDefaultRepoPrio = 90
BuildImgName = "elemental"
EfiBootPath = "/EFI/BOOT"
GrubEfiImagex86 = "/usr/share/grub2/x86_64-efi/grub.efi"
GrubEfiImageArm64 = "/usr/share/grub2/arm64-efi/grub.efi"
GrubEfiImagex86Dest = EfiBootPath + "/bootx64.efi"
GrubEfiImageArm64Dest = EfiBootPath + "/bootaa64.efi"
GrubCfg = "grub.cfg"
GrubPrefixDir = "/boot/grub2"
GrubEfiCfg = "search --no-floppy --file --set=root " + IsoKernelPath +
"\nset prefix=($root)" + GrubPrefixDir +
"\nconfigfile $prefix/" + GrubCfg
GrubFont = "/usr/share/grub2/unicode.pf2"
GrubBootHybridImg = "/usr/share/grub2/i386-pc/boot_hybrid.img"
SyslinuxFiles = "/usr/share/syslinux/isolinux.bin " +
"/usr/share/syslinux/menu.c32 " +
"/usr/share/syslinux/chain.c32 " +
"/usr/share/syslinux/mboot.c32"
IsoLoaderPath = "/boot/x86_64/loader"
GrubCfgTemplate = `search --no-floppy --file --set=root /boot/kernel
set default=0
set timeout=10
set timeout_style=menu
set linux=linux
set initrd=initrd
if [ "${grub_cpu}" = "x86_64" -o "${grub_cpu}" = "i386" -o "${grub_cpu}" = "arm64" ];then
if [ "${grub_platform}" = "efi" ]; then
if [ "${grub_cpu}" != "arm64" ]; then
set linux=linuxefi
set initrd=initrdefi
fi
fi
fi
if [ "${grub_platform}" = "efi" ]; then
echo "Please press 't' to show the boot menu on this console"
fi
set font=($root)/boot/${grub_cpu}/loader/grub2/fonts/unicode.pf2
if [ -f ${font} ];then
loadfont ${font}
fi
menuentry "%s" --class os --unrestricted {
echo Loading kernel...
$linux ($root)/boot/kernel cdroot root=live:CDLABEL=%s rd.live.dir=/ rd.live.squashimg=rootfs.squashfs console=tty1 console=ttyS0 rd.cos.disable
echo Loading initrd...
$initrd ($root)/boot/initrd
}
if [ "${grub_platform}" = "efi" ]; then
hiddenentry "Text mode" --hotkey "t" {
set textmode=true
terminal_output console
}
fi`
GrubBiosTarget = "i386-pc"
GrubI386BinDir = "/usr/share/grub2/i386-pc"
GrubBiosImg = GrubI386BinDir + "/core.img"
GrubBiosCDBoot = GrubI386BinDir + "/cdboot.img"
GrubEltoritoImg = GrubI386BinDir + "/eltorito.img"
//TODO this list could be optimized
GrubModules = "ext2 iso9660 linux echo configfile search_label search_fs_file search search_fs_uuid " +
"ls normal gzio png fat gettext font minicmd gfxterm gfxmenu all_video xfs btrfs lvm luks " +
"gcry_rijndael gcry_sha256 gcry_sha512 crypto cryptodisk test true loadenv part_gpt " +
"part_msdos biosdisk vga vbe chain boot"
IsoHybridMBR = "/boot/x86_64/loader/boot_hybrid.img"
IsoBootCatalog = "/boot/x86_64/boot.catalog"
IsoBootFile = "/boot/x86_64/loader/eltorito.img"
// These paths are arbitrary but coupled to grub.cfg
IsoKernelPath = "/boot/kernel"
IsoInitrdPath = "/boot/initrd"
// Default directory and file fileModes
DirPerm = os.ModeDir | os.ModePerm
FilePerm = 0666
NoWriteDirPerm = 0555 | os.ModeDir
TempDirPerm = os.ModePerm | os.ModeSticky | os.ModeDir
ArchAmd64 = "amd64"
Archx86 = "x86_64"
ArchArm64 = "arm64"
)
// GetDefaultSquashfsOptions returns the default options to use when creating a squashfs
func GetDefaultSquashfsOptions() []string {
return []string{"-b", "1024k"}
}
func GetXorrisoBooloaderArgs(root string) []string {
args := []string{
"-boot_image", "grub", fmt.Sprintf("bin_path=%s", IsoBootFile),
"-boot_image", "grub", fmt.Sprintf("grub2_mbr=%s/%s", root, IsoHybridMBR),
"-boot_image", "grub", "grub2_boot_info=on",
"-boot_image", "any", "partition_offset=16",
"-boot_image", "any", fmt.Sprintf("cat_path=%s", IsoBootCatalog),
"-boot_image", "any", "cat_hidden=on",
"-boot_image", "any", "boot_info_table=on",
"-boot_image", "any", "platform_id=0x00",
"-boot_image", "any", "emul_type=no_emulation",
"-boot_image", "any", "load_size=2048",
"-append_partition", "2", "0xef", filepath.Join(root, IsoEFIPath),
"-boot_image", "any", "next",
"-boot_image", "any", "efi_path=--interval:appended_partition_2:all::",
"-boot_image", "any", "platform_id=0xef",
"-boot_image", "any", "emul_type=no_emulation",
}
return args
}
// GetBuildKeyEnvMap returns environment variable bindings to BuildConfig data
func GetBuildKeyEnvMap() map[string]string {
return map[string]string{
"name": "NAME",
}
}
// GetISOKeyEnvMap returns environment variable bindings to LiveISO data
func GetISOKeyEnvMap() map[string]string {
// None for the time being
return map[string]string{}
}

View File

@ -0,0 +1,218 @@
/*
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 utils
import (
"errors"
"fmt"
"os"
"sort"
"strings"
"github.com/kairos-io/enki/pkg/constants"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
)
// Chroot represents the struct that will allow us to run commands inside a given chroot
type Chroot struct {
path string
defaultMounts []string
extraMounts map[string]string
activeMounts []string
config *v1.Config
}
func NewChroot(path string, config *v1.Config) *Chroot {
return &Chroot{
path: path,
defaultMounts: []string{"/dev", "/dev/pts", "/proc", "/sys"},
extraMounts: map[string]string{},
activeMounts: []string{},
config: config,
}
}
// ChrootedCallback runs the given callback in a chroot environment
func ChrootedCallback(cfg *v1.Config, path string, bindMounts map[string]string, callback func() error) error {
chroot := NewChroot(path, cfg)
chroot.SetExtraMounts(bindMounts)
return chroot.RunCallback(callback)
}
// Sets additional bind mounts for the chroot enviornment. They are represented
// in a map where the key is the path outside the chroot and the value is the
// path inside the chroot.
func (c *Chroot) SetExtraMounts(extraMounts map[string]string) {
c.extraMounts = extraMounts
}
// Prepare will mount the defaultMounts as bind mounts, to be ready when we run chroot
func (c *Chroot) Prepare() error {
var err error
keys := []string{}
mountOptions := []string{"bind"}
if len(c.activeMounts) > 0 {
return errors.New("There are already active mountpoints for this instance")
}
defer func() {
if err != nil {
c.Close()
}
}()
for _, mnt := range c.defaultMounts {
mountPoint := fmt.Sprintf("%s%s", strings.TrimSuffix(c.path, "/"), mnt)
err = MkdirAll(c.config.Fs, mountPoint, constants.DirPerm)
if err != nil {
return err
}
err = c.config.Mounter.Mount(mnt, mountPoint, "bind", mountOptions)
if err != nil {
return err
}
c.activeMounts = append(c.activeMounts, mountPoint)
}
for k := range c.extraMounts {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
mountPoint := fmt.Sprintf("%s%s", strings.TrimSuffix(c.path, "/"), c.extraMounts[k])
err = MkdirAll(c.config.Fs, mountPoint, constants.DirPerm)
if err != nil {
return err
}
err = c.config.Mounter.Mount(k, mountPoint, "bind", mountOptions)
if err != nil {
return err
}
c.activeMounts = append(c.activeMounts, mountPoint)
}
return nil
}
// Close will unmount all active mounts created in Prepare on reverse order
func (c *Chroot) Close() error {
failures := []string{}
for len(c.activeMounts) > 0 {
curr := c.activeMounts[len(c.activeMounts)-1]
c.config.Logger.Debugf("Unmounting %s from chroot", curr)
c.activeMounts = c.activeMounts[:len(c.activeMounts)-1]
err := c.config.Mounter.Unmount(curr)
if err != nil {
c.config.Logger.Errorf("Error unmounting %s: %s", curr, err)
failures = append(failures, curr)
}
}
if len(failures) > 0 {
c.activeMounts = failures
return fmt.Errorf("failed closing chroot environment. Unmount failures: %v", failures)
}
return nil
}
// RunCallback runs the given callback in a chroot environment
func (c *Chroot) RunCallback(callback func() error) (err error) {
var currentPath string
var oldRootF *os.File
// Store current path
currentPath, err = os.Getwd()
if err != nil {
c.config.Logger.Error("Failed to get current path")
return err
}
defer func() {
tmpErr := os.Chdir(currentPath)
if err == nil && tmpErr != nil {
err = tmpErr
}
}()
// Store current root
oldRootF, err = c.config.Fs.Open("/")
if err != nil {
c.config.Logger.Errorf("Can't open current root")
return err
}
defer oldRootF.Close()
if len(c.activeMounts) == 0 {
err = c.Prepare()
if err != nil {
c.config.Logger.Errorf("Can't mount default mounts")
return err
}
defer func() {
tmpErr := c.Close()
if err == nil {
err = tmpErr
}
}()
}
// Change to new dir before running chroot!
err = c.config.Syscall.Chdir(c.path)
if err != nil {
c.config.Logger.Errorf("Can't chdir %s: %s", c.path, err)
return err
}
err = c.config.Syscall.Chroot(c.path)
if err != nil {
c.config.Logger.Errorf("Can't chroot %s: %s", c.path, err)
return err
}
// Restore to old root
defer func() {
tmpErr := oldRootF.Chdir()
if tmpErr != nil {
c.config.Logger.Errorf("Can't change to old root dir")
if err == nil {
err = tmpErr
}
} else {
tmpErr = c.config.Syscall.Chroot(".")
if tmpErr != nil {
c.config.Logger.Errorf("Can't chroot back to old root")
if err == nil {
err = tmpErr
}
}
}
}()
return callback()
}
// Run executes a command inside a chroot
func (c *Chroot) Run(command string, args ...string) (out []byte, err error) {
callback := func() error {
out, err = c.config.Runner.Run(command, args...)
return err
}
err = c.RunCallback(callback)
if err != nil {
c.config.Logger.Errorf("Cant run command %s with args %v on chroot: %s", command, args, err)
c.config.Logger.Debugf("Output from command: %s", out)
}
return out, err
}

View File

@ -0,0 +1,50 @@
package utils
import (
"github.com/hashicorp/go-multierror"
)
type CleanJob func() error
// NewCleanStack returns a new stack.
func NewCleanStack() *CleanStack {
return &CleanStack{}
}
// Stack is a basic LIFO stack that resizes as needed.
type CleanStack struct {
jobs []CleanJob
count int
}
// Push adds a node to the stack
func (clean *CleanStack) Push(job CleanJob) {
clean.jobs = append(clean.jobs[:clean.count], job)
clean.count++
}
// Pop removes and returns a node from the stack in last to first order.
func (clean *CleanStack) Pop() CleanJob {
if clean.count == 0 {
return nil
}
clean.count--
return clean.jobs[clean.count]
}
// Cleanup runs the whole cleanup stack. In case of error it runs all jobs
// and returns the first error occurrence.
func (clean *CleanStack) Cleanup(err error) error {
var errs error
if err != nil {
errs = multierror.Append(errs, err)
}
for clean.count > 0 {
job := clean.Pop()
err = job()
if err != nil {
errs = multierror.Append(errs, err)
}
}
return errs
}

View File

@ -0,0 +1,40 @@
package utils
import (
"fmt"
"github.com/kairos-io/enki/pkg/constants"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
"strings"
)
// CreateSquashFS creates a squash file at destination from a source, with options
// TODO: Check validity of source maybe?
func CreateSquashFS(runner v1.Runner, logger v1.Logger, source string, destination string, options []string) error {
// create args
args := []string{source, destination}
// append options passed to args in order to have the correct order
// protect against options passed together in the same string , i.e. "-x add" instead of "-x", "add"
var optionsExpanded []string
for _, op := range options {
optionsExpanded = append(optionsExpanded, strings.Split(op, " ")...)
}
args = append(args, optionsExpanded...)
out, err := runner.Run("mksquashfs", args...)
if err != nil {
logger.Debugf("Error running squashfs creation, stdout: %s", out)
logger.Errorf("Error while creating squashfs from %s to %s: %s", source, destination, err)
return err
}
return nil
}
func GolangArchToArch(arch string) (string, error) {
switch strings.ToLower(arch) {
case constants.ArchAmd64:
return constants.Archx86, nil
case constants.ArchArm64:
return constants.ArchArm64, nil
default:
return "", fmt.Errorf("invalid arch")
}
}

View File

@ -0,0 +1,223 @@
package utils
import (
"crypto/sha256"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"sync"
"syscall"
"time"
"github.com/kairos-io/enki/pkg/constants"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
"github.com/twpayne/go-vfs"
"github.com/twpayne/go-vfs/vfst"
)
// MkdirAll directory and all parents if not existing
func MkdirAll(fs v1.FS, name string, mode os.FileMode) (err error) {
if _, isReadOnly := fs.(*vfs.ReadOnlyFS); isReadOnly {
return permError("mkdir", name)
}
if name, err = fs.RawPath(name); err != nil {
return &os.PathError{Op: "mkdir", Path: name, Err: err}
}
return os.MkdirAll(name, mode)
}
// permError returns an *os.PathError with Err syscall.EPERM.
func permError(op, path string) error {
return &os.PathError{
Op: op,
Path: path,
Err: syscall.EPERM,
}
}
// Copies source file to target file using Fs interface
func CreateDirStructure(fs v1.FS, target string) error {
for _, dir := range []string{"/run", "/dev", "/boot", "/usr/local", "/oem"} {
err := MkdirAll(fs, filepath.Join(target, dir), constants.DirPerm)
if err != nil {
return err
}
}
for _, dir := range []string{"/proc", "/sys"} {
err := MkdirAll(fs, filepath.Join(target, dir), constants.NoWriteDirPerm)
if err != nil {
return err
}
}
err := MkdirAll(fs, filepath.Join(target, "/tmp"), constants.DirPerm)
if err != nil {
return err
}
// Set /tmp permissions regardless the umask setup
err = fs.Chmod(filepath.Join(target, "/tmp"), constants.TempDirPerm)
if err != nil {
return err
}
return nil
}
// TempDir creates a temp file in the virtual fs
// Took from afero.FS code and adapted
func TempDir(fs v1.FS, dir, prefix string) (name string, err error) {
if dir == "" {
dir = os.TempDir()
}
// This skips adding random stuff to the created temp dir so the temp dir created is predictable for testing
if _, isTestFs := fs.(*vfst.TestFS); isTestFs {
err = MkdirAll(fs, filepath.Join(dir, prefix), 0700)
if err != nil {
return "", err
}
name = filepath.Join(dir, prefix)
return
}
nconflict := 0
for i := 0; i < 10000; i++ {
try := filepath.Join(dir, prefix+nextRandom())
err = MkdirAll(fs, try, 0700)
if os.IsExist(err) {
if nconflict++; nconflict > 10 {
randmu.Lock()
rand = reseed()
randmu.Unlock()
}
continue
}
if err == nil {
name = try
}
break
}
return
}
// Random number state.
// We generate random temporary file names so that there's a good
// chance the file doesn't exist yet - keeps the number of tries in
// TempFile to a minimum.
var rand uint32
var randmu sync.Mutex
func reseed() uint32 {
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
}
func nextRandom() string {
randmu.Lock()
r := rand
if r == 0 {
r = reseed()
}
r = r*1664525 + 1013904223 // constants from Numerical Recipes
rand = r
randmu.Unlock()
return strconv.Itoa(int(1e9 + r%1e9))[1:]
}
// CopyFile Copies source file to target file using Fs interface. If target
// is directory source is copied into that directory using source name file.
func CopyFile(fs v1.FS, source string, target string) (err error) {
return ConcatFiles(fs, []string{source}, target)
}
// IsDir check if the path is a dir
func IsDir(fs v1.FS, path string) (bool, error) {
fi, err := fs.Stat(path)
if err != nil {
return false, err
}
return fi.IsDir(), nil
}
// ConcatFiles Copies source files to target file using Fs interface.
// Source files are concatenated into target file in the given order.
// If target is a directory source is copied into that directory using
// 1st source name file.
func ConcatFiles(fs v1.FS, sources []string, target string) (err error) {
if len(sources) == 0 {
return fmt.Errorf("Empty sources list")
}
if dir, _ := IsDir(fs, target); dir {
target = filepath.Join(target, filepath.Base(sources[0]))
}
targetFile, err := fs.Create(target)
if err != nil {
return err
}
defer func() {
if err == nil {
err = targetFile.Close()
} else {
_ = fs.Remove(target)
}
}()
var sourceFile *os.File
for _, source := range sources {
sourceFile, err = fs.Open(source)
if err != nil {
break
}
_, err = io.Copy(targetFile, sourceFile)
if err != nil {
break
}
err = sourceFile.Close()
if err != nil {
break
}
}
return err
}
// DirSize returns the accumulated size of all files in folder
func DirSize(fs v1.FS, path string) (int64, error) {
var size int64
err := vfs.Walk(fs, path, func(_ string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
size += info.Size()
}
return err
})
return size, err
}
// Check if a file or directory exists.
func Exists(fs v1.FS, path string) (bool, error) {
_, err := fs.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
// CalcFileChecksum opens the given file and returns the sha256 checksum of it.
func CalcFileChecksum(fs v1.FS, fileName string) (string, error) {
f, err := fs.Open(fileName)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}

View File

@ -2,4 +2,4 @@
set -ex
elemental --config-dir /config $@
enki --config-dir /config $@