Migrate enki from osbuilder

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
This commit is contained in:
Dimitris Karakasilis 2023-10-03 12:40:28 +03:00
commit 5eabf74c53
No known key found for this signature in database
GPG Key ID: 286DCAFD2C97DDE3
21 changed files with 3429 additions and 0 deletions

16
Dockerfile Normal file
View File

@ -0,0 +1,16 @@
ARG GO_VERSION=1.20-alpine3.18
FROM golang:$GO_VERSION AS builder
WORKDIR /build
COPY . .
ENV CGO_ENABLED=0
RUN go build -ldflags '-extldflags "-static"' -o /enki
FROM gcr.io/kaniko-project/executor:latest
COPY --from=builder /enki /enki
ENTRYPOINT ["/enki"]
CMD ["convert"]

19
Earthfile Normal file
View File

@ -0,0 +1,19 @@
VERSION 0.7
# renovate: datasource=docker depName=golang
ARG --global GO_VERSION=1.20-alpine3.18
test:
FROM golang:$GO_VERSION
RUN apk add rsync gcc musl-dev docker jq
WORKDIR /build
COPY . .
RUN go mod download
ARG TEST_PATHS=./...
ARG LABEL_FILTER=
ENV CGO_ENABLED=1
# Some test require the docker sock exposed
WITH DOCKER
RUN go run github.com/onsi/ginkgo/v2/ginkgo run --label-filter "$LABEL_FILTER" -v --fail-fast --race --covermode=atomic --coverprofile=coverage.out --coverpkg=github.com/kairos-io/enki/... -p -r $TEST_PATHS
END
SAVE ARTIFACT coverage.out AS LOCAL coverage.out

122
cmd/build-iso.go Normal file
View File

@ -0,0 +1,122 @@
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"
)
// NewBuildISOCmd returns a new instance of the build-iso subcommand and appends it to
// the root command.
func NewBuildISOCmd() *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 {
return CheckRoot()
},
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
},
}
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
}
func init() {
rootCmd.AddCommand(NewBuildISOCmd())
}

70
cmd/build-iso_test.go Normal file
View File

