/* 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()) }) }) })