kairos-agent/pkg/action/upgrade_test.go
2023-07-10 14:39:48 +02:00

601 lines
22 KiB
Go

/*
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"
"fmt"
"path/filepath"
"github.com/jaypipes/ghw/pkg/block"
"github.com/kairos-io/kairos-agent/v2/pkg/action"
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
conf "github.com/kairos-io/kairos-agent/v2/pkg/elementalConfig"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
"github.com/kairos-io/kairos-agent/v2/pkg/utils"
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("Runtime Actions", func() {
var config *v1.RunConfig
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 ghwTest v1mock.GhwMock
var extractor *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)
extractor = v1mock.NewFakeImageExtractor(logger)
var err error
fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{})
Expect(err).Should(BeNil())
cloudInit = &v1mock.FakeCloudInitRunner{}
config = conf.NewRunConfig(
conf.WithFs(fs),
conf.WithRunner(runner),
conf.WithLogger(logger),
conf.WithMounter(mounter),
conf.WithSyscall(syscall),
conf.WithClient(client),
conf.WithCloudInitRunner(cloudInit),
conf.WithImageExtractor(extractor),
conf.WithPlatform("linux/amd64"),
)
})
AfterEach(func() {
cleanup()
})
Describe("Upgrade Action", Label("upgrade"), func() {
var spec *v1.UpgradeSpec
var upgrade *action.UpgradeAction
var memLog *bytes.Buffer
activeImg := fmt.Sprintf("%s/cOS/%s", constants.RunningStateDir, constants.ActiveImgFile)
passiveImg := fmt.Sprintf("%s/cOS/%s", constants.RunningStateDir, constants.PassiveImgFile)
recoveryImgSquash := fmt.Sprintf("%s/cOS/%s", constants.LiveDir, constants.RecoverySquashFile)
recoveryImg := fmt.Sprintf("%s/cOS/%s", constants.LiveDir, constants.RecoveryImgFile)
BeforeEach(func() {
memLog = &bytes.Buffer{}
logger = v1.NewBufferLogger(memLog)
extractor = v1mock.NewFakeImageExtractor(logger)
config.Logger = logger
config.ImageExtractor = extractor
logger.SetLevel(logrus.DebugLevel)
// Create paths used by tests
utils.MkdirAll(fs, fmt.Sprintf("%s/cOS", constants.RunningStateDir), constants.DirPerm)
utils.MkdirAll(fs, fmt.Sprintf("%s/cOS", constants.LiveDir), constants.DirPerm)
mainDisk := block.Disk{
Name: "device",
Partitions: []*block.Partition{
{
Name: "device1",
FilesystemLabel: "COS_GRUB",
Type: "ext4",
},
{
Name: "device2",
FilesystemLabel: "COS_STATE",
Type: "ext4",
MountPoint: constants.RunningStateDir,
},
{
Name: "loop0",
FilesystemLabel: "COS_ACTIVE",
Type: "ext4",
},
{
Name: "device5",
FilesystemLabel: "COS_RECOVERY",
Type: "ext4",
MountPoint: constants.LiveDir,
},
{
Name: "device6",
FilesystemLabel: "COS_OEM",
Type: "ext4",
},
},
}
ghwTest = v1mock.GhwMock{}
ghwTest.AddDisk(mainDisk)
ghwTest.CreateDevices()
})
AfterEach(func() {
ghwTest.Clean()
})
Describe(fmt.Sprintf("Booting from %s", constants.ActiveLabel), Label("active_label"), func() {
var err error
BeforeEach(func() {
spec, err = conf.NewUpgradeSpec(config.Config)
Expect(err).ShouldNot(HaveOccurred())
err = utils.MkdirAll(config.Fs, filepath.Join(spec.Active.MountPoint, "etc"), constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
err = fs.WriteFile(
filepath.Join(spec.Active.MountPoint, "etc", "os-release"),
[]byte("GRUB_ENTRY_NAME=TESTOS"),
constants.FilePerm,
)
Expect(err).ShouldNot(HaveOccurred())
spec.Active.Size = 10
spec.Passive.Size = 10
spec.Recovery.Size = 10
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
if command == "cat" && args[0] == "/proc/cmdline" {
return []byte(constants.ActiveLabel), nil
}
if command == "mv" && args[0] == "-f" && args[1] == activeImg && args[2] == passiveImg {
// we doing backup, do the "move"
source, _ := fs.ReadFile(activeImg)
_ = fs.WriteFile(passiveImg, source, constants.FilePerm)
_ = fs.RemoveAll(activeImg)
}
if command == "mv" && args[0] == "-f" && args[1] == spec.Active.File && args[2] == activeImg {
// we doing the image substitution, do the "move"
source, _ := fs.ReadFile(spec.Active.File)
_ = fs.WriteFile(activeImg, source, constants.FilePerm)
_ = fs.RemoveAll(spec.Active.File)
}
return []byte{}, nil
}
config.Runner = runner
// Create fake active/passive files
_ = fs.WriteFile(activeImg, []byte("active"), constants.FilePerm)
_ = fs.WriteFile(passiveImg, []byte("passive"), constants.FilePerm)
// Mount state partition as it is expected to be mounted when booting from active
mounter.Mount("device2", constants.RunningStateDir, "auto", []string{"ro"})
})
AfterEach(func() {
_ = fs.RemoveAll(activeImg)
_ = fs.RemoveAll(passiveImg)
mounter.Unmount("device2")
})
It("Fails if some hook fails and strict is set", func() {
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
if command == "cat" && args[0] == "/proc/cmdline" {
return []byte(constants.ActiveLabel), nil
}
return []byte{}, nil
}
config.Strict = true
cloudInit.Error = true
upgrade = action.NewUpgradeAction(config, spec)
err := upgrade.Run()
Expect(err).To(HaveOccurred())
// Make sure is a cloud init error!
Expect(err.Error()).To(ContainSubstring("cloud init"))
})
It("Successfully upgrades from docker image", Label("docker"), func() {
spec.Active.Source = v1.NewDockerSrc("alpine")
upgrade = action.NewUpgradeAction(config, spec)
err := upgrade.Run()
Expect(err).ToNot(HaveOccurred())
// Check that the rebrand worked with our os-release value
Expect(memLog).To(ContainSubstring("default_menu_entry=TESTOS"))
// This should be the new image
info, err := fs.Stat(activeImg)
Expect(err).ToNot(HaveOccurred())
// Image size should be the config.ImgSize as its truncated from the upgrade
Expect(info.Size()).To(BeNumerically("==", int64(spec.Active.Size*1024*1024)))
Expect(info.IsDir()).To(BeFalse())
// Should have backed up active to passive
info, err = fs.Stat(passiveImg)
Expect(err).ToNot(HaveOccurred())
// Should be a tiny image as it should only contain our text
// As this was generated by us at the start test and moved by the upgrade from active.iomg
Expect(info.Size()).To(BeNumerically(">", 0))
Expect(info.Size()).To(BeNumerically("<", int64(spec.Active.Size*1024*1024)))
f, _ := fs.ReadFile(passiveImg)
// This should be a backup so it should read active
Expect(f).To(ContainSubstring("active"))
// Expect transition image to be gone
_, err = fs.Stat(spec.Active.File)
Expect(err).To(HaveOccurred())
})
It("Successfully reboots after upgrade from docker image", Label("docker"), func() {
spec.Active.Source = v1.NewDockerSrc("alpine")
config.Reboot = true
upgrade = action.NewUpgradeAction(config, spec)
By("Upgrading")
err := upgrade.Run()
Expect(err).ToNot(HaveOccurred())
By("Checking the log")
// Check that the rebrand worked with our os-release value
Expect(memLog).To(ContainSubstring("default_menu_entry=TESTOS"))
By("checking active image")
// This should be the new image
info, err := fs.Stat(activeImg)
Expect(err).ToNot(HaveOccurred())
// Image size should be the config.ImgSize as its truncated from the upgrade
Expect(info.Size()).To(BeNumerically("==", int64(spec.Active.Size*1024*1024)))
Expect(info.IsDir()).To(BeFalse())
By("Checking passive image")
// Should have backed up active to passive
info, err = fs.Stat(passiveImg)
Expect(err).ToNot(HaveOccurred())
// Should be a tiny image as it should only contain our text
// As this was generated by us at the start test and moved by the upgrade from active.iomg
Expect(info.Size()).To(BeNumerically(">", 0))
Expect(info.Size()).To(BeNumerically("<", int64(spec.Active.Size*1024*1024)))
f, _ := fs.ReadFile(passiveImg)
// This should be a backup so it should read active
Expect(f).To(ContainSubstring("active"))
By("checking transition image")
// Expect transition image to be gone
_, err = fs.Stat(spec.Active.File)
Expect(err).To(HaveOccurred())
By("checking it called reboot")
Expect(runner.IncludesCmds([][]string{{"reboot", "-f"}})).To(BeNil())
})
It("Successfully powers off after upgrade from docker image", Label("docker"), func() {
spec.Active.Source = v1.NewDockerSrc("alpine")
config.PowerOff = true
upgrade = action.NewUpgradeAction(config, spec)
err := upgrade.Run()
Expect(err).ToNot(HaveOccurred())
// Check that the rebrand worked with our os-release value
Expect(memLog).To(ContainSubstring("default_menu_entry=TESTOS"))
// This should be the new image
info, err := fs.Stat(activeImg)
Expect(err).ToNot(HaveOccurred())
// Image size should be the config.ImgSize as its truncated from the upgrade
Expect(info.Size()).To(BeNumerically("==", int64(spec.Active.Size*1024*1024)))
Expect(info.IsDir()).To(BeFalse())
// Should have backed up active to passive
info, err = fs.Stat(passiveImg)
Expect(err).ToNot(HaveOccurred())
// Should be a tiny image as it should only contain our text
// As this was generated by us at the start test and moved by the upgrade from active.iomg
Expect(info.Size()).To(BeNumerically(">", 0))
Expect(info.Size()).To(BeNumerically("<", int64(spec.Active.Size*1024*1024)))
f, _ := fs.ReadFile(passiveImg)
// This should be a backup so it should read active
Expect(f).To(ContainSubstring("active"))
// Expect transition image to be gone
_, err = fs.Stat(spec.Active.File)
Expect(err).To(HaveOccurred())
Expect(runner.IncludesCmds([][]string{{"poweroff", "-f"}})).To(BeNil())
})
It("Successfully upgrades from directory", Label("directory"), func() {
dirSrc, _ := utils.TempDir(fs, "", "elementalupgrade")
// Create the dir on real os as rsync works on the real os
defer fs.RemoveAll(dirSrc)
spec.Active.Source = v1.NewDirSrc(dirSrc)
// create a random file on it
err := fs.WriteFile(fmt.Sprintf("%s/file.file", dirSrc), []byte("something"), constants.FilePerm)
Expect(err).ToNot(HaveOccurred())
upgrade = action.NewUpgradeAction(config, spec)
err = upgrade.Run()
Expect(err).ToNot(HaveOccurred())
// Check that the rebrand worked with our os-release value
Expect(memLog).To(ContainSubstring("default_menu_entry=TESTOS"))
// Not much that we can create here as the dir copy was done on the real os, but we do the rest of the ops on a mem one
// This should be the new image
info, err := fs.Stat(activeImg)
Expect(err).ToNot(HaveOccurred())
// Image size should not be empty
Expect(info.Size()).To(BeNumerically("==", int64(spec.Active.Size*1024*1024)))
Expect(info.IsDir()).To(BeFalse())
// Should have backed up active to passive
info, err = fs.Stat(passiveImg)
Expect(err).ToNot(HaveOccurred())
// Should be a tiny image as it should only contain our text
// As this was generated by us at the start test and moved by the upgrade from active.img
Expect(info.Size()).To(BeNumerically(">", 0))
Expect(info.Size()).To(BeNumerically("<", int64(spec.Active.Size*1024*1024)))
f, _ := fs.ReadFile(passiveImg)
// This should be a backup so it should read active
Expect(f).To(ContainSubstring("active"))
// Expect transition image to be gone
_, err = fs.Stat(spec.Active.File)
Expect(err).To(HaveOccurred())
})
})
Describe(fmt.Sprintf("Booting from %s", constants.PassiveLabel), Label("passive_label"), func() {
var err error
BeforeEach(func() {
spec, err = conf.NewUpgradeSpec(config.Config)
Expect(err).ShouldNot(HaveOccurred())
err = utils.MkdirAll(config.Fs, filepath.Join(spec.Active.MountPoint, "etc"), constants.DirPerm)
Expect(err).ShouldNot(HaveOccurred())
err = fs.WriteFile(
filepath.Join(spec.Active.MountPoint, "etc", "os-release"),
[]byte("GRUB_ENTRY_NAME=TESTOS"),
constants.FilePerm,
)
Expect(err).ShouldNot(HaveOccurred())
spec.Active.Size = 10
spec.Passive.Size = 10
spec.Recovery.Size = 10
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
if command == "cat" && args[0] == "/proc/cmdline" {
return []byte(constants.PassiveLabel), nil
}
if command == "mv" && args[0] == "-f" && args[1] == spec.Active.File && args[2] == activeImg {
// we doing the image substitution, do the "move"
source, _ := fs.ReadFile(spec.Active.File)
_ = fs.WriteFile(activeImg, source, constants.FilePerm)
_ = fs.RemoveAll(spec.Active.File)
}
return []byte{}, nil
}
config.Runner = runner
// Create fake active/passive files
_ = fs.WriteFile(activeImg, []byte("active"), constants.FilePerm)
_ = fs.WriteFile(passiveImg, []byte("passive"), constants.FilePerm)
// Mount state partition as it is expected to be mounted when booting from active
mounter.Mount("device2", constants.RunningStateDir, "auto", []string{"ro"})
})
AfterEach(func() {
_ = fs.RemoveAll(activeImg)
_ = fs.RemoveAll(passiveImg)
})
It("does not backup active img to passive", Label("docker"), func() {
spec.Active.Source = v1.NewDockerSrc("alpine")
upgrade = action.NewUpgradeAction(config, spec)
err := upgrade.Run()
Expect(err).ToNot(HaveOccurred())
// Check that the rebrand worked with our os-release value
Expect(memLog).To(ContainSubstring("default_menu_entry=TESTOS"))
// This should be the new image
info, err := fs.Stat(activeImg)
Expect(err).ToNot(HaveOccurred())
// Image size should not be empty
Expect(info.Size()).To(BeNumerically("==", int64(spec.Active.Size*1024*1024)))
Expect(info.IsDir()).To(BeFalse())
// Passive should have not been touched
info, err = fs.Stat(passiveImg)
Expect(err).ToNot(HaveOccurred())
// Should be a tiny image as it should only contain our text
// As this was generated by us at the start test and moved by the upgrade from active.iomg
Expect(info.Size()).To(BeNumerically(">", 0))
Expect(info.Size()).To(BeNumerically("<", int64(spec.Active.Size*1024*1024)))
f, _ := fs.ReadFile(passiveImg)
Expect(f).To(ContainSubstring("passive"))
// Expect transition image to be gone
_, err = fs.Stat(spec.Active.File)
Expect(err).To(HaveOccurred())
})
})
Describe(fmt.Sprintf("Booting from %s", constants.RecoveryLabel), Label("recovery_label"), func() {
Describe("Using squashfs", Label("squashfs"), func() {
var err error
BeforeEach(func() {
// Mount recovery partition as it is expected to be mounted when booting from recovery
mounter.Mount("device5", constants.LiveDir, "auto", []string{"ro"})
// Create recoveryImgSquash so ti identifies that we are using squash recovery
err = fs.WriteFile(recoveryImgSquash, []byte("recovery"), constants.FilePerm)
Expect(err).ShouldNot(HaveOccurred())
spec, err = conf.NewUpgradeSpec(config.Config)
Expect(err).ShouldNot(HaveOccurred())
spec.Active.Size = 10
spec.Passive.Size = 10
spec.Recovery.Size = 10
spec.RecoveryUpgrade = true
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
if command == "cat" && args[0] == "/proc/cmdline" {
return []byte(constants.RecoveryLabel), nil
}
if command == "mksquashfs" && args[1] == spec.Recovery.File {
// create the transition img for squash to fake it
_, _ = fs.Create(spec.Recovery.File)
}
if command == "mv" && args[0] == "-f" && args[1] == spec.Recovery.File && args[2] == recoveryImgSquash {
// fake "move"
f, _ := fs.ReadFile(spec.Recovery.File)
_ = fs.WriteFile(recoveryImgSquash, f, constants.FilePerm)
_ = fs.RemoveAll(spec.Recovery.File)
}
return []byte{}, nil
}
config.Runner = runner
})
It("Successfully upgrades recovery from docker image", Label("docker"), func() {
// This should be the old image
info, err := fs.Stat(recoveryImgSquash)
Expect(err).ToNot(HaveOccurred())
// Image size should be empty
Expect(info.Size()).To(BeNumerically(">", 0))
Expect(info.IsDir()).To(BeFalse())
f, _ := fs.ReadFile(recoveryImgSquash)
Expect(f).To(ContainSubstring("recovery"))
spec.Recovery.Source = v1.NewDockerSrc("alpine")
upgrade = action.NewUpgradeAction(config, spec)
err = upgrade.Run()
Expect(err).ToNot(HaveOccurred())
// This should be the new image
info, err = fs.Stat(recoveryImgSquash)
Expect(err).ToNot(HaveOccurred())
// Image size should be empty
Expect(info.Size()).To(BeNumerically("==", 0))
Expect(info.IsDir()).To(BeFalse())
f, _ = fs.ReadFile(recoveryImgSquash)
Expect(f).ToNot(ContainSubstring("recovery"))
// Transition squash should not exist
info, err = fs.Stat(spec.Recovery.File)
Expect(err).To(HaveOccurred())
})
It("Successfully upgrades recovery from directory", Label("directory"), func() {
srcDir, _ := utils.TempDir(fs, "", "elemental")
// create a random file on it
_ = fs.WriteFile(fmt.Sprintf("%s/file.file", srcDir), []byte("something"), constants.FilePerm)
spec.Recovery.Source = v1.NewDirSrc(srcDir)
upgrade = action.NewUpgradeAction(config, spec)
err := upgrade.Run()
Expect(err).ToNot(HaveOccurred())
// This should be the new image
info, err := fs.Stat(recoveryImgSquash)
Expect(err).ToNot(HaveOccurred())
// Image size should be empty
Expect(info.Size()).To(BeNumerically("==", 0))
Expect(info.IsDir()).To(BeFalse())
// Transition squash should not exist
info, err = fs.Stat(spec.Recovery.File)
Expect(err).To(HaveOccurred())
})
})
Describe("Not using squashfs", Label("non-squashfs"), func() {
var err error
BeforeEach(func() {
// Create recoveryImg so it identifies that we are using nonsquash recovery
err = fs.WriteFile(recoveryImg, []byte("recovery"), constants.FilePerm)
Expect(err).ShouldNot(HaveOccurred())
spec, err = conf.NewUpgradeSpec(config.Config)
Expect(err).ShouldNot(HaveOccurred())
spec.Active.Size = 10
spec.Passive.Size = 10
spec.Recovery.Size = 10
spec.RecoveryUpgrade = true
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
if command == "cat" && args[0] == "/proc/cmdline" {
return []byte(constants.RecoveryLabel), nil
}
if command == "mv" && args[0] == "-f" && args[1] == spec.Recovery.File && args[2] == recoveryImg {
// fake "move"
f, _ := fs.ReadFile(spec.Recovery.File)
_ = fs.WriteFile(recoveryImg, f, constants.FilePerm)
_ = fs.RemoveAll(spec.Recovery.File)
}
return []byte{}, nil
}
config.Runner = runner
_ = fs.WriteFile(recoveryImg, []byte("recovery"), constants.FilePerm)
// Mount recovery partition as it is expected to be mounted when booting from recovery
mounter.Mount("device5", constants.LiveDir, "auto", []string{"ro"})
})
It("Successfully upgrades recovery from docker image", Label("docker"), func() {
// This should be the old image
info, err := fs.Stat(recoveryImg)
Expect(err).ToNot(HaveOccurred())
// Image size should not be empty
Expect(info.Size()).To(BeNumerically(">", 0))
Expect(info.Size()).To(BeNumerically("<", int64(spec.Recovery.Size*1024*1024)))
Expect(info.IsDir()).To(BeFalse())
f, _ := fs.ReadFile(recoveryImg)
Expect(f).To(ContainSubstring("recovery"))
spec.Recovery.Source = v1.NewDockerSrc("apline")
upgrade = action.NewUpgradeAction(config, spec)
err = upgrade.Run()
Expect(err).ToNot(HaveOccurred())
// Should have created recovery image
info, err = fs.Stat(recoveryImg)
Expect(err).ToNot(HaveOccurred())
// Image size should be default size
Expect(info.Size()).To(BeNumerically("==", int64(spec.Recovery.Size*1024*1024)))
// Expect the rest of the images to not be there
for _, img := range []string{activeImg, passiveImg, recoveryImgSquash} {
_, err := fs.Stat(img)
Expect(err).To(HaveOccurred())
}
})
It("Successfully upgrades recovery from directory", Label("directory"), func() {
srcDir, _ := utils.TempDir(fs, "", "elemental")
// create a random file on it
_ = fs.WriteFile(fmt.Sprintf("%s/file.file", srcDir), []byte("something"), constants.FilePerm)
spec.Recovery.Source = v1.NewDirSrc(srcDir)
upgrade = action.NewUpgradeAction(config, spec)
err := upgrade.Run()
Expect(err).ToNot(HaveOccurred())
// This should be the new image
info, err := fs.Stat(recoveryImg)
Expect(err).ToNot(HaveOccurred())
// Image size should be default size
Expect(info.Size()).To(BeNumerically("==", int64(spec.Recovery.Size*1024*1024)))
Expect(info.IsDir()).To(BeFalse())
// Transition squash should not exist
info, err = fs.Stat(spec.Recovery.File)
Expect(err).To(HaveOccurred())
})
})
})
})
})