From d118a852b995f13fc5160acb7c95d2186adaac41 Mon Sep 17 00:00:00 2001 From: Noel Georgi Date: Thu, 29 Feb 2024 20:26:46 +0530 Subject: [PATCH] feat: implement `Install` for imager overlays Implement `Install` for imager overlays. Also add support for generating installers. Depends on: #8377 Fixes: #8350 Fixes: #8351 Fixes: #8350 Signed-off-by: Noel Georgi --- .drone.jsonnet | 17 +-- Dockerfile | 7 - Makefile | 8 -- cmd/installer/cmd/imager/root.go | 41 ++++++ cmd/installer/cmd/installer/root.go | 3 +- cmd/installer/pkg/install/install.go | 96 +++++++++++--- hack/release.toml | 7 + .../pkg/runtime/v1alpha1/board/board.go | 24 +--- internal/pkg/install/install.go | 4 - pkg/imager/imager.go | 23 ++-- .../internal/overlay/executor/executor.go | 6 +- pkg/imager/out.go | 120 ++++++++++++++---- pkg/imager/overlay/executor/executor.go | 83 ++++++++++++ pkg/imager/profile/deep_copy.generated.go | 8 +- pkg/imager/profile/profile.go | 3 +- pkg/imager/quirks/quirks.go | 7 +- pkg/imager/quirks/quirks_test.go | 36 ++++++ pkg/machinery/constants/constants.go | 15 +++ pkg/machinery/overlay/adapter/adapter.go | 10 +- pkg/machinery/overlay/overlay.go | 20 +-- 20 files changed, 400 insertions(+), 138 deletions(-) create mode 100644 pkg/imager/overlay/executor/executor.go diff --git a/.drone.jsonnet b/.drone.jsonnet index 7c153d8a38..b5015858e4 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -640,7 +640,6 @@ local integration_qemu_csi = Step('e2e-csi', target='e2e-qemu', privileged=true, }); local integration_images = Step('images', target='images', depends_on=[load_artifacts], environment={ IMAGE_REGISTRY: local_registry }); -local integration_sbcs = Step('sbcs', target='sbcs', depends_on=[integration_images], environment={ IMAGE_REGISTRY: local_registry }); local integration_cloud_images = Step('cloud-images', depends_on=[integration_images], environment=creds_env_vars); local integration_reproducibility_test = Step('reproducibility-test', target='reproducibility-test', depends_on=[load_artifacts], environment={ IMAGE_REGISTRY: local_registry }); @@ -704,7 +703,7 @@ local integration_pipelines = [ Pipeline('integration-qemu-encrypted-vip', default_pipeline_steps + [integration_qemu_encrypted_vip]) + integration_trigger(['integration-qemu-encrypted-vip']), Pipeline('integration-qemu-race', default_pipeline_steps + [build_race, integration_qemu_race]) + integration_trigger(['integration-qemu-race']), Pipeline('integration-qemu-csi', default_pipeline_steps + [integration_qemu_csi]) + integration_trigger(['integration-qemu-csi']), - Pipeline('integration-images', default_pipeline_steps + [integration_images, integration_sbcs]) + integration_trigger(['integration-images']), + Pipeline('integration-images', default_pipeline_steps + [integration_images]) + integration_trigger(['integration-images']), Pipeline('integration-reproducibility-test', default_pipeline_steps + [integration_reproducibility_test]) + integration_trigger(['integration-reproducibility']), Pipeline('integration-cloud-images', default_pipeline_steps + [integration_images, integration_cloud_images]) + literal_trigger(['integration-cloud-images']), Pipeline('image-factory', default_pipeline_steps + [ @@ -738,7 +737,7 @@ local integration_pipelines = [ Pipeline('cron-integration-qemu-encrypted-vip', default_pipeline_steps + [integration_qemu_encrypted_vip], [default_cron_pipeline]) + cron_trigger(['thrice-daily', 'nightly']), Pipeline('cron-integration-qemu-race', default_pipeline_steps + [build_race, integration_qemu_race], [default_cron_pipeline]) + cron_trigger(['nightly']), Pipeline('cron-integration-qemu-csi', default_pipeline_steps + [integration_qemu_csi], [default_cron_pipeline]) + cron_trigger(['nightly']), - Pipeline('cron-integration-images', default_pipeline_steps + [integration_images, integration_sbcs], [default_cron_pipeline]) + cron_trigger(['nightly']), + Pipeline('cron-integration-images', default_pipeline_steps + [integration_images], [default_cron_pipeline]) + cron_trigger(['nightly']), Pipeline('cron-integration-reproducibility-test', default_pipeline_steps + [integration_reproducibility_test], [default_cron_pipeline]) + cron_trigger(['nightly']), Pipeline('cron-image-factory', default_pipeline_steps + [ integration_factory_16_iso, @@ -891,7 +890,6 @@ local conformance_pipelines = [ local cloud_images = Step('cloud-images', depends_on=[e2e_docker, e2e_qemu], environment=creds_env_vars); local images = Step('images', target='images', depends_on=[iso, images_essential, save_artifacts], environment={ IMAGE_REGISTRY: local_registry }); -local sbcs = Step('sbcs', target='sbcs', depends_on=[images], environment={ IMAGE_REGISTRY: local_registry }); // TODO(andrewrynhard): We should run E2E tests on a release. local release = { @@ -921,15 +919,6 @@ local release = { '_out/metal-arm64.iso', '_out/metal-amd64.raw.xz', '_out/metal-arm64.raw.xz', - '_out/metal-rpi_generic-arm64.raw.xz', - '_out/metal-rockpi_4-arm64.raw.xz', - '_out/metal-rockpi_4c-arm64.raw.xz', - '_out/metal-rock64-arm64.raw.xz', - '_out/metal-pine64-arm64.raw.xz', - '_out/metal-bananapi_m64-arm64.raw.xz', - '_out/metal-libretech_all_h3_cc_h5-arm64.raw.xz', - '_out/metal-jetson_nano-arm64.raw.xz', - '_out/metal-nanopi_r4s-arm64.raw.xz', '_out/nocloud-amd64.raw.xz', '_out/nocloud-arm64.raw.xz', '_out/opennebula-amd64.raw.xz', @@ -973,7 +962,6 @@ local release = { cloud_images.name, talosctl_cni_bundle.name, images.name, - sbcs.name, iso.name, push.name, release_notes.name, @@ -982,7 +970,6 @@ local release = { local release_steps = default_steps + [ images, - sbcs, cloud_images, release, ]; diff --git a/Dockerfile b/Dockerfile index 8bf697e0e9..817dbab270 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,8 +32,6 @@ ARG PKG_RUNC ARG PKG_XFSPROGS ARG PKG_UTIL_LINUX ARG PKG_KMOD -ARG PKG_U_BOOT -ARG PKG_RASPBERYPI_FIRMWARE ARG PKG_KERNEL ARG PKG_TALOSCTL_CNI_BUNDLE_INSTALL @@ -114,9 +112,6 @@ FROM ${PKG_KERNEL} AS pkg-kernel FROM --platform=amd64 ${PKG_KERNEL} AS pkg-kernel-amd64 FROM --platform=arm64 ${PKG_KERNEL} AS pkg-kernel-arm64 -FROM --platform=arm64 ${PKG_U_BOOT} AS pkg-u-boot-arm64 -FROM --platform=arm64 ${PKG_RASPBERYPI_FIRMWARE} AS pkg-raspberrypi-firmware-arm64 - # Resolve package images using ${EXTRAS} to be used later in COPY --from=. FROM ${PKG_TALOSCTL_CNI_BUNDLE_INSTALL} AS extras-talosctl-cni-bundle-install @@ -780,8 +775,6 @@ COPY --from=pkg-kernel-arm64 /dtb /usr/install/arm64/dtb COPY --from=initramfs-archive-arm64 /initramfs.xz /usr/install/arm64/initramfs.xz COPY --from=pkg-sd-boot-arm64 /linuxaa64.efi.stub /usr/install/arm64/systemd-stub.efi COPY --from=pkg-sd-boot-arm64 /systemd-bootaa64.efi /usr/install/arm64/systemd-boot.efi -COPY --from=pkg-u-boot-arm64 / /usr/install/arm64/u-boot -COPY --from=pkg-raspberrypi-firmware-arm64 / /usr/install/arm64/raspberrypi-firmware FROM scratch AS install-artifacts-all COPY --from=install-artifacts-amd64 / / diff --git a/Makefile b/Makefile index 957923369d..19c512142e 100644 --- a/Makefile +++ b/Makefile @@ -45,8 +45,6 @@ PKG_RUNC ?= $(PKGS_PREFIX)/runc:$(PKGS) PKG_XFSPROGS ?= $(PKGS_PREFIX)/xfsprogs:$(PKGS) PKG_UTIL_LINUX ?= $(PKGS_PREFIX)/util-linux:$(PKGS) PKG_KMOD ?= $(PKGS_PREFIX)/kmod:$(PKGS) -PKG_U_BOOT ?= $(PKGS_PREFIX)/u-boot:$(PKGS) -PKG_RASPBERYPI_FIRMWARE ?= $(PKGS_PREFIX)/raspberrypi-firmware:$(PKGS) PKG_KERNEL ?= $(PKGS_PREFIX)/kernel:$(PKGS) PKG_TALOSCTL_CNI_BUNDLE_INSTALL ?= $(PKGS_PREFIX)/talosctl-cni-bundle-install:$(EXTRAS) @@ -376,12 +374,6 @@ images-essential: image-aws image-gcp image-metal secureboot-installer ## Builds images: image-aws image-azure image-digital-ocean image-exoscale image-gcp image-hcloud image-iso image-metal image-nocloud image-opennebula image-openstack image-oracle image-scaleway image-upcloud image-vmware image-vultr ## Builds all known images (AWS, Azure, DigitalOcean, Exoscale, GCP, HCloud, Metal, NoCloud, OpenNebula, Openstack, Oracle, Scaleway, UpCloud, Vultr and VMware). -sbc-%: ## Builds the specified SBC image. Valid options are rpi_generic, rock64, bananapi_m64, libretech_all_h3_cc_h5, rockpi_4, rockpi_4c, pine64, jetson_nano and nanopi_r4s (e.g. sbc-rpi_generic) - @docker pull $(REGISTRY_AND_USERNAME)/imager:$(IMAGE_TAG) - @docker run --rm -t -v /dev:/dev -v $(PWD)/$(ARTIFACTS):/out --network=host --privileged $(REGISTRY_AND_USERNAME)/imager:$(IMAGE_TAG) $* --arch arm64 $(IMAGER_ARGS) - -sbcs: sbc-rpi_generic sbc-rock64 sbc-bananapi_m64 sbc-libretech_all_h3_cc_h5 sbc-rockpi_4 sbc-rockpi_4c sbc-pine64 sbc-jetson_nano sbc-nanopi_r4s ## Builds all known SBC images (Raspberry Pi 4, Rock64, Banana Pi M64, Radxa ROCK Pi 4, Radxa ROCK Pi 4c, Pine64, Libre Computer Board ALL-H3-CC, Jetson Nano and Nano Pi R4S). - .PHONY: iso iso: image-iso ## Builds the ISO and outputs it to the artifact directory. diff --git a/cmd/installer/cmd/imager/root.go b/cmd/installer/cmd/imager/root.go index a94669f382..3482b2a5df 100644 --- a/cmd/installer/cmd/imager/root.go +++ b/cmd/installer/cmd/imager/root.go @@ -6,10 +6,12 @@ package imager import ( + "bytes" "context" "fmt" "os" "runtime" + "strings" "github.com/dustin/go-humanize" "github.com/siderolabs/gen/xslices" @@ -22,6 +24,7 @@ import ( "github.com/siderolabs/talos/pkg/imager" "github.com/siderolabs/talos/pkg/imager/profile" "github.com/siderolabs/talos/pkg/machinery/constants" + "github.com/siderolabs/talos/pkg/machinery/overlay" "github.com/siderolabs/talos/pkg/reporter" ) @@ -39,6 +42,7 @@ var cmdFlags struct { TarToStdout bool OverlayName string OverlayImage string + OverlayOptions []string } // rootCmd represents the base command when called without any subcommands. @@ -76,12 +80,47 @@ var rootCmd = &cobra.Command{ }, } + extraOverlayOptions := overlay.ExtraOptions{} + + for _, option := range cmdFlags.OverlayOptions { + if strings.HasPrefix(option, "@") { + data, err := os.ReadFile(option[1:]) + if err != nil { + return err + } + + decoder := yaml.NewDecoder(bytes.NewReader(data)) + decoder.KnownFields(true) + + if err := decoder.Decode(&extraOverlayOptions); err != nil { + return err + } + + continue + + } + + k, v, _ := strings.Cut(option, "=") + + if strings.HasPrefix(v, "@") { + data, err := os.ReadFile(v[1:]) + if err != nil { + return err + } + + v = string(data) + } + + extraOverlayOptions[k] = v + } + if cmdFlags.OverlayName != "" || cmdFlags.OverlayImage != "" { prof.Overlay = &profile.OverlayOptions{ Name: cmdFlags.OverlayName, Image: profile.ContainerAsset{ ImageRef: cmdFlags.OverlayImage, }, + ExtraOptions: extraOverlayOptions, } } @@ -176,6 +215,8 @@ func init() { rootCmd.PersistentFlags().BoolVar(&cmdFlags.TarToStdout, "tar-to-stdout", false, "Tar output and send to stdout") rootCmd.PersistentFlags().StringVar(&cmdFlags.OverlayName, "overlay-name", "", "The name of the overlay to use") rootCmd.PersistentFlags().StringVar(&cmdFlags.OverlayImage, "overlay-image", "", "The image reference to the overlay") + rootCmd.PersistentFlags().StringArrayVar(&cmdFlags.OverlayOptions, "overlay-option", []string{}, "Extra options to pass to the overlay") rootCmd.MarkFlagsMutuallyExclusive("board", "overlay-name") rootCmd.MarkFlagsMutuallyExclusive("board", "overlay-image") + rootCmd.MarkFlagsMutuallyExclusive("board", "overlay-option") } diff --git a/cmd/installer/cmd/installer/root.go b/cmd/installer/cmd/installer/root.go index 81fa02136d..ae150b325d 100644 --- a/cmd/installer/cmd/installer/root.go +++ b/cmd/installer/cmd/installer/root.go @@ -52,13 +52,12 @@ var options = &install.Options{} var bootloader bool -//nolint:goconst func init() { rootCmd.PersistentFlags().StringVar(&options.ConfigSource, "config", "", "The value of "+constants.KernelParamConfig) rootCmd.PersistentFlags().StringVar(&options.Disk, "disk", "", "The path to the disk to install to") rootCmd.PersistentFlags().StringVar(&options.Platform, "platform", "", "The value of "+constants.KernelParamPlatform) rootCmd.PersistentFlags().StringVar(&options.Arch, "arch", runtime.GOARCH, "The target architecture") - rootCmd.PersistentFlags().StringVar(&options.Board, "board", constants.BoardNone, "The value of "+constants.KernelParamBoard) + rootCmd.PersistentFlags().StringVar(&options.Board, "board", constants.BoardNone, "Deprecated: no op") rootCmd.PersistentFlags().StringArrayVar(&options.ExtraKernelArgs, "extra-kernel-arg", []string{}, "Extra argument to pass to the kernel") rootCmd.PersistentFlags().BoolVar(&bootloader, "bootloader", true, "Deprecated: no op") rootCmd.PersistentFlags().BoolVar(&options.Upgrade, "upgrade", false, "Indicates that the install is being performed by an upgrade") diff --git a/cmd/installer/pkg/install/install.go b/cmd/installer/pkg/install/install.go index 4fb09f6894..262d98e7ae 100644 --- a/cmd/installer/pkg/install/install.go +++ b/cmd/installer/pkg/install/install.go @@ -5,17 +5,20 @@ package install import ( + "bytes" "context" "errors" "fmt" "log" "os" + "path/filepath" "syscall" "time" "github.com/siderolabs/go-blockdevice/blockdevice" "github.com/siderolabs/go-procfs/procfs" "github.com/siderolabs/go-retry/retry" + "gopkg.in/yaml.v3" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/board" @@ -23,24 +26,29 @@ import ( bootloaderoptions "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/options" "github.com/siderolabs/talos/internal/pkg/meta" "github.com/siderolabs/talos/internal/pkg/mount" + "github.com/siderolabs/talos/pkg/imager/overlay/executor" "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/machinery/kernel" + "github.com/siderolabs/talos/pkg/machinery/overlay" "github.com/siderolabs/talos/pkg/machinery/version" ) // Options represents the set of options available for an install. type Options struct { - ConfigSource string - Disk string - Platform string - Arch string - Board string - ExtraKernelArgs []string - Upgrade bool - Force bool - Zero bool - LegacyBIOSSupport bool - MetaValues MetaValues + ConfigSource string + Disk string + Platform string + Arch string + Board string + ExtraKernelArgs []string + Upgrade bool + Force bool + Zero bool + LegacyBIOSSupport bool + MetaValues MetaValues + OverlayInstaller overlay.Installer[overlay.ExtraOptions] + OverlayExtractedDir string + ExtraOptions overlay.ExtraOptions // Options specific for the image creation mode. ImageSecureboot bool @@ -68,7 +76,15 @@ func (m Mode) IsImage() bool { } // Install installs Talos. -func Install(ctx context.Context, p runtime.Platform, mode Mode, opts *Options) (err error) { +// +//nolint:gocyclo +func Install(ctx context.Context, p runtime.Platform, mode Mode, opts *Options) error { + overlayPresent := overlayPresent() + + if b := getBoard(); b != constants.BoardNone && !overlayPresent { + return fmt.Errorf("using standard installer image is not supported for board: %s, use an installer with overlay", b) + } + cmdline := procfs.NewCmdline("") cmdline.Append(constants.KernelParamPlatform, p.Name()) @@ -79,7 +95,7 @@ func Install(ctx context.Context, p runtime.Platform, mode Mode, opts *Options) cmdline.SetAll(p.KernelArgs().Strings()) // first defaults, then extra kernel args to allow extra kernel args to override defaults - if err = cmdline.AppendAll(kernel.DefaultArgs); err != nil { + if err := cmdline.AppendAll(kernel.DefaultArgs); err != nil { return err } @@ -91,7 +107,7 @@ func Install(ctx context.Context, p runtime.Platform, mode Mode, opts *Options) var b runtime.Board - b, err = board.NewBoard(opts.Board) + b, err := board.NewBoard(opts.Board) if err != nil { return err } @@ -101,7 +117,7 @@ func Install(ctx context.Context, p runtime.Platform, mode Mode, opts *Options) cmdline.SetAll(b.KernelArgs().Strings()) } - if err = cmdline.AppendAll( + if err := cmdline.AppendAll( opts.ExtraKernelArgs, procfs.WithOverwriteArgs("console"), procfs.WithOverwriteArgs(constants.KernelParamPlatform), @@ -110,6 +126,25 @@ func Install(ctx context.Context, p runtime.Platform, mode Mode, opts *Options) return err } + if overlayPresent { + extraOptionsBytes, err := os.ReadFile(constants.ImagerOverlayExtraOptionsPath) + if err != nil { + return err + } + + var extraOptions overlay.ExtraOptions + + decoder := yaml.NewDecoder(bytes.NewReader(extraOptionsBytes)) + decoder.KnownFields(true) + + if err := decoder.Decode(&extraOptions); err != nil { + return fmt.Errorf("failed to decode extra options: %w", err) + } + + opts.OverlayInstaller = executor.New(constants.ImagerOverlayInstallerDefaultPath) + opts.ExtraOptions = extraOptions + } + i, err := NewInstaller(ctx, cmdline, mode, opts) if err != nil { return err @@ -302,6 +337,17 @@ func (i *Installer) Install(ctx context.Context, mode Mode) (err error) { } } + if i.options.OverlayInstaller != nil { + if err = i.options.OverlayInstaller.Install(overlay.InstallOptions[overlay.ExtraOptions]{ + InstallDisk: i.options.Disk, + MountPrefix: i.options.MountPrefix, + ArtifactsPath: filepath.Join(i.options.OverlayExtractedDir, constants.ImagerOverlayArtifactsPath), + ExtraOptions: i.options.ExtraOptions, + }); err != nil { + return err + } + } + if mode == ModeUpgrade || len(i.options.MetaValues.values) > 0 { var ( metaState *meta.Meta @@ -394,3 +440,23 @@ func retryBlockdeviceOpen(device string) (*blockdevice.BlockDevice, error) { return bd, err } + +func overlayPresent() bool { + _, err := os.Stat(constants.ImagerOverlayInstallerDefaultPath) + + return err == nil +} + +func getBoard() string { + cmdline := procfs.ProcCmdline() + if cmdline == nil { + return constants.BoardNone + } + + board := cmdline.Get(constants.KernelParamBoard) + if board == nil { + return constants.BoardNone + } + + return *board.First() +} diff --git a/hack/release.toml b/hack/release.toml index 9eacf3db2a..32a4e1dece 100644 --- a/hack/release.toml +++ b/hack/release.toml @@ -122,6 +122,13 @@ machine: kubespan: harvestExtraEndpoints: true ``` +""" + + [notes.sbc] + title = "SBC" + description = """\ +Talos core will drop support for SBC's and will not include the SBC binaries in the release. +*Overlays* are being developed to support SBC's. """ [notes.syslog] diff --git a/internal/app/machined/pkg/runtime/v1alpha1/board/board.go b/internal/app/machined/pkg/runtime/v1alpha1/board/board.go index 743655caa7..0d0641e445 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/board/board.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/board/board.go @@ -6,11 +6,7 @@ package board import ( - "errors" "fmt" - "os" - - "github.com/siderolabs/go-procfs/procfs" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime" bananapim64 "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/board/bananapi_m64" @@ -25,26 +21,8 @@ import ( "github.com/siderolabs/talos/pkg/machinery/constants" ) -// CurrentBoard is a helper func for discovering the current board. -func CurrentBoard() (b runtime.Board, err error) { - var board string - - if p := procfs.ProcCmdline().Get(constants.KernelParamBoard).First(); p != nil { - board = *p - } - - if p, ok := os.LookupEnv("BOARD"); ok { - board = p - } - - if board == "" { - return nil, errors.New("failed to determine board") - } - - return newBoard(board) -} - // NewBoard initializes and returns a runtime.Board. +// Deprecated: Not supported anymore, use overlays instead. func NewBoard(board string) (b runtime.Board, err error) { return newBoard(board) } diff --git a/internal/pkg/install/install.go b/internal/pkg/install/install.go index 25b7ca3458..fe14311002 100644 --- a/internal/pkg/install/install.go +++ b/internal/pkg/install/install.go @@ -173,10 +173,6 @@ func RunInstallerContainer(disk, platform, ref string, cfg configcore.Config, cf "--zero=" + zero, } - if c := procfs.ProcCmdline().Get(constants.KernelParamBoard).First(); c != nil { - args = append(args, "--board="+*c) - } - for _, arg := range options.ExtraKernelArgs { args = append(args, "--extra-kernel-arg", arg) } diff --git a/pkg/imager/imager.go b/pkg/imager/imager.go index 4b5528136e..f3e9913110 100644 --- a/pkg/imager/imager.go +++ b/pkg/imager/imager.go @@ -22,7 +22,7 @@ import ( "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform" "github.com/siderolabs/talos/internal/pkg/secureboot/uki" "github.com/siderolabs/talos/pkg/imager/extensions" - "github.com/siderolabs/talos/pkg/imager/internal/overlay/executor" + "github.com/siderolabs/talos/pkg/imager/overlay/executor" "github.com/siderolabs/talos/pkg/imager/profile" "github.com/siderolabs/talos/pkg/imager/quirks" "github.com/siderolabs/talos/pkg/imager/utils" @@ -38,7 +38,7 @@ import ( type Imager struct { prof profile.Profile - overlayInstaller overlay.Installer + overlayInstaller overlay.Installer[overlay.ExtraOptions] tempDir string @@ -170,7 +170,7 @@ func (i *Imager) handleOverlay(ctx context.Context, report *reporter.Reporter) e return nil } - tempOverlayPath := filepath.Join(i.tempDir, "overlay") + tempOverlayPath := filepath.Join(i.tempDir, constants.ImagerOverlayBasePath) if err := os.MkdirAll(tempOverlayPath, 0o755); err != nil { return fmt.Errorf("failed to create overlay directory: %w", err) @@ -180,19 +180,17 @@ func (i *Imager) handleOverlay(ctx context.Context, report *reporter.Reporter) e return err } - // find all *.yaml files in the tempOverlayPath/profiles/ directory - profileYAMLs, err := filepath.Glob(filepath.Join(tempOverlayPath, "profiles", "*.yaml")) + // find all *.yaml files in the overlay/profiles/ directory + profileYAMLs, err := filepath.Glob(filepath.Join(i.tempDir, constants.ImagerOverlayProfilesPath, "*.yaml")) if err != nil { return fmt.Errorf("failed to find profiles: %w", err) } - installerName := i.prof.Overlay.Name - - if installerName == "" { - installerName = "default" + if i.prof.Overlay.Name == "" { + i.prof.Overlay.Name = constants.ImagerOverlayInstallerDefault } - i.overlayInstaller = executor.New(filepath.Join(tempOverlayPath, "installers", installerName)) + i.overlayInstaller = executor.New(filepath.Join(i.tempDir, constants.ImagerOverlayInstallersPath, i.prof.Overlay.Name)) for _, profilePath := range profileYAMLs { profileName := strings.TrimSuffix(filepath.Base(profilePath), ".yaml") @@ -327,8 +325,7 @@ func (i *Imager) buildCmdline() error { cmdline.SetAll(p.KernelArgs().Strings()) // board kernel args - // TODO: check if supports overlay quirk - if i.prof.Board != "" { + if i.prof.Board != "" && !quirks.New(i.prof.Version).SupportsOverlay() { var b talosruntime.Board b, err = board.NewBoard(i.prof.Board) @@ -342,7 +339,7 @@ func (i *Imager) buildCmdline() error { // overlay kernel args if i.overlayInstaller != nil { - options, optsErr := i.overlayInstaller.GetOptions(i.prof.Overlay.Options) + options, optsErr := i.overlayInstaller.GetOptions(i.prof.Overlay.ExtraOptions) if optsErr != nil { return optsErr } diff --git a/pkg/imager/internal/overlay/executor/executor.go b/pkg/imager/internal/overlay/executor/executor.go index c6e72ff7be..9dc0f02ffb 100644 --- a/pkg/imager/internal/overlay/executor/executor.go +++ b/pkg/imager/internal/overlay/executor/executor.go @@ -16,7 +16,7 @@ import ( "github.com/siderolabs/talos/pkg/machinery/overlay" ) -var _ overlay.Installer = (*Options)(nil) +var _ overlay.Installer[overlay.ExtraOptions] = (*Options)(nil) // Options executor options. type Options struct { @@ -31,7 +31,7 @@ func New(commandPath string) *Options { } // GetOptions returns the options for the overlay installer. -func (o *Options) GetOptions(extra overlay.InstallExtraOptions) (overlay.Options, error) { +func (o *Options) GetOptions(extra overlay.ExtraOptions) (overlay.Options, error) { // parse extra as yaml extraYAML, err := yaml.Marshal(extra) if err != nil { @@ -53,7 +53,7 @@ func (o *Options) GetOptions(extra overlay.InstallExtraOptions) (overlay.Options } // Install installs the overlay. -func (o *Options) Install(options overlay.InstallOptions) error { +func (o *Options) Install(options overlay.InstallOptions[overlay.ExtraOptions]) error { optionsBytes, err := yaml.Marshal(&options) if err != nil { return fmt.Errorf("failed to marshal options: %w", err) diff --git a/pkg/imager/out.go b/pkg/imager/out.go index 4755f78a52..0b427f136e 100644 --- a/pkg/imager/out.go +++ b/pkg/imager/out.go @@ -23,6 +23,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/types" "github.com/siderolabs/go-pointer" "github.com/siderolabs/go-procfs/procfs" + "gopkg.in/yaml.v3" "github.com/siderolabs/talos/cmd/installer/pkg/install" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/options" @@ -33,6 +34,7 @@ import ( "github.com/siderolabs/talos/pkg/imager/ova" "github.com/siderolabs/talos/pkg/imager/profile" "github.com/siderolabs/talos/pkg/imager/qemuimg" + "github.com/siderolabs/talos/pkg/imager/quirks" "github.com/siderolabs/talos/pkg/imager/utils" "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/reporter" @@ -282,6 +284,12 @@ func (i *Imager) buildImage(ctx context.Context, path string, printf func(string Printf: printf, } + if i.overlayInstaller != nil { + opts.OverlayInstaller = i.overlayInstaller + opts.ExtraOptions = i.prof.Overlay.ExtraOptions + opts.OverlayExtractedDir = i.tempDir + } + if opts.Board == "" { opts.Board = constants.BoardNone } @@ -298,7 +306,7 @@ func (i *Imager) buildImage(ctx context.Context, path string, printf func(string return nil } -//nolint:gocyclo +//nolint:gocyclo,cyclop func (i *Imager) outInstaller(ctx context.Context, path string, report *reporter.Reporter) error { printf := progressPrintf(report, reporter.Update{Message: "building installer...", Status: reporter.StatusRunning}) @@ -374,35 +382,37 @@ func (i *Imager) outInstaller(ctx context.Context, path string, report *reporter ) } - for _, extraArtifact := range []struct { - sourcePath string - imagePath string - }{ - { - sourcePath: i.prof.Input.DTB.Path, - imagePath: strings.TrimLeft(fmt.Sprintf(constants.DTBAssetPath, i.prof.Arch), "/"), - }, - { - sourcePath: i.prof.Input.UBoot.Path, - imagePath: strings.TrimLeft(fmt.Sprintf(constants.UBootAssetPath, i.prof.Arch), "/"), - }, - { - sourcePath: i.prof.Input.RPiFirmware.Path, - imagePath: strings.TrimLeft(fmt.Sprintf(constants.RPiFirmwareAssetPath, i.prof.Arch), "/"), - }, - } { - if extraArtifact.sourcePath == "" { - continue - } + if !quirks.New(i.prof.Version).SupportsOverlay() { + for _, extraArtifact := range []struct { + sourcePath string + imagePath string + }{ + { + sourcePath: i.prof.Input.DTB.Path, + imagePath: strings.TrimLeft(fmt.Sprintf(constants.DTBAssetPath, i.prof.Arch), "/"), + }, + { + sourcePath: i.prof.Input.UBoot.Path, + imagePath: strings.TrimLeft(fmt.Sprintf(constants.UBootAssetPath, i.prof.Arch), "/"), + }, + { + sourcePath: i.prof.Input.RPiFirmware.Path, + imagePath: strings.TrimLeft(fmt.Sprintf(constants.RPiFirmwareAssetPath, i.prof.Arch), "/"), + }, + } { + if extraArtifact.sourcePath == "" { + continue + } - var extraFiles []filemap.File + var extraFiles []filemap.File - extraFiles, err = filemap.Walk(extraArtifact.sourcePath, extraArtifact.imagePath) - if err != nil { - return fmt.Errorf("failed to walk extra artifact %s: %w", extraArtifact.sourcePath, err) - } + extraFiles, err = filemap.Walk(extraArtifact.sourcePath, extraArtifact.imagePath) + if err != nil { + return fmt.Errorf("failed to walk extra artifact %s: %w", extraArtifact.sourcePath, err) + } - artifacts = append(artifacts, extraFiles...) + artifacts = append(artifacts, extraFiles...) + } } artifactsLayer, err := filemap.Layer(artifacts) @@ -415,6 +425,62 @@ func (i *Imager) outInstaller(ctx context.Context, path string, report *reporter return fmt.Errorf("failed to append artifacts layer: %w", err) } + if i.overlayInstaller != nil { + extraOpts, internalErr := yaml.Marshal(i.prof.Overlay.ExtraOptions) + if internalErr != nil { + return fmt.Errorf("failed to marshal extra options: %w", internalErr) + } + + if internalErr = os.WriteFile(filepath.Join(i.tempDir, constants.ImagerOverlayExtraOptionsPath), extraOpts, 0o644); internalErr != nil { + return fmt.Errorf("failed to write extra options yaml: %w", internalErr) + } + + printf("generating overlay installer layer") + + var overlayArtifacts []filemap.File + + for _, extraArtifact := range []struct { + sourcePath string + imagePath string + }{ + { + sourcePath: filepath.Join(i.tempDir, constants.ImagerOverlayArtifactsPath), + imagePath: constants.ImagerOverlayArtifactsPath, + }, + { + sourcePath: filepath.Join(i.tempDir, constants.ImagerOverlayInstallersPath, i.prof.Overlay.Name), + imagePath: constants.ImagerOverlayInstallerDefaultPath, + }, + { + sourcePath: filepath.Join(i.tempDir, constants.ImagerOverlayExtraOptionsPath), + imagePath: constants.ImagerOverlayExtraOptionsPath, + }, + } { + if extraArtifact.sourcePath == "" { + continue + } + + var extraFiles []filemap.File + + extraFiles, err = filemap.Walk(extraArtifact.sourcePath, extraArtifact.imagePath) + if err != nil { + return fmt.Errorf("failed to walk extra artifact %s: %w", extraArtifact.sourcePath, err) + } + + overlayArtifacts = append(overlayArtifacts, extraFiles...) + } + + overlayArtifactsLayer, internalErr := filemap.Layer(overlayArtifacts) + if internalErr != nil { + return fmt.Errorf("failed to create overlay artifacts layer: %w", internalErr) + } + + newInstallerImg, internalErr = mutate.AppendLayers(newInstallerImg, overlayArtifactsLayer) + if internalErr != nil { + return fmt.Errorf("failed to append overlay artifacts layer: %w", internalErr) + } + } + ref, err := name.ParseReference(i.prof.Input.BaseInstaller.ImageRef) if err != nil { return fmt.Errorf("failed to parse image reference: %w", err) diff --git a/pkg/imager/overlay/executor/executor.go b/pkg/imager/overlay/executor/executor.go new file mode 100644 index 0000000000..a83380bbc6 --- /dev/null +++ b/pkg/imager/overlay/executor/executor.go @@ -0,0 +1,83 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package executor implements overlay.Installer +package executor + +import ( + "bytes" + "fmt" + "io" + "os/exec" + + "gopkg.in/yaml.v3" + + "github.com/siderolabs/talos/pkg/machinery/overlay" +) + +var _ overlay.Installer[overlay.ExtraOptions] = (*Options)(nil) + +// Options executor options. +type Options struct { + commandPath string +} + +// New returns a new overlay installer executor. +func New(commandPath string) *Options { + return &Options{ + commandPath: commandPath, + } +} + +// GetOptions returns the options for the overlay installer. +func (o *Options) GetOptions(extra overlay.ExtraOptions) (overlay.Options, error) { + // parse extra as yaml + extraYAML, err := yaml.Marshal(extra) + if err != nil { + return overlay.Options{}, fmt.Errorf("failed to marshal extra: %w", err) + } + + out, err := o.execute(bytes.NewReader(extraYAML), "get-options") + if err != nil { + return overlay.Options{}, err + } + + var options overlay.Options + + if err := yaml.Unmarshal(out, &options); err != nil { + return overlay.Options{}, fmt.Errorf("failed to unmarshal overlay options: %w", err) + } + + return options, nil +} + +// Install installs the overlay. +func (o *Options) Install(options overlay.InstallOptions[overlay.ExtraOptions]) error { + optionsBytes, err := yaml.Marshal(&options) + if err != nil { + return fmt.Errorf("failed to marshal options: %w", err) + } + + if _, err := o.execute(bytes.NewReader(optionsBytes), "install"); err != nil { + return err + } + + return nil +} + +func (o *Options) execute(stdin io.Reader, args ...string) ([]byte, error) { + cmd := exec.Command(o.commandPath, args...) + cmd.Stdin = stdin + + var stdOut, stdErr bytes.Buffer + + cmd.Stdout = &stdOut + cmd.Stderr = &stdErr + + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("failed to run overlay installer: %w, stdErr: %s", err, stdErr.Bytes()) + } + + return stdOut.Bytes(), nil +} diff --git a/pkg/imager/profile/deep_copy.generated.go b/pkg/imager/profile/deep_copy.generated.go index 0fd0bb1d6e..3717f6815f 100644 --- a/pkg/imager/profile/deep_copy.generated.go +++ b/pkg/imager/profile/deep_copy.generated.go @@ -36,10 +36,10 @@ func (o Profile) DeepCopy() Profile { if o.Overlay != nil { cp.Overlay = new(OverlayOptions) *cp.Overlay = *o.Overlay - if o.Overlay.Options != nil { - cp.Overlay.Options = make(map[string]any, len(o.Overlay.Options)) - for k4, v4 := range o.Overlay.Options { - cp.Overlay.Options[k4] = v4 + if o.Overlay.ExtraOptions != nil { + cp.Overlay.ExtraOptions = make(map[string]any, len(o.Overlay.ExtraOptions)) + for k4, v4 := range o.Overlay.ExtraOptions { + cp.Overlay.ExtraOptions[k4] = v4 } } } diff --git a/pkg/imager/profile/profile.go b/pkg/imager/profile/profile.go index 387e42436d..42806ae033 100644 --- a/pkg/imager/profile/profile.go +++ b/pkg/imager/profile/profile.go @@ -14,6 +14,7 @@ import ( "gopkg.in/yaml.v3" "github.com/siderolabs/talos/pkg/machinery/meta" + "github.com/siderolabs/talos/pkg/machinery/overlay" ) //go:generate deep-copy -type Profile -header-file ../../../hack/boilerplate.txt -o deep_copy.generated.go . @@ -50,7 +51,7 @@ type OverlayOptions struct { // Image to use for the overlay. Image ContainerAsset `yaml:"image"` // Options for the overlay. - Options map[string]any `yaml:"options,omitempty"` + overlay.ExtraOptions `yaml:"options,omitempty"` } // CustomizationProfile describes customizations that can be applied to the image. diff --git a/pkg/imager/quirks/quirks.go b/pkg/imager/quirks/quirks.go index 9a31ceeb99..dab6962614 100644 --- a/pkg/imager/quirks/quirks.go +++ b/pkg/imager/quirks/quirks.go @@ -19,7 +19,12 @@ func New(talosVersion string) Quirks { return Quirks{} } - return Quirks{v: &v} + // we only care about major, minor, and patch, so that alpha, beta, etc. are ignored + return Quirks{v: &semver.Version{ + Major: v.Major, + Minor: v.Minor, + Patch: v.Patch, + }} } var minVersionResetOption = semver.MustParse("1.4.0") diff --git a/pkg/imager/quirks/quirks_test.go b/pkg/imager/quirks/quirks_test.go index fbf85a9dc6..f55f3a925b 100644 --- a/pkg/imager/quirks/quirks_test.go +++ b/pkg/imager/quirks/quirks_test.go @@ -63,3 +63,39 @@ func TestSupportsCompressedEncodedMETA(t *testing.T) { }) } } + +func TestSupportsOverlay(t *testing.T) { + for _, test := range []struct { + version string + + expected bool + }{ + { + version: "1.6.3", + expected: false, + }, + { + version: "1.7.0", + expected: true, + }, + { + expected: true, + }, + { + version: "1.6.2", + expected: false, + }, + { + version: "1.7.0-alpha.0", + expected: true, + }, + { + version: "v1.7.0-alpha.0-75-gff08e2821", + expected: true, + }, + } { + t.Run(test.version, func(t *testing.T) { + assert.Equal(t, test.expected, quirks.New(test.version).SupportsOverlay()) + }) + } +} diff --git a/pkg/machinery/constants/constants.go b/pkg/machinery/constants/constants.go index 79cf3468eb..5b1d6f369f 100644 --- a/pkg/machinery/constants/constants.go +++ b/pkg/machinery/constants/constants.go @@ -574,6 +574,21 @@ const ( // RPiFirmwareAssetPath is the path to the raspberrypi firmware in the installer. RPiFirmwareAssetPath = "/usr/install/%s/raspberrypi-firmware" + // ImagerOverlayBasePath is the base path for the imager overlay. + ImagerOverlayBasePath = "/overlay" + // ImagerOverlayArtifactsPath is the path to the artifacts in the imager overlay. + ImagerOverlayArtifactsPath = ImagerOverlayBasePath + "/" + "artifacts" + // ImagerOverlayInstallersPath is the path to the installers in the imager overlay. + ImagerOverlayInstallersPath = ImagerOverlayBasePath + "/" + "installers" + // ImagerOverlayProfilesPath is the path to the profiles in the imager overlay. + ImagerOverlayProfilesPath = ImagerOverlayBasePath + "/" + "profiles" + // ImagerOverlayInstallerDefault is the default installer name. + ImagerOverlayInstallerDefault = "default" + // ImagerOverlayInstallerDefaultPath is the path to the default installer in the imager overlay. + ImagerOverlayInstallerDefaultPath = ImagerOverlayInstallersPath + "/" + ImagerOverlayInstallerDefault + // ImagerOverlayExtraOptionsPath is the path to the generated extra options file in the imager overlay. + ImagerOverlayExtraOptionsPath = ImagerOverlayBasePath + "/" + "extra-options" + // PlatformKeyAsset defines a well known name for the platform key filename used for auto-enrolling. PlatformKeyAsset = "PK.auth" diff --git a/pkg/machinery/overlay/adapter/adapter.go b/pkg/machinery/overlay/adapter/adapter.go index 2880d9c0db..6e74ed5b67 100644 --- a/pkg/machinery/overlay/adapter/adapter.go +++ b/pkg/machinery/overlay/adapter/adapter.go @@ -15,7 +15,7 @@ import ( ) // Execute executes the overlay installer. -func Execute(installer overlay.Installer) { +func Execute[T any](installer overlay.Installer[T]) { if len(os.Args) < 2 { fmt.Fprint(os.Stderr, "missing command") @@ -34,8 +34,8 @@ func Execute(installer overlay.Installer) { } } -func getOptions(installer overlay.Installer) { - var opts overlay.InstallExtraOptions +func getOptions[T any](installer overlay.Installer[T]) { + var opts T withErrorHandler(yaml.NewDecoder(os.Stdin).Decode(&opts)) @@ -49,8 +49,8 @@ func getOptions(installer overlay.Installer) { withErrorHandler(yaml.NewEncoder(os.Stdout).Encode(opt)) } -func install(installer overlay.Installer) { - var opts overlay.InstallOptions +func install[T any](installer overlay.Installer[T]) { + var opts overlay.InstallOptions[T] withErrorHandler(yaml.NewDecoder(os.Stdin).Decode(&opts)) diff --git a/pkg/machinery/overlay/overlay.go b/pkg/machinery/overlay/overlay.go index c5828ac872..671eb5ba68 100644 --- a/pkg/machinery/overlay/overlay.go +++ b/pkg/machinery/overlay/overlay.go @@ -6,9 +6,9 @@ package overlay // Installer is an interface for overlay installers. -type Installer interface { - GetOptions(extra InstallExtraOptions) (Options, error) - Install(options InstallOptions) error +type Installer[T any] interface { + GetOptions(extra T) (Options, error) + Install(options InstallOptions[T]) error } // Options for the overlay installer. @@ -21,12 +21,12 @@ type Options struct { } // InstallOptions for the overlay installer. -type InstallOptions struct { - InstallDisk string `yaml:"installDisk"` - MountPrefix string `yaml:"mountPrefix"` - ArtifactsPath string `yaml:"artifactsPath"` - ExtraOptions InstallExtraOptions `yaml:"extraOptions,omitempty"` +type InstallOptions[T any] struct { + InstallDisk string `yaml:"installDisk"` + MountPrefix string `yaml:"mountPrefix"` + ArtifactsPath string `yaml:"artifactsPath"` + ExtraOptions T `yaml:"extraOptions,omitempty"` } -// InstallExtraOptions for the overlay installer. -type InstallExtraOptions map[string]any +// ExtraOptions for the overlay installer. +type ExtraOptions map[string]any