mirror of
https://github.com/siderolabs/image-factory.git
synced 2025-12-06 10:01:15 +01:00
This appends a "virtual" (built on the fly) extension which contains
flavor ID to all boot assets of Talos.
This allows to easily identify which flavor of Talos which asset was
built with.
E.g.:
```
$ talosctl -n 172.20.0.2 get extensions -i
NODE NAMESPACE TYPE ID VERSION NAME VERSION
runtime ExtensionStatus 0 1 flavor 376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba
```
```yaml
node:
metadata:
namespace: runtime
type: ExtensionStatuses.runtime.talos.dev
id: 0
version: 1
owner: runtime.ExtensionStatusController
phase: running
created: 2023-09-07T14:06:03Z
updated: 2023-09-07T14:06:03Z
spec:
image: 0.sqsh
metadata:
name: flavor
version: 376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba
author: Image Service
description: Virtual extension which specifies the flavor of the image built with Image Service.
compatibility:
talos:
version: '>= 1.0.0'
```
And (as an empty file):
```
$ talosctl -n 172.20.0.2 ls /usr/local/share/flavor/
NODE NAME
172.20.0.2 .
172.20.0.2 376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba
```
Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
259 lines
7.4 KiB
Go
259 lines
7.4 KiB
Go
// 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 profile implements handling of Talos profiles.
|
|
package profile
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/siderolabs/gen/xerrors"
|
|
"github.com/siderolabs/go-pointer"
|
|
"github.com/siderolabs/talos/pkg/imager/profile"
|
|
"github.com/siderolabs/talos/pkg/machinery/constants"
|
|
|
|
"github.com/siderolabs/image-service/internal/artifacts"
|
|
"github.com/siderolabs/image-service/pkg/flavor"
|
|
)
|
|
|
|
// InvalidErrorTag tags errors related to invalid profiles.
|
|
type InvalidErrorTag struct{}
|
|
|
|
// parsePlatformArch parses platform-arch string into the profile.
|
|
//
|
|
// Supported formats:
|
|
// - metal-amd64
|
|
// - aws-arm64-secureboot
|
|
// - metal-rpi_generic-arm64.
|
|
func parsePlatformArch(s string, prof *profile.Profile) error {
|
|
s, ok := strings.CutSuffix(s, "-secureboot")
|
|
if ok {
|
|
prof.SecureBoot = pointer.To(true)
|
|
}
|
|
|
|
platform, rest, ok := strings.Cut(s, "-")
|
|
if !ok {
|
|
return xerrors.NewTaggedf[InvalidErrorTag]("invalid platform-arch: %q", s)
|
|
}
|
|
|
|
prof.Platform = platform
|
|
|
|
if platform == constants.PlatformMetal && strings.HasSuffix(rest, "-"+string(artifacts.ArchArm64)) {
|
|
// arm64 metal images might be "board" images
|
|
prof.Board, rest, _ = strings.Cut(rest, "-")
|
|
}
|
|
|
|
return parseArch(rest, prof)
|
|
}
|
|
|
|
func parseArch(s string, prof *profile.Profile) error {
|
|
switch artifacts.Arch(s) {
|
|
case artifacts.ArchAmd64, artifacts.ArchArm64:
|
|
prof.Arch = s
|
|
|
|
return nil
|
|
default:
|
|
return xerrors.NewTaggedf[InvalidErrorTag]("invalid architecture: %q", s)
|
|
}
|
|
}
|
|
|
|
// ParseFromPath parses imager profile from the file path.
|
|
//
|
|
//nolint:gocognit,gocyclo,cyclop
|
|
func ParseFromPath(path string) (profile.Profile, error) {
|
|
var prof profile.Profile
|
|
|
|
// kernel-<arch>
|
|
if rest, ok := strings.CutPrefix(path, "kernel-"); ok {
|
|
prof.Output.Kind = profile.OutKindKernel
|
|
prof.Output.OutFormat = profile.OutFormatRaw
|
|
prof.Platform = constants.PlatformMetal // doesn't matter for kernel output
|
|
|
|
if err := parseArch(rest, &prof); err != nil {
|
|
return prof, err
|
|
}
|
|
|
|
return prof, nil
|
|
}
|
|
|
|
// cmdline-<platform>-<arch>
|
|
if rest, ok := strings.CutPrefix(path, "cmdline-"); ok {
|
|
prof.Output.Kind = profile.OutKindCmdline
|
|
prof.Output.OutFormat = profile.OutFormatRaw
|
|
|
|
if err := parsePlatformArch(rest, &prof); err != nil {
|
|
return prof, err
|
|
}
|
|
|
|
return prof, nil
|
|
}
|
|
|
|
// initramfs-<arch>.xz
|
|
if rest, ok := strings.CutPrefix(path, "initramfs-"); ok {
|
|
if rest, ok = strings.CutSuffix(rest, ".xz"); ok {
|
|
prof.Output.Kind = profile.OutKindInitramfs
|
|
prof.Output.OutFormat = profile.OutFormatRaw
|
|
prof.Platform = constants.PlatformMetal // doesn't matter for initramfs output
|
|
|
|
if err := parseArch(rest, &prof); err != nil {
|
|
return prof, err
|
|
}
|
|
|
|
return prof, nil
|
|
}
|
|
}
|
|
|
|
// <platform>-<arch>.iso
|
|
if rest, ok := strings.CutSuffix(path, ".iso"); ok {
|
|
prof.Output.Kind = profile.OutKindISO
|
|
prof.Output.OutFormat = profile.OutFormatRaw
|
|
|
|
if err := parsePlatformArch(rest, &prof); err != nil {
|
|
return prof, err
|
|
}
|
|
|
|
return prof, nil
|
|
}
|
|
|
|
// <platform>-<arch>-secureboot-uki.efi
|
|
if rest, ok := strings.CutSuffix(path, "-uki.efi"); ok {
|
|
prof.Output.Kind = profile.OutKindUKI
|
|
prof.Output.OutFormat = profile.OutFormatRaw
|
|
|
|
if err := parsePlatformArch(rest, &prof); err != nil {
|
|
return prof, err
|
|
}
|
|
|
|
return prof, nil
|
|
}
|
|
|
|
// installer-<arch>[-secureboot].tar
|
|
if rest, ok := strings.CutPrefix(path, "installer-"); ok {
|
|
if rest, ok = strings.CutSuffix(rest, ".tar"); ok {
|
|
prof.Output.Kind = profile.OutKindInstaller
|
|
prof.Output.OutFormat = profile.OutFormatRaw
|
|
prof.Platform = constants.PlatformMetal // doesn't matter for installer output
|
|
|
|
rest, ok = strings.CutSuffix(rest, "-secureboot")
|
|
if ok {
|
|
prof.SecureBoot = pointer.To(true)
|
|
}
|
|
|
|
if err := parseArch(rest, &prof); err != nil {
|
|
return prof, err
|
|
}
|
|
|
|
return prof, nil
|
|
}
|
|
}
|
|
|
|
// at this point, we assume that the path is a disk image, so we start parsing it from the end, cutting the output format suffixes
|
|
prof.Output.Kind = profile.OutKindImage
|
|
prof.Output.ImageOptions = &profile.ImageOptions{
|
|
DiskSize: profile.DefaultRAWDiskSize,
|
|
}
|
|
|
|
// first, cut output format: .tar.gz, .gz, .xz (otherwise it's raw uncompressed)
|
|
prof.Output.OutFormat = profile.OutFormatRaw
|
|
|
|
for _, outFormat := range []profile.OutFormat{
|
|
profile.OutFormatTar,
|
|
profile.OutFormatGZ,
|
|
profile.OutFormatXZ,
|
|
} {
|
|
var ok bool
|
|
|
|
if path, ok = strings.CutSuffix(path, outFormat.String()); ok {
|
|
prof.Output.OutFormat = outFormat
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
// second, figure out the disk format
|
|
for _, diskFormat := range []profile.DiskFormat{
|
|
profile.DiskFormatRaw,
|
|
profile.DiskFormatQCOW2,
|
|
profile.DiskFormatVPC,
|
|
profile.DiskFormatOVA,
|
|
} {
|
|
var ok bool
|
|
|
|
if path, ok = strings.CutSuffix(path, "."+diskFormat.String()); ok {
|
|
prof.Output.ImageOptions.DiskFormat = diskFormat
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if prof.Output.ImageOptions.DiskFormat == profile.DiskFormatUnknown {
|
|
return prof, xerrors.NewTaggedf[InvalidErrorTag]("invalid profile path: %q", path)
|
|
}
|
|
|
|
// third, figure out the platform and arch
|
|
if err := parsePlatformArch(path, &prof); err != nil {
|
|
return prof, err
|
|
}
|
|
|
|
// last step: pull in the disk format options from the respective default profile (if any)
|
|
if defaultProfile, ok := profile.Default[prof.Platform]; ok {
|
|
if defaultProfile.Output.ImageOptions.DiskSize != 0 {
|
|
prof.Output.ImageOptions.DiskSize = defaultProfile.Output.ImageOptions.DiskSize
|
|
}
|
|
|
|
if defaultProfile.Output.ImageOptions.DiskFormatOptions != "" {
|
|
prof.Output.ImageOptions.DiskFormatOptions = defaultProfile.Output.ImageOptions.DiskFormatOptions
|
|
}
|
|
}
|
|
|
|
return prof, nil
|
|
}
|
|
|
|
// InstallerProfile returns a profile to be used for installer image.
|
|
func InstallerProfile(secureboot bool, arch artifacts.Arch) profile.Profile {
|
|
var prof profile.Profile
|
|
|
|
prof.Output.Kind = profile.OutKindInstaller
|
|
prof.Output.OutFormat = profile.OutFormatRaw
|
|
prof.Arch = string(arch)
|
|
prof.Platform = constants.PlatformMetal // doesn't matter for installer output
|
|
|
|
if secureboot {
|
|
prof.SecureBoot = pointer.To(true)
|
|
}
|
|
|
|
return prof
|
|
}
|
|
|
|
// FlavorExtensionProducer is a function which produces a flavor extension tarballs.
|
|
type FlavorExtensionProducer interface {
|
|
GetFlavorExtension(context.Context, *flavor.Flavor) (string, error)
|
|
}
|
|
|
|
// EnhanceFromFlavor enhances the profile with the flavor.
|
|
func EnhanceFromFlavor(ctx context.Context, prof profile.Profile, flavor *flavor.Flavor, flavorExtensionProducer FlavorExtensionProducer, versionTag string) (profile.Profile, error) {
|
|
if len(flavor.Customization.SystemExtensions.OfficialExtensions) > 0 {
|
|
// TODO: implement me
|
|
return prof, xerrors.NewTaggedf[InvalidErrorTag]("system extensions are not supported yet")
|
|
}
|
|
|
|
if prof.Output.Kind != profile.OutKindInitramfs && prof.Output.Kind != profile.OutKindKernel && prof.Output.Kind != profile.OutKindInstaller {
|
|
// skip customizations for profile kinds which don't support it
|
|
prof.Customization.ExtraKernelArgs = append(prof.Customization.ExtraKernelArgs, flavor.Customization.ExtraKernelArgs...)
|
|
}
|
|
|
|
// append flavor extension
|
|
flavorExtensionPath, err := flavorExtensionProducer.GetFlavorExtension(ctx, flavor)
|
|
if err != nil {
|
|
return prof, err
|
|
}
|
|
|
|
prof.Input.SystemExtensions = append(prof.Input.SystemExtensions, profile.ContainerAsset{TarballPath: flavorExtensionPath})
|
|
|
|
prof.Version = versionTag
|
|
|
|
return prof, nil
|
|
}
|