Migrate enki from osbuilder
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
This commit is contained in:
commit
5eabf74c53
16
Dockerfile
Normal file
16
Dockerfile
Normal 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
19
Earthfile
Normal 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
122
cmd/build-iso.go
Normal 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
70
cmd/build-iso_test.go
Normal 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
29
cmd/cmd_suite_test.go
Normal 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
53
cmd/command_test.go
Normal 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
90
cmd/root.go
Normal 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
154
go.mod
Normal 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
|
||||
)
|
36
internal/version/version.go
Normal file
36
internal/version/version.go
Normal 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
7
main.go
Normal file
@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "github.com/kairos-io/enki/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
28
pkg/action/action_suite_test.go
Normal file
28
pkg/action/action_suite_test.go
Normal 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
360
pkg/action/build-iso.go
Normal 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
196
pkg/action/build_test.go
Normal 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
303
pkg/config/config.go
Normal 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
124
pkg/constants/constants.go
Normal 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
218
pkg/utils/chroot.go
Normal 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
40
pkg/utils/common.go
Normal 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
223
pkg/utils/fs.go
Normal 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
|
||||
}
|
29
pkg/utils/utils_suite_test.go
Normal file
29
pkg/utils/utils_suite_test.go
Normal 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
294
pkg/utils/utils_test.go
Normal 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())
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user