diff --git a/pkg/action/build-disk.go b/pkg/action/build-disk.go index 6281219784..5bc6ff13de 100644 --- a/pkg/action/build-disk.go +++ b/pkg/action/build-disk.go @@ -162,7 +162,7 @@ func (b *BuildDiskAction) BuildDiskRun() (err error) { //nolint:gocyclo recRoot := filepath.Join(workdir, filepath.Base(b.spec.RecoverySystem.File)+rootSuffix) // Create recovery root - err = elemental.DumpSource(b.cfg.Config, recRoot, b.spec.RecoverySystem.Source) + err = elemental.MirrorRoot(b.cfg.Config, recRoot, b.spec.RecoverySystem.Source) if err != nil { b.cfg.Logger.Errorf("failed loading recovery image source tree: %s", err.Error()) return err @@ -425,7 +425,7 @@ func (b *BuildDiskAction) createStatePartitionImage() (*types.Image, error) { } // Deploy system image - err = elemental.DumpSource(b.cfg.Config, b.snapshot.WorkDir, system) + err = elemental.MirrorRoot(b.cfg.Config, b.snapshot.WorkDir, system) if err != nil { _ = b.snapshotter.CloseTransactionOnError(b.snapshot) b.cfg.Logger.Errorf("failed deploying source: %s", system.String()) diff --git a/pkg/action/build-iso.go b/pkg/action/build-iso.go index 329620fa99..56fe0acd66 100644 --- a/pkg/action/build-iso.go +++ b/pkg/action/build-iso.go @@ -315,7 +315,7 @@ func (b BuildISOAction) burnISO(root, efiImg string) error { func (b BuildISOAction) applySources(target string, sources ...*types.ImageSource) error { for _, src := range sources { - err := elemental.DumpSource(b.cfg.Config, target, src) + err := elemental.DumpSource(b.cfg.Config, target, src, utils.SyncData) if err != nil { return elementalError.NewFromError(err, elementalError.DumpSource) } diff --git a/pkg/action/install.go b/pkg/action/install.go index 3e7ac35b29..42da7e74ee 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -212,7 +212,7 @@ func (i InstallAction) Run() (err error) { cleanup.PushErrorOnly(func() error { return i.snapshotter.CloseTransactionOnError(i.snapshot) }) // Deploy system image - err = elemental.DumpSource(i.cfg.Config, i.snapshot.WorkDir, i.spec.System) + err = elemental.MirrorRoot(i.cfg.Config, i.snapshot.WorkDir, i.spec.System) if err != nil { i.cfg.Logger.Errorf("failed deploying source: %s", i.spec.System.String()) return elementalError.NewFromError(err, elementalError.DumpSource) diff --git a/pkg/action/reset.go b/pkg/action/reset.go index a91f2925af..55ddbd4b53 100644 --- a/pkg/action/reset.go +++ b/pkg/action/reset.go @@ -240,7 +240,7 @@ func (r ResetAction) Run() (err error) { cleanup.PushErrorOnly(func() error { return r.snapshotter.CloseTransactionOnError(r.snapshot) }) // Deploy system image - err = elemental.DumpSource(r.cfg.Config, r.snapshot.WorkDir, r.spec.System) + err = elemental.MirrorRoot(r.cfg.Config, r.snapshot.WorkDir, r.spec.System) if err != nil { r.cfg.Logger.Errorf("failed deploying source: %s", r.spec.System.String()) return elementalError.NewFromError(err, elementalError.DumpSource) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index afd5a3bdd1..1fbbe63507 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -277,7 +277,7 @@ func (u *UpgradeAction) Run() (err error) { cleanup.PushErrorOnly(func() error { return u.snapshotter.CloseTransactionOnError(u.snapshot) }) // Deploy system image - err = elemental.DumpSource(u.cfg.Config, u.snapshot.WorkDir, u.spec.System) + err = elemental.MirrorRoot(u.cfg.Config, u.snapshot.WorkDir, u.spec.System) if err != nil { u.cfg.Logger.Errorf("failed deploying source: %s", u.spec.System.String()) return elementalError.NewFromError(err, elementalError.DumpSource) diff --git a/pkg/elemental/elemental.go b/pkg/elemental/elemental.go index 2251f475c5..77aafa77ae 100644 --- a/pkg/elemental/elemental.go +++ b/pkg/elemental/elemental.go @@ -438,49 +438,6 @@ func CopyFileImg(c types.Config, img *types.Image) error { return err } -// DeployImage will deploy the given image into the target. This method -// creates the filesystem image file and fills it with the correspondant data -func DeployImage(c types.Config, img *types.Image) error { - var err error - var cleaner func() error - - c.Logger.Infof("Deploying image: %s", img.File) - transientTree := strings.TrimSuffix(img.File, filepath.Ext(img.File)) + ".imgTree" - if img.Source.IsDir() { - transientTree = img.Source.Value() - } else if img.Source.IsFile() { - srcImg := &types.Image{ - File: img.Source.Value(), - MountPoint: transientTree, - } - err := MountFileSystemImage(c, srcImg) - if err != nil { - c.Logger.Errorf("failed mounting image tree: %v", err) - return err - } - cleaner = func() error { - err := UnmountFileSystemImage(c, srcImg) - if err != nil { - return err - } - return c.Fs.RemoveAll(transientTree) - } - } else if img.Source.IsImage() { - err = DumpSource(c, transientTree, img.Source) - if err != nil { - c.Logger.Errorf("failed dumping image tree: %v", err) - return err - } - cleaner = func() error { return c.Fs.RemoveAll(transientTree) } - } - err = CreateImageFromTree(c, img, transientTree, false, cleaner) - if err != nil { - c.Logger.Errorf("failed creating image from image tree: %v", err) - return err - } - return nil -} - // DeployRecoverySystem deploys the rootfs image from the img parameter and // extracts kernel+initrd to the same directory. // This can be used for both ISO (all artifacts in same output dir) and raw @@ -518,7 +475,7 @@ func DeployRecoverySystem(cfg types.Config, img *types.Image) error { return cfg.Fs.RemoveAll(transientTree) } } else if img.Source.IsImage() { - err = DumpSource(cfg, transientTree, img.Source) + err = MirrorRoot(cfg, transientTree, img.Source) if err != nil { cfg.Logger.Errorf("failed dumping image tree: %v", err) return err @@ -585,11 +542,21 @@ func DeployRecoverySystem(cfg types.Config, img *types.Image) error { return nil } -// DumpSource sets the image data according to the image source type -func DumpSource(c types.Config, target string, imgSrc *types.ImageSource) error { // nolint:gocyclo +// DumpSource dumps the imgSrc data to target. SyncFunc argument is the function used to synchronize file or directory +// sources (unused for contaier images), defaults to utils.SyncData if nil provided. +func DumpSource( + c types.Config, target string, imgSrc *types.ImageSource, + syncFunc func( + l types.Logger, r types.Runner, f types.FS, src string, dst string, excl ...string, + ) error, +) error { // nolint:gocyclo var err error var digest string + if syncFunc == nil { + syncFunc = utils.SyncData + } + c.Logger.Infof("Copying %s source...", imgSrc.Value()) err = utils.MkdirAll(c.Fs, target, cnst.DirPerm) @@ -618,7 +585,7 @@ func DumpSource(c types.Config, target string, imgSrc *types.ImageSource) error imgSrc.SetDigest(digest) } else if imgSrc.IsDir() { excludes := cnst.GetDefaultSystemExcludes(imgSrc.Value()) - err = utils.MirrorData(c.Logger, c.Runner, c.Fs, imgSrc.Value(), target, excludes...) + err = syncFunc(c.Logger, c.Runner, c.Fs, imgSrc.Value(), target, excludes...) if err != nil { return err } @@ -633,22 +600,28 @@ func DumpSource(c types.Config, target string, imgSrc *types.ImageSource) error return err } defer UnmountFileSystemImage(c, img) // nolint:errcheck - err = utils.MirrorData(c.Logger, c.Runner, c.Fs, cnst.ImgSrcDir, target) + err = syncFunc(c.Logger, c.Runner, c.Fs, cnst.ImgSrcDir, target) if err != nil { return err } } else { return fmt.Errorf("unknown image source type") } - // Create essential directories such as /tmp, /dev, etc. - err = utils.CreateDirStructure(c.Fs, target) - if err != nil { - return err - } + c.Logger.Infof("Finished copying %s into %s", imgSrc.Value(), target) return nil } +// MirrorRoot mirrors image source contents to target. Any preexisting data in target is going to be overwritten or +// deleted to perfectly match image source contents. +func MirrorRoot(c types.Config, target string, imgSrc *types.ImageSource) error { + err := DumpSource(c, target, imgSrc, utils.MirrorData) + if err != nil { + return nil + } + return utils.CreateDirStructure(c.Fs, target) +} + // CopyCloudConfig will check if there is a cloud init in the config and store it on the target func CopyCloudConfig(c types.Config, path string, cloudInit []string) (err error) { if path == "" { diff --git a/pkg/elemental/elemental_test.go b/pkg/elemental/elemental_test.go index 8cfead2086..a9f31d0cad 100644 --- a/pkg/elemental/elemental_test.go +++ b/pkg/elemental/elemental_test.go @@ -537,78 +537,94 @@ var _ = Describe("Elemental", Label("elemental"), func() { }) }) }) + Describe("MirrorRoot", func() { + var destDir string + var syncFunc func(l types.Logger, r types.Runner, f types.FS, src string, dst string, excl ...string) error + var fErr error + BeforeEach(func() { + var err error + destDir, err = utils.TempDir(fs, "", "elemental") + Expect(err).ShouldNot(HaveOccurred()) + syncFunc = func(_ types.Logger, _ types.Runner, _ types.FS, src string, dst string, _ ...string) error { + return fErr + } + }) + It("Unpacks a docker image to target", Label("docker"), func() { + dockerSrc := types.NewDockerSrc("docker/image:latest") + err := elemental.DumpSource(*config, destDir, dockerSrc, syncFunc) + Expect(dockerSrc.GetDigest()).To(Equal("fakeDigest")) + Expect(err).To(BeNil()) + }) + It("Fails to mirror data", func() { + fErr = errors.New("fake synching failure") + err := elemental.DumpSource(*config, destDir, types.NewDirSrc("/source"), syncFunc) + Expect(err).NotTo(BeNil()) + Expect(err.Error()).To(ContainSubstring("fake synching failure")) + }) + }) Describe("DumpSource", Label("dump"), func() { var destDir string + var syncFunc func(l types.Logger, r types.Runner, f types.FS, src string, dst string, excl ...string) error + var fErr error + var src, dst string BeforeEach(func() { var err error + src = "" + dst = "" destDir, err = utils.TempDir(fs, "", "elemental") Expect(err).ShouldNot(HaveOccurred()) + syncFunc = func(_ types.Logger, _ types.Runner, _ types.FS, s string, d string, _ ...string) error { + src = s + dst = d + return fErr + } }) It("Copies files from a directory source", func() { - rsyncCount := 0 - src := "" - dest := "" - - runner.SideEffect = func(cmd string, args ...string) ([]byte, error) { - if cmd == "rsync" { - rsyncCount += 1 - src = args[len(args)-2] - dest = args[len(args)-1] - } - - return []byte{}, nil - } - - err := elemental.DumpSource(*config, "/dest", types.NewDirSrc("/source")) - Expect(err).ShouldNot(HaveOccurred()) - Expect(rsyncCount).To(Equal(1)) - Expect(src).To(HaveSuffix("/source/")) - Expect(dest).To(HaveSuffix("/dest/")) + Expect(elemental.DumpSource(*config, "/dest", types.NewDirSrc("/source"), syncFunc)).To(Succeed()) + Expect(src).To(Equal("/source")) + Expect(dst).To(Equal("/dest")) }) It("Unpacks a docker image to target", Label("docker"), func() { dockerSrc := types.NewDockerSrc("docker/image:latest") - err := elemental.DumpSource(*config, destDir, dockerSrc) + err := elemental.DumpSource(*config, destDir, dockerSrc, syncFunc) Expect(dockerSrc.GetDigest()).To(Equal("fakeDigest")) Expect(err).To(BeNil()) + + // SyncFunc is not used + Expect(src).To(BeEmpty()) + Expect(dst).To(BeEmpty()) }) It("Unpacks a docker image to target with cosign validation", Label("docker", "cosign"), func() { config.Cosign = true - err := elemental.DumpSource(*config, destDir, types.NewDockerSrc("docker/image:latest")) + err := elemental.DumpSource(*config, destDir, types.NewDockerSrc("docker/image:latest"), nil) Expect(err).To(BeNil()) Expect(runner.CmdsMatch([][]string{{"cosign", "verify", "docker/image:latest"}})) }) It("Fails cosign validation", Label("cosign"), func() { runner.ReturnError = errors.New("cosign error") config.Cosign = true - err := elemental.DumpSource(*config, destDir, types.NewDockerSrc("docker/image:latest")) + err := elemental.DumpSource(*config, destDir, types.NewDockerSrc("docker/image:latest"), nil) Expect(err).NotTo(BeNil()) Expect(runner.CmdsMatch([][]string{{"cosign", "verify", "docker/image:latest"}})) }) It("Fails to unpack a docker image to target", Label("docker"), func() { unpackErr := errors.New("failed to unpack") extractor.SideEffect = func(_, _, _ string, _ bool) (string, error) { return "", unpackErr } - err := elemental.DumpSource(*config, destDir, types.NewDockerSrc("docker/image:latest")) + err := elemental.DumpSource(*config, destDir, types.NewDockerSrc("docker/image:latest"), nil) Expect(err).To(Equal(unpackErr)) }) It("Copies image file to target", func() { sourceImg := "/source.img" destFile := filepath.Join(destDir, "active.img") - err := elemental.DumpSource(*config, destFile, types.NewFileSrc(sourceImg)) + err := elemental.DumpSource(*config, destFile, types.NewFileSrc(sourceImg), syncFunc) Expect(err).To(BeNil()) - Expect(runner.IncludesCmds([][]string{{"rsync"}})) + Expect(dst).To(Equal(destFile)) + Expect(src).To(Equal(constants.ImgSrcDir)) }) It("Fails to copy, source can't be mounted", func() { mounter.ErrorOnMount = true - err := elemental.DumpSource(*config, "whatever", types.NewFileSrc("/source.img")) - Expect(err).To(HaveOccurred()) - }) - It("Fails to copy, no write permissions", func() { - sourceImg := "/source.img" - _, err := fs.Create(sourceImg) - Expect(err).To(BeNil()) - config.Fs = vfs.NewReadOnlyFS(fs) - err = elemental.DumpSource(*config, "whatever", types.NewFileSrc("/source.img")) + err := elemental.DumpSource(*config, "whatever", types.NewFileSrc("/source.img"), nil) Expect(err).To(HaveOccurred()) }) }) @@ -670,111 +686,6 @@ var _ = Describe("Elemental", Label("elemental"), func() { Expect(runner.IncludesCmds([][]string{{"rsync"}})) }) }) - Describe("DeployImage", Label("deployImg"), func() { - var imgFile, srcDir string - var img *types.Image - - BeforeEach(func() { - destDir, err := utils.TempDir(fs, "", "test") - Expect(err).ShouldNot(HaveOccurred()) - srcDir, err = utils.TempDir(fs, "", "test") - Expect(err).ShouldNot(HaveOccurred()) - - imgFile = filepath.Join(destDir, "dst.img") - - sf, err := fs.Create(filepath.Join(srcDir, "somefile")) - Expect(err).ShouldNot(HaveOccurred()) - Expect(sf.Truncate(32 * 1024 * 1024)).To(Succeed()) - Expect(sf.Close()).To(Succeed()) - - Expect(err).ShouldNot(HaveOccurred()) - img = &types.Image{ - FS: constants.LinuxImgFs, - File: imgFile, - MountPoint: "/some/mountpoint", - Source: types.NewDirSrc(srcDir), - } - }) - It("Deploys a directory image source into a filesystem image", func() { - err := elemental.DeployImage(*config, img) - Expect(err).ShouldNot(HaveOccurred()) - Expect(runner.IncludesCmds([][]string{{"mkfs.ext2"}, {"rsync"}})).To(Succeed()) - }) - It("Deploys a file image source into a filesystem image", func() { - runner.SideEffect = func(cmd string, args ...string) ([]byte, error) { - if cmd == "losetup" { - return []byte("/dev/loop0"), nil - } - return []byte{}, nil - } - img.Source = types.NewFileSrc("/some/file/path") - err := elemental.DeployImage(*config, img) - Expect(err).ShouldNot(HaveOccurred()) - Expect(runner.IncludesCmds([][]string{{"losetup"}, {"mkfs.ext2"}, {"rsync"}, {"losetup"}})).To(Succeed()) - }) - It("Deploys a container image source into a filesystem image", func() { - img.Source = types.NewDockerSrc("image:tag") - err := elemental.DeployImage(*config, img) - Expect(err).ShouldNot(HaveOccurred()) - Expect(runner.IncludesCmds([][]string{{"mkfs.ext2"}, {"rsync"}})).To(Succeed()) - }) - It("Fails to extract a docker image", func() { - extractor.SideEffect = func(_, _, _ string, _ bool) (string, error) { - return "", fmt.Errorf("failed extracting image") - } - img.Source = types.NewDockerSrc("image:tag") - err := elemental.DeployImage(*config, img) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("extracting image")) - }) - It("Fails to create a loop device", func() { - runner.SideEffect = func(cmd string, args ...string) ([]byte, error) { - if cmd == "losetup" { - return []byte{}, fmt.Errorf("Failed calling losetup") - } - return []byte{}, nil - } - img.Source = types.NewFileSrc("/some/file/path") - err := elemental.DeployImage(*config, img) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("calling losetup")) - Expect(runner.IncludesCmds([][]string{{"losetup"}})).To(Succeed()) - }) - It("Fails to delete a loop device", func() { - runner.SideEffect = func(cmd string, args ...string) ([]byte, error) { - if cmd == "losetup" { - if args[0] == "-d" { - return []byte{}, fmt.Errorf("Failed deleting loop") - } - return []byte("/dev/loop0"), nil - } - return []byte{}, nil - } - img.Source = types.NewFileSrc("/some/file/path") - err := elemental.DeployImage(*config, img) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("deleting loop")) - Expect(runner.IncludesCmds([][]string{{"losetup"}, {"mkfs.ext2"}, {"rsync"}, {"losetup"}})).To(Succeed()) - }) - It("Fails to dump source without write permissions", func() { - config.Fs = vfs.NewReadOnlyFS(fs) - err := elemental.DeployImage(*config, img) - Expect(err).Should(HaveOccurred()) - Expect(runner.IncludesCmds([][]string{{"mkfs.ext2"}})).NotTo(Succeed()) - }) - It("Fails to create filesystem", func() { - runner.SideEffect = func(cmd string, args ...string) ([]byte, error) { - if cmd == "mkfs.ext2" { - return []byte{}, fmt.Errorf("Failed calling mkfs.ext2") - } - return []byte{}, nil - } - err := elemental.DeployImage(*config, img) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("calling mkfs.ext2")) - Expect(runner.IncludesCmds([][]string{{"mkfs.ext2"}})).To(Succeed()) - }) - }) Describe("CopyImgFile", Label("copyimg"), func() { var imgFile, srcFile string var img *types.Image