Skip to content

Commit

Permalink
build-disk command (#1794)
Browse files Browse the repository at this point in the history
This commit adds in elemental client the build-disk command. With this command we can eventually build an image that includes partitions:

* EFI
* OEM
* Recovery
* State

Having State partition to match the minimum size (to reduce resulting image size), only includes config files no image.

Then the State partition could be expanded on first boot to desired size (build-disk command already pre-appends the required cloud-config files for that to happen) and then the Persistent partition created at the end with all the available space (or some desired specific size too).

This setup can be executed without running a single mount (thanks to squashfs usage), meaning this disk could be built in a container or Dockerfile (like we do with ISOs).

Building full disks including all partitions with an specified size is still possible, however this approach requires mount privileges and because of that it can't be executed inside non privileged containers.


Signed-off-by: David Cassany <[email protected]>
  • Loading branch information
davidcassany authored Oct 17, 2023
1 parent 4c84315 commit 3ea9d75
Show file tree
Hide file tree
Showing 29 changed files with 2,163 additions and 157 deletions.
120 changes: 120 additions & 0 deletions cmd/build-disk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
Copyright © 2022 - 2023 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 (
"os/exec"

"github.com/rancher/elemental-toolkit/pkg/constants"
eleError "github.com/rancher/elemental-toolkit/pkg/error"
elementalError "github.com/rancher/elemental-toolkit/pkg/error"
v1 "github.com/rancher/elemental-toolkit/pkg/types/v1"

"github.com/spf13/cobra"
"github.com/spf13/viper"
"k8s.io/mount-utils"

"github.com/rancher/elemental-toolkit/cmd/config"
"github.com/rancher/elemental-toolkit/pkg/action"
)

// NewBuildDisk returns a new instance of the build-disk subcommand and appends it to
// the root command. requireRoot is to initiate it with or without the CheckRoot
// pre-run check. This method is mostly used for testing purposes.
func NewBuildDisk(root *cobra.Command, addCheckRoot bool) *cobra.Command {
c := &cobra.Command{
Use: "build-disk image",
Short: "Build a disk image using the given image (experimental and subject to change)",
Args: cobra.MaximumNArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
if addCheckRoot {
return CheckRoot()
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) (err error) {
var cfg *v1.BuildConfig
var spec *v1.Disk
var imgSource *v1.ImageSource

defer func() {
if cfg != nil && err != nil {
cfg.Logger.Errorf("Woophs, something went terribly wrong: %s", err)
}
}()

path, err := exec.LookPath("mount")
if err != nil {
return err
}
mounter := mount.New(path)

flags := cmd.Flags()
cfg, err = config.ReadConfigBuild(viper.GetString("config-dir"), flags, mounter)
if err != nil {
return eleError.NewFromError(err, eleError.ReadingBuildConfig)
}

err = validateCosignFlags(cfg.Logger, flags)
if err != nil {
return eleError.NewFromError(err, eleError.CosignWrongFlags)
}

// 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.ReadBuildDisk(cfg, flags)
if err != nil {
cfg.Logger.Errorf("invalid install command setup %v", err)
return eleError.NewFromError(err, eleError.ReadingBuildDiskConfig)
}

if len(args) > 0 {
imgSource, err = v1.NewSrcFromURI(args[0])
if err != nil {
cfg.Logger.Errorf("not a valid image argument: %s", args[0])
return elementalError.NewFromError(err, elementalError.IdentifySource)
}
spec.Recovery.Source = imgSource
}

// TODO add logic for an already existing output file

builder := action.NewBuildDiskAction(cfg, spec)

return builder.BuildDiskRun()
},
}
root.AddCommand(c)
imgType := newEnumFlag([]string{constants.RawType, constants.AzureType, constants.GCEType}, constants.RawType)
c.Flags().StringP("name", "n", "", "Basename of the generated disk file")
c.Flags().StringP("output", "o", "", "Output directory (defaults to current directory)")
c.Flags().Bool("date", false, "Adds a date suffix into the generated disk file")
c.Flags().Bool("expandable", false, "Creates an expandable image including only the recovery image")
c.Flags().Bool("unprivileged", false, "Makes a build runnable within a non-privileged container, avoids mounting filesystems (experimental)")
c.Flags().VarP(imgType, "type", "t", "Type of image to create")
// TODO verify if cross-arch builds make any sense
//addArchFlags(c)
addLocalImageFlag(c)
addSquashFsCompressionFlags(c)
addCosignFlags(c)
return c
}

// register the subcommand into rootCmd
var _ = NewBuildDisk(rootCmd, true)
20 changes: 20 additions & 0 deletions cmd/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,26 @@ func ReadBuildISO(b *v1.BuildConfig, flags *pflag.FlagSet) (*v1.LiveISO, error)
return iso, err
}

func ReadBuildDisk(b *v1.BuildConfig, flags *pflag.FlagSet) (*v1.Disk, error) {
disk := config.NewDisk(b)
vp := viper.Sub("disk")
if vp == nil {
vp = viper.New()
}
// Bind build-disk cmd flags
bindGivenFlags(vp, flags)
// Bind build-disk env vars
viperReadEnv(vp, "DISK", constants.GetDiskKeyEnvMap())

err := vp.Unmarshal(disk, setDecoder, decodeHook)
if err != nil {
b.Logger.Warnf("error unmarshalling Disk: %s", err)
}
err = disk.Sanitize()
b.Logger.Debugf("Loaded Disk: %s", litter.Sdump(disk))
return disk, err
}

func configLogger(log v1.Logger, vfs v1.FS) {
// Set debug level
if viper.GetBool("debug") {
Expand Down
13 changes: 13 additions & 0 deletions cmd/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,19 @@ var _ = Describe("Config", Label("config"), func() {
Expect(iso.Label).To(Equal("LIVE_LABEL"))
})
})
Describe("RawDisk spec", Label("disk"), func() {
It("initiates a RawDisk spec", func() {
disk, err := ReadBuildDisk(cfg, nil)
Expect(err).ShouldNot(HaveOccurred())

// From config file
Expect(disk.Size).To(Equal(uint(32768)))
Expect(disk.Partitions.OEM.Size).To(Equal(uint(32)))
Expect(disk.Unprivileged).To(BeTrue())
Expect(disk.Expandable).To(BeTrue())
Expect(disk.Recovery.Label).To(BeEmpty())
})
})
})
Describe("Run config", Label("run"), func() {
var flags *pflag.FlagSet
Expand Down
19 changes: 19 additions & 0 deletions cmd/config/fixtures/config/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,24 @@ iso:
- oci:recovery/cos-img
label: "LIVE_LABEL"

disk:
size: 32768
partitions:
oem:
size: 32
state:
size: 8192
recovery:
size: 2048
persistent:
size: 0
fs: xfs
unprivileged: true
expandable: true
recovery-system:
uri: some.registry.org/my/image:mytag
fs: squashfs
type: raw

name: "cOS-0"
date: true
Loading

0 comments on commit 3ea9d75

Please sign in to comment.