@ -0,0 +1,70 @@
/*
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 cmd
import (
"bytes"
"fmt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/spf13/viper"
)
var _ = Describe("BuildISO", Label("iso", "cmd"), func() {
var buf *bytes.Buffer
BeforeEach(func() {
buf = new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetErr(buf)
})
AfterEach(func() {
viper.Reset()
})
It("Errors out if no rootfs sources are defined", Label("flags"), func() {
_, _, err := executeCommandC(rootCmd, "build-iso")
fmt.Println(buf)
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("rootfs source image for building ISO was not provided"))
})
It("Errors out if rootfs is a non valid argument", Label("flags"), func() {
_, _, err := executeCommandC(rootCmd, "build-iso", "/no/image/reference")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("invalid image reference"))
})
It("Errors out if overlay roofs path does not exist", Label("flags"), func() {
_, _, err := executeCommandC(
rootCmd, "build-iso", "system/cos", "--overlay-rootfs", "/nonexistingpath",
)
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("Invalid path"))
})
It("Errors out if overlay uefi path does not exist", Label("flags"), func() {
_, _, err := executeCommandC(
rootCmd, "build-iso", "someimage:latest", "--overlay-uefi", "/nonexistingpath",
)
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("Invalid path"))
})
It("Errors out if overlay iso path does not exist", Label("flags"), func() {
_, _, err := executeCommandC(
rootCmd, "build-iso", "some/image:latest", "--overlay-iso", "/nonexistingpath",
)
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("Invalid path"))
})
})

29
cmd/cmd_suite_test.go Normal file
View File

@ -0,0 +1,29 @@
/*
Copyright © 2021 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 cmd
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestWhitebox(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "CLI whitebox test suite")
}

53
cmd/command_test.go Normal file
View File

@ -0,0 +1,53 @@
/*
Copyright © 2021 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 cmd
import (
"io/ioutil"
"os"
"github.com/spf13/cobra"
)
func executeCommandC(cmd *cobra.Command, args ...string) (c *cobra.Command, output string, err error) {
// Set args to command
cmd.SetArgs(args)
// store old stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
// Change stdout to our pipe
os.Stdout = w
// run the command
c, err = cmd.ExecuteC()
if err != nil {
// Remember to restore stdout!
os.Stdout = oldStdout
return nil, "", err
}
err = w.Close()
if err != nil {
// Remember to restore stdout!
os.Stdout = oldStdout
return nil, "", err
}
// Read output from our pipe
out, _ := ioutil.ReadAll(r)
// restore stdout
os.Stdout = oldStdout
return c, string(out), nil
}

90
cmd/root.go Normal file
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,
}
}

154
go.mod Normal file
View File

@ -0,0 +1,154 @@
module github.com/kairos-io/enki
go 1.20
require (
github.com/kairos-io/kairos-agent/v2 v2.1.11-0.20230713071318-9a16b94e2af6
github.com/kairos-io/kairos-sdk v0.0.9-0.20230719194412-fe26d1de9166
github.com/mitchellh/mapstructure v1.5.0
github.com/onsi/ginkgo/v2 v2.11.0
github.com/onsi/gomega v1.27.10
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.3 // 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/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // 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-cmp v0.5.9 // indirect
github.com/google/go-containerregistry v0.15.2 // indirect
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 // 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/go-multierror v1.1.1 // 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/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.63 // 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.11.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.12.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/term v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.9.3 // 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
)

1018
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
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,28 @@
/*
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 action_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestActionSuite(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Actions test suite")
}

360
pkg/action/build-iso.go Normal file
View File

@ -0,0 +1,360 @@
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"
sdk "github.com/kairos-io/kairos-sdk/utils"
)
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 := sdk.NewCleanStack()
defer func() { err = cleanup.Cleanup(err) }()
isoTmpDir, err := utils.TempDir(b.cfg.Fs, "", "enki-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...")
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...")
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
}

196
pkg/action/build_test.go Normal file
View File

@ -0,0 +1,196 @@
/*
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 action_test
import (
"bytes"
"errors"
"fmt"
"path/filepath"
"github.com/kairos-io/enki/pkg/action"
"github.com/kairos-io/enki/pkg/config"
"github.com/kairos-io/enki/pkg/constants"
"github.com/kairos-io/enki/pkg/utils"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
v1mock "github.com/kairos-io/kairos-agent/v2/tests/mocks"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/sirupsen/logrus"
"github.com/twpayne/go-vfs"
"github.com/twpayne/go-vfs/vfst"
)
var _ = Describe("BuildISOAction", func() {
var cfg *v1.BuildConfig
var runner *v1mock.FakeRunner
var fs vfs.FS
var logger v1.Logger
var mounter *v1mock.ErrorMounter
var syscall *v1mock.FakeSyscall
var client *v1mock.FakeHTTPClient
var cloudInit *v1mock.FakeCloudInitRunner
var cleanup func()
var memLog *bytes.Buffer
var imageExtractor *v1mock.FakeImageExtractor
BeforeEach(func() {
runner = v1mock.NewFakeRunner()
syscall = &v1mock.FakeSyscall{}
mounter = v1mock.NewErrorMounter()
client = &v1mock.FakeHTTPClient{}
memLog = &bytes.Buffer{}
logger = v1.NewBufferLogger(memLog)
logger.SetLevel(logrus.DebugLevel)
cloudInit = &v1mock.FakeCloudInitRunner{}
fs, cleanup, _ = vfst.NewTestFS(map[string]interface{}{})
imageExtractor = v1mock.NewFakeImageExtractor(logger)
cfg = config.NewBuildConfig(
config.WithFs(fs),
config.WithRunner(runner),
config.WithLogger(logger),
config.WithMounter(mounter),
config.WithSyscall(syscall),
config.WithClient(client),
config.WithCloudInitRunner(cloudInit),
config.WithImageExtractor(imageExtractor),
)
})
AfterEach(func() {
cleanup()
})
Describe("Build ISO", Label("iso"), func() {
var iso *v1.LiveISO
BeforeEach(func() {
iso = config.NewISO()
tmpDir, err := utils.TempDir(fs, "", "test")
Expect(err).ShouldNot(HaveOccurred())
cfg.Date = false
cfg.OutDir = tmpDir
runner.SideEffect = func(cmd string, args ...string) ([]byte, error) {
switch cmd {
case "xorriso":
err := fs.WriteFile(filepath.Join(tmpDir, "elemental.iso"), []byte("profound thoughts"), constants.FilePerm)
return []byte{}, err
default:
return []byte{}, nil
}
}
})
It("Successfully builds an ISO from a Docker image", func() {
rootSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.RootFS = []*v1.ImageSource{rootSrc}
uefiSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.UEFI = []*v1.ImageSource{uefiSrc}
imageSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.Image = []*v1.ImageSource{imageSrc}
// Create kernel and vmlinuz
// Thanks to the testfs stuff in utils.TempDir we know what the temp fs is gonna be as
// its predictable
bootDir := filepath.Join("/tmp/enki-iso/rootfs", "boot")
err := utils.MkdirAll(fs, bootDir, constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Create(filepath.Join(bootDir, "vmlinuz"))
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Create(filepath.Join(bootDir, "initrd"))
Expect(err).ShouldNot(HaveOccurred())
buildISO := action.NewBuildISOAction(cfg, iso)
err = buildISO.ISORun()
Expect(err).ShouldNot(HaveOccurred())
})
It("Fails if kernel or initrd is not found in rootfs", func() {
rootSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.RootFS = []*v1.ImageSource{rootSrc}
uefiSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.UEFI = []*v1.ImageSource{uefiSrc}
imageSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.Image = []*v1.ImageSource{imageSrc}
By("fails without kernel")
buildISO := action.NewBuildISOAction(cfg, iso)
err := buildISO.ISORun()
Expect(err).Should(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("No file found with prefixes"))
Expect(err.Error()).To(ContainSubstring("uImage Image zImage vmlinuz image"))
bootDir := filepath.Join("/tmp/enki-iso/rootfs", "boot")
err = utils.MkdirAll(fs, bootDir, constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Create(filepath.Join(bootDir, "vmlinuz"))
Expect(err).ShouldNot(HaveOccurred())
By("fails without initrd")
buildISO = action.NewBuildISOAction(cfg, iso)
err = buildISO.ISORun()
Expect(err).Should(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("No file found with prefixes"))
Expect(err.Error()).To(ContainSubstring("initrd initramfs"))
})
It("Fails installing image sources", func() {
rootSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.RootFS = []*v1.ImageSource{rootSrc}
uefiSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.UEFI = []*v1.ImageSource{uefiSrc}
imageSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.Image = []*v1.ImageSource{imageSrc}
imageExtractor.SideEffect = func(imageRef, destination, platformRef string) error {
return fmt.Errorf("uh oh")
}
buildISO := action.NewBuildISOAction(cfg, iso)
err := buildISO.ISORun()
Expect(err).Should(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("uh oh"))
})
It("Fails on ISO filesystem creation", func() {
rootSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.RootFS = []*v1.ImageSource{rootSrc}
uefiSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.UEFI = []*v1.ImageSource{uefiSrc}
imageSrc, _ := v1.NewSrcFromURI("oci:image:version")
iso.Image = []*v1.ImageSource{imageSrc}
bootDir := filepath.Join("/tmp/enki-iso/rootfs", "boot")
err := utils.MkdirAll(fs, bootDir, constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Create(filepath.Join(bootDir, "vmlinuz"))
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Create(filepath.Join(bootDir, "initrd"))
Expect(err).ShouldNot(HaveOccurred())
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
if command == "xorriso" {
return []byte{}, errors.New("Burn ISO error")
}
return []byte{}, nil
}
buildISO := action.NewBuildISOAction(cfg, iso)
err = buildISO.ISORun()
Expect(err).Should(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Burn ISO error"))
})
})
})

303
pkg/config/config.go Normal file
View File

@ -0,0 +1,303 @@
package config
import (
"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"
"io"
"io/fs"
"k8s.io/mount-utils"
"os"
"reflect"
"runtime"
)
var decodeHook = viper.DecodeHook(
mapstructure.ComposeDecodeHookFunc(
UnmarshalerHook(),
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
),
)
func WithFs(fs v1.FS) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Fs = fs
return nil
}
}
func WithLogger(logger v1.Logger) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Logger = logger
return nil
}
}
func WithSyscall(syscall v1.SyscallInterface) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Syscall = syscall
return nil
}
}
func WithMounter(mounter mount.Interface) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Mounter = mounter
return nil
}
}
func WithRunner(runner v1.Runner) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Runner = runner
return nil
}
}
func WithClient(client v1.HTTPClient) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Client = client
return nil
}
}
func WithCloudInitRunner(ci v1.CloudInitRunner) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.CloudInitRunner = ci
return nil
}
}
func WithArch(arch string) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.Arch = arch
return nil
}
}
func WithImageExtractor(extractor v1.ImageExtractor) func(r *v1.Config) error {
return func(r *v1.Config) error {
r.ImageExtractor = extractor
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)
// 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)
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,
}
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)
}
}
log.Infof("Starting enki version %s", version.GetVersion())
if log.GetLevel() == logrus.DebugLevel {
log.Debugf("%+v\n", version.Get())
}
}
// 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
}
}

124
pkg/constants/constants.go Normal file
View File

@ -0,0 +1,124 @@
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"
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 rd.live.overlay.overlayfs 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
}

218
pkg/utils/chroot.go Normal file
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
}

40
pkg/utils/common.go Normal file
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")
}
}

223
pkg/utils/fs.go Normal file
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

@ -0,0 +1,29 @@
/*
Copyright © 2021 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_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestWhitebox(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Utils test suite")
}

294
pkg/utils/utils_test.go Normal file
View File

@ -0,0 +1,294 @@
/*
Copyright © 2021 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_test
import (
"errors"
"fmt"
conf "github.com/kairos-io/enki/pkg/config"
"github.com/kairos-io/enki/pkg/constants"
"github.com/kairos-io/enki/pkg/utils"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
v1mock "github.com/kairos-io/kairos-agent/v2/tests/mocks"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/twpayne/go-vfs"
"github.com/twpayne/go-vfs/vfst"
"os"
"strings"
)
var _ = Describe("Utils", Label("utils"), func() {
var config *v1.Config
var runner *v1mock.FakeRunner
var logger v1.Logger
var syscall *v1mock.FakeSyscall
var client *v1mock.FakeHTTPClient
var mounter *v1mock.ErrorMounter
var fs vfs.FS
var cleanup func()
BeforeEach(func() {
runner = v1mock.NewFakeRunner()
syscall = &v1mock.FakeSyscall{}
mounter = v1mock.NewErrorMounter()
client = &v1mock.FakeHTTPClient{}
logger = v1.NewNullLogger()
// Ensure /tmp exists in the VFS
fs, cleanup, _ = vfst.NewTestFS(nil)
fs.Mkdir("/tmp", constants.DirPerm)
fs.Mkdir("/run", constants.DirPerm)
fs.Mkdir("/etc", constants.DirPerm)
config = conf.NewConfig(
conf.WithFs(fs),
conf.WithRunner(runner),
conf.WithLogger(logger),
conf.WithMounter(mounter),
conf.WithSyscall(syscall),
conf.WithClient(client),
)
})
AfterEach(func() { cleanup() })
Describe("Chroot", Label("chroot"), func() {
var chroot *utils.Chroot
BeforeEach(func() {
chroot = utils.NewChroot(
"/whatever",
config,
)
})
Describe("ChrootedCallback method", func() {
It("runs a callback in a chroot", func() {
err := utils.ChrootedCallback(config, "/somepath", map[string]string{}, func() error {
return nil
})
Expect(err).ShouldNot(HaveOccurred())
err = utils.ChrootedCallback(config, "/somepath", map[string]string{}, func() error {
return fmt.Errorf("callback error")
})
Expect(err).Should(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("callback error"))
})
})
Describe("on success", func() {
It("command should be called in the chroot", func() {
_, err := chroot.Run("chroot-command")
Expect(err).To(BeNil())
Expect(syscall.WasChrootCalledWith("/whatever")).To(BeTrue())
})
It("commands should be called with a customized chroot", func() {
chroot.SetExtraMounts(map[string]string{"/real/path": "/in/chroot/path"})
Expect(chroot.Prepare()).To(BeNil())
defer chroot.Close()
_, err := chroot.Run("chroot-command")
Expect(err).To(BeNil())
Expect(syscall.WasChrootCalledWith("/whatever")).To(BeTrue())
_, err = chroot.Run("chroot-another-command")
Expect(err).To(BeNil())
})
It("runs a callback in a custom chroot", func() {
called := false
callback := func() error {
called = true
return nil
}
err := chroot.RunCallback(callback)
Expect(err).To(BeNil())
Expect(syscall.WasChrootCalledWith("/whatever")).To(BeTrue())
Expect(called).To(BeTrue())
})
})
Describe("on failure", func() {
It("should return error if chroot-command fails", func() {
runner.ReturnError = errors.New("run error")
_, err := chroot.Run("chroot-command")
Expect(err).NotTo(BeNil())
Expect(syscall.WasChrootCalledWith("/whatever")).To(BeTrue())
})
It("should return error if callback fails", func() {
called := false
callback := func() error {
called = true
return errors.New("Callback error")
}
err := chroot.RunCallback(callback)
Expect(err).NotTo(BeNil())
Expect(syscall.WasChrootCalledWith("/whatever")).To(BeTrue())
Expect(called).To(BeTrue())
})
It("should return error if preparing twice before closing", func() {
Expect(chroot.Prepare()).To(BeNil())
defer chroot.Close()
Expect(chroot.Prepare()).NotTo(BeNil())
Expect(chroot.Close()).To(BeNil())
Expect(chroot.Prepare()).To(BeNil())
})
It("should return error if failed to chroot", func() {
syscall.ErrorOnChroot = true
_, err := chroot.Run("chroot-command")
Expect(err).ToNot(BeNil())
Expect(syscall.WasChrootCalledWith("/whatever")).To(BeTrue())
Expect(err.Error()).To(ContainSubstring("chroot error"))
})
It("should return error if failed to mount on prepare", Label("mount"), func() {
mounter.ErrorOnMount = true
_, err := chroot.Run("chroot-command")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("mount error"))
})
It("should return error if failed to unmount on close", Label("unmount"), func() {
mounter.ErrorOnUnmount = true
_, err := chroot.Run("chroot-command")
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("failed closing chroot"))
})
})
})
Describe("CopyFile", Label("CopyFile"), func() {
It("Copies source file to target file", func() {
err := utils.MkdirAll(fs, "/some", constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Create("/some/file")
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Stat("/some/otherfile")
Expect(err).Should(HaveOccurred())
Expect(utils.CopyFile(fs, "/some/file", "/some/otherfile")).ShouldNot(HaveOccurred())
e, err := utils.Exists(fs, "/some/otherfile")
Expect(err).ShouldNot(HaveOccurred())
Expect(e).To(BeTrue())
})
It("Copies source file to target folder", func() {
err := utils.MkdirAll(fs, "/some", constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
err = utils.MkdirAll(fs, "/someotherfolder", constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Create("/some/file")
Expect(err).ShouldNot(HaveOccurred())
_, err = fs.Stat("/someotherfolder/file")
Expect(err).Should(HaveOccurred())
Expect(utils.CopyFile(fs, "/some/file", "/someotherfolder")).ShouldNot(HaveOccurred())
e, err := utils.Exists(fs, "/someotherfolder/file")
Expect(err).ShouldNot(HaveOccurred())
Expect(e).To(BeTrue())
})
It("Fails to open non existing file", func() {
err := utils.MkdirAll(fs, "/some", constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
Expect(utils.CopyFile(fs, "/some/file", "/some/otherfile")).NotTo(BeNil())
_, err = fs.Stat("/some/otherfile")
Expect(err).NotTo(BeNil())
})
It("Fails to copy on non writable target", func() {
err := utils.MkdirAll(fs, "/some", constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
fs.Create("/some/file")
_, err = fs.Stat("/some/otherfile")
Expect(err).NotTo(BeNil())
fs = vfs.NewReadOnlyFS(fs)
Expect(utils.CopyFile(fs, "/some/file", "/some/otherfile")).NotTo(BeNil())
_, err = fs.Stat("/some/otherfile")
Expect(err).NotTo(BeNil())
})
})
Describe("CreateDirStructure", Label("CreateDirStructure"), func() {
It("Creates essential directories", func() {
dirList := []string{"sys", "proc", "dev", "tmp", "boot", "usr/local", "oem"}
for _, dir := range dirList {
_, err := fs.Stat(fmt.Sprintf("/my/root/%s", dir))
Expect(err).NotTo(BeNil())
}
Expect(utils.CreateDirStructure(fs, "/my/root")).To(BeNil())
for _, dir := range dirList {
fi, err := fs.Stat(fmt.Sprintf("/my/root/%s", dir))
Expect(err).To(BeNil())
if fi.Name() == "tmp" {
Expect(fmt.Sprintf("%04o", fi.Mode().Perm())).To(Equal("0777"))
Expect(fi.Mode() & os.ModeSticky).NotTo(Equal(0))
}
if fi.Name() == "sys" {
Expect(fmt.Sprintf("%04o", fi.Mode().Perm())).To(Equal("0555"))
}
}
})
It("Fails on non writable target", func() {
fs = vfs.NewReadOnlyFS(fs)
Expect(utils.CreateDirStructure(fs, "/my/root")).NotTo(BeNil())
})
})
Describe("DirSize", Label("fs"), func() {
BeforeEach(func() {
err := utils.MkdirAll(fs, "/folder/subfolder", constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
f, err := fs.Create("/folder/file")
Expect(err).ShouldNot(HaveOccurred())
err = f.Truncate(1024)
Expect(err).ShouldNot(HaveOccurred())
f, err = fs.Create("/folder/subfolder/file")
Expect(err).ShouldNot(HaveOccurred())
err = f.Truncate(2048)
Expect(err).ShouldNot(HaveOccurred())
})
It("Returns the expected size of a test folder", func() {
size, err := utils.DirSize(fs, "/folder")
Expect(err).ShouldNot(HaveOccurred())
Expect(size).To(Equal(int64(3072)))
})
})
Describe("CalcFileChecksum", Label("checksum"), func() {
It("compute correct sha256 checksum", func() {
testData := strings.Repeat("abcdefghilmnopqrstuvz\n", 20)
testDataSHA256 := "7f182529f6362ae9cfa952ab87342a7180db45d2c57b52b50a68b6130b15a422"
err := fs.Mkdir("/iso", constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
err = fs.WriteFile("/iso/test.iso", []byte(testData), 0644)
Expect(err).ShouldNot(HaveOccurred())
checksum, err := utils.CalcFileChecksum(fs, "/iso/test.iso")
Expect(err).ShouldNot(HaveOccurred())
Expect(checksum).To(Equal(testDataSHA256))
})
})
Describe("CreateSquashFS", Label("CreateSquashFS"), func() {
It("runs with no options if none given", func() {
err := utils.CreateSquashFS(runner, logger, "source", "dest", []string{})
Expect(runner.IncludesCmds([][]string{
{"mksquashfs", "source", "dest"},
})).To(BeNil())
Expect(err).ToNot(HaveOccurred())
})
It("runs with options if given", func() {
err := utils.CreateSquashFS(runner, logger, "source", "dest", constants.GetDefaultSquashfsOptions())
cmd := []string{"mksquashfs", "source", "dest"}
cmd = append(cmd, constants.GetDefaultSquashfsOptions()...)
Expect(runner.IncludesCmds([][]string{
cmd,
})).To(BeNil())
Expect(err).ToNot(HaveOccurred())
})
It("returns an error if it fails", func() {
runner.ReturnError = errors.New("error")
err := utils.CreateSquashFS(runner, logger, "source", "dest", []string{})
Expect(runner.IncludesCmds([][]string{
{"mksquashfs", "source", "dest"},
})).To(BeNil())
Expect(err).To(HaveOccurred())
})
})
})