talos/pkg/imager/profile/input.go
Andrey Smirnov 0a785802ea
fix: overlay installer operations
1. Use overlay installer to build the `cmdline` when running in
   install/upgrade mode.

2. Pull down the overlay installer with the arch specific to the
   installer being generated, vs. the arch of the `imager`.

3. Print a message when running an overlay installer.

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
2024-04-16 20:07:44 +04:00

365 lines
11 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
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/authn/github"
"github.com/google/go-containerregistry/pkg/crane"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/siderolabs/gen/value"
"golang.org/x/sync/errgroup"
"github.com/siderolabs/talos/internal/pkg/secureboot/measure"
"github.com/siderolabs/talos/internal/pkg/secureboot/pesign"
"github.com/siderolabs/talos/pkg/archiver"
"github.com/siderolabs/talos/pkg/imager/profile/internal/signer/aws"
"github.com/siderolabs/talos/pkg/imager/profile/internal/signer/azure"
"github.com/siderolabs/talos/pkg/imager/profile/internal/signer/file"
"github.com/siderolabs/talos/pkg/images"
"github.com/siderolabs/talos/pkg/machinery/constants"
"github.com/siderolabs/talos/pkg/machinery/imager/quirks"
)
const (
arm64 = "arm64"
amd64 = "amd64"
)
// Input describes inputs for image generation.
type Input struct {
// Kernel is a vmlinuz file.
Kernel FileAsset `yaml:"kernel"`
// Initramfs is a initramfs file (without system extensions).
Initramfs FileAsset `yaml:"initramfs"`
// SDStub is a sd-stub file (only for SecureBoot).
SDStub FileAsset `yaml:"sdStub,omitempty"`
// SDBoot is a sd-boot file (only for SecureBoot).
SDBoot FileAsset `yaml:"sdBoot,omitempty"`
// DTB is a path to the device tree blobs (arm64 only).
DTB FileAsset `yaml:"dtb,omitempty"`
// UBoot is a path to the u-boot binary (arm64 only).
UBoot FileAsset `yaml:"uBoot,omitempty"`
// RPiFirmware is a path to the Raspberry Pi firmware (arm64 only).
RPiFirmware FileAsset `yaml:"rpiFirmware,omitempty"`
// Base installer image to mutate.
BaseInstaller ContainerAsset `yaml:"baseInstaller,omitempty"`
// OverlayInstaller is an overlay image to inject into the installer.
//
// OverlayInstaller architecture should match the output installer architecture.
OverlayInstaller ContainerAsset `yaml:"overlayInstaller,omitempty"`
// SecureBoot is a section with secureboot keys, only for SecureBoot enabled builds.
SecureBoot *SecureBootAssets `yaml:"secureboot,omitempty"`
// SystemExtensions is a list of system extensions to install.
SystemExtensions []ContainerAsset `yaml:"systemExtensions,omitempty"`
}
// FileAsset describes a file asset.
type FileAsset struct {
// Path to the file.
Path string `yaml:"path"`
}
// ContainerAsset describes a container asset.
type ContainerAsset struct {
// ImageRef is a reference to the container image.
ImageRef string `yaml:"imageRef"`
// ForceInsecure forces insecure registry communication.
ForceInsecure bool `yaml:"forceInsecure,omitempty"`
// TarballPath is a path to the .tar format container image contents.
//
// If TarballPath is set, ImageRef is ignored.
TarballPath string `yaml:"tarballPath,omitempty"`
// OCIPath is a path to the OCI format container image contents.
//
// If OCIPath is set, ImageRef is ignored.
OCIPath string `yaml:"ociPath,omitempty"`
}
// SecureBootAssets describes secureboot assets.
type SecureBootAssets struct {
// SecureBoot signing key & cert.
SecureBootSigner SigningKeyAndCertificate `yaml:"secureBootSigner"`
// PCR signing key.
PCRSigner SigningKey `yaml:"pcrSigner"`
// Optional, auto-enrollment paths.
PlatformKeyPath string `yaml:"platformKeyPath,omitempty"`
KeyExchangeKeyPath string `yaml:"keyExchangeKeyPath,omitempty"`
SignatureKeyPath string `yaml:"signatureKeyPath,omitempty"`
}
// SigningKeyAndCertificate describes a signing key & certificate.
type SigningKeyAndCertificate struct {
// File-based.
//
// Static key and certificate paths.
KeyPath string `yaml:"keyPath,omitempty"`
CertPath string `yaml:"certPath,omitempty"`
// Azure.
//
// Azure Vault URL and certificate ID, key will be found from the certificate.
AzureVaultURL string `yaml:"azureVaultURL,omitempty"`
AzureCertificateID string `yaml:"azureCertificateID,omitempty"`
// AWS.
//
// AWS KMS Key ID and region.
// AWS doesn't have a good way to store a certificate, so it's expected to be a file.
AwsKMSKeyID string `yaml:"awsKMSKeyID,omitempty"`
AwsRegion string `yaml:"awsRegion,omitempty"`
AwsCertPath string `yaml:"awsCertPath,omitempty"`
}
// SigningKey describes a signing key.
type SigningKey struct {
// File-based.
//
// Static key path.
KeyPath string `yaml:"keyPath,omitempty"`
// Azure.
//
// Azure Vault URL and key ID.
// AzureKeyVersion might be left empty to use the latest key version.
AzureVaultURL string `yaml:"azureVaultURL,omitempty"`
AzureKeyID string `yaml:"azureKeyID,omitempty"`
AzureKeyVersion string `yaml:"azureKeyVersion,omitempty"`
// AWS.
//
// AWS KMS Key ID and region.
AwsKMSKeyID string `yaml:"awsKMSKeyID,omitempty"`
AwsRegion string `yaml:"awsRegion,omitempty"`
}
// GetSigner returns the signer.
func (key SigningKey) GetSigner(ctx context.Context) (measure.RSAKey, error) {
switch {
case key.KeyPath != "":
return file.NewPCRSigner(key.KeyPath)
case key.AzureVaultURL != "" && key.AzureKeyID != "":
return azure.NewPCRSigner(ctx, key.AzureVaultURL, key.AzureKeyID, key.AzureKeyVersion)
case key.AwsKMSKeyID != "":
return aws.NewPCRSigner(ctx, key.AwsKMSKeyID, key.AwsRegion)
default:
return nil, errors.New("unsupported PCR signer")
}
}
// GetSigner returns the signer.
func (keyAndCert SigningKeyAndCertificate) GetSigner(ctx context.Context) (pesign.CertificateSigner, error) {
switch {
case keyAndCert.KeyPath != "" && keyAndCert.CertPath != "":
return file.NewSecureBootSigner(keyAndCert.CertPath, keyAndCert.KeyPath)
case keyAndCert.AzureVaultURL != "" && keyAndCert.AzureCertificateID != "":
return azure.NewSecureBootSigner(ctx, keyAndCert.AzureVaultURL, keyAndCert.AzureCertificateID, keyAndCert.AzureCertificateID)
case keyAndCert.AwsKMSKeyID != "" && keyAndCert.AwsCertPath != "":
return aws.NewSecureBootSigner(ctx, keyAndCert.AwsKMSKeyID, keyAndCert.AwsRegion, keyAndCert.AwsCertPath)
default:
return nil, errors.New("unsupported PCR signer")
}
}
const defaultSecureBootPrefix = "/secureboot"
// FillDefaults fills default values for the input.
//
//nolint:gocyclo,cyclop
func (i *Input) FillDefaults(arch, version string, secureboot bool) {
var (
zeroFileAsset FileAsset
zeroContainerAsset ContainerAsset
)
if i.Kernel == zeroFileAsset {
i.Kernel.Path = fmt.Sprintf(constants.KernelAssetPath, arch)
}
if i.Initramfs == zeroFileAsset {
i.Initramfs.Path = fmt.Sprintf(constants.InitramfsAssetPath, arch)
}
if arch == arm64 && !quirks.New(version).SupportsOverlay() {
if i.DTB == zeroFileAsset {
i.DTB.Path = fmt.Sprintf(constants.DTBAssetPath, arch)
}
if i.UBoot == zeroFileAsset {
i.UBoot.Path = fmt.Sprintf(constants.UBootAssetPath, arch)
}
if i.RPiFirmware == zeroFileAsset {
i.RPiFirmware.Path = fmt.Sprintf(constants.RPiFirmwareAssetPath, arch)
}
}
if i.BaseInstaller == zeroContainerAsset {
i.BaseInstaller.ImageRef = fmt.Sprintf("%s:%s", images.DefaultInstallerImageRepository, version)
}
if secureboot {
if i.SDStub == zeroFileAsset {
i.SDStub.Path = fmt.Sprintf(constants.SDStubAssetPath, arch)
}
if i.SDBoot == zeroFileAsset {
i.SDBoot.Path = fmt.Sprintf(constants.SDBootAssetPath, arch)
}
if i.SecureBoot == nil {
i.SecureBoot = &SecureBootAssets{}
}
if value.IsZero(i.SecureBoot.SecureBootSigner) {
i.SecureBoot.SecureBootSigner.KeyPath = filepath.Join(defaultSecureBootPrefix, constants.SecureBootSigningKeyAsset)
i.SecureBoot.SecureBootSigner.CertPath = filepath.Join(defaultSecureBootPrefix, constants.SecureBootSigningCertAsset)
}
if value.IsZero(i.SecureBoot.PCRSigner) {
i.SecureBoot.PCRSigner.KeyPath = filepath.Join(defaultSecureBootPrefix, constants.PCRSigningKeyAsset)
}
if i.SecureBoot.PlatformKeyPath == "" {
if platformKeyPath := filepath.Join(defaultSecureBootPrefix, constants.PlatformKeyAsset); fileExists(platformKeyPath) {
i.SecureBoot.PlatformKeyPath = platformKeyPath
}
}
if i.SecureBoot.KeyExchangeKeyPath == "" {
if keyExchangeKeyPath := filepath.Join(defaultSecureBootPrefix, constants.KeyExchangeKeyAsset); fileExists(keyExchangeKeyPath) {
i.SecureBoot.KeyExchangeKeyPath = keyExchangeKeyPath
}
}
if i.SecureBoot.SignatureKeyPath == "" {
if signatureKeyPath := filepath.Join(defaultSecureBootPrefix, constants.SignatureKeyAsset); fileExists(signatureKeyPath) {
i.SecureBoot.SignatureKeyPath = signatureKeyPath
}
}
}
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// Pull the container asset to the path.
func (c *ContainerAsset) Pull(ctx context.Context, arch string, printf func(string, ...any)) (v1.Image, error) {
if c.TarballPath != "" {
return nil, errors.New("pulling tarball container image is not supported")
}
if c.OCIPath != "" {
printf("using OCI image from %s...", c.OCIPath)
return c.pullFromOCI(arch)
}
printf("pulling %s...", c.ImageRef)
opts := []crane.Option{
crane.WithPlatform(&v1.Platform{
Architecture: arch,
OS: "linux",
}),
crane.WithContext(ctx),
crane.WithAuthFromKeychain(
authn.NewMultiKeychain(
authn.DefaultKeychain,
github.Keychain,
),
),
}
if c.ForceInsecure {
opts = append(opts, crane.Insecure)
}
img, err := crane.Pull(c.ImageRef, opts...)
if err != nil {
return nil, fmt.Errorf("error pulling image %s: %w", c.ImageRef, err)
}
return img, nil
}
func (c *ContainerAsset) pullFromOCI(arch string) (v1.Image, error) {
ociLayout, err := layout.FromPath(c.OCIPath)
if err != nil {
return nil, fmt.Errorf("error opening OCI layout: %w", err)
}
ociIndex, err := ociLayout.ImageIndex()
if err != nil {
return nil, fmt.Errorf("error opening OCI index: %w", err)
}
ociManifest, err := ociIndex.IndexManifest()
if err != nil {
return nil, fmt.Errorf("error opening OCI manifest: %w", err)
}
for _, manifest := range ociManifest.Manifests {
if manifest.Platform == nil {
continue
}
if manifest.Platform.OS == "linux" && manifest.Platform.Architecture == arch {
img, err := ociLayout.Image(manifest.Digest)
if err != nil {
return nil, fmt.Errorf("error opening OCI image: %w", err)
}
return img, nil
}
}
return nil, fmt.Errorf("no OCI image found for %s", arch)
}
// Extract the container asset to the path.
func (c *ContainerAsset) Extract(ctx context.Context, destination, arch string, printf func(string, ...any)) error {
if c.TarballPath != "" {
in, err := os.Open(c.TarballPath)
if err != nil {
return err
}
defer in.Close() //nolint:errcheck
printf("extracting %s...", c.TarballPath)
return archiver.Untar(ctx, in, destination)
}
img, err := c.Pull(ctx, arch, printf)
if err != nil {
return err
}
r, w := io.Pipe()
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
defer w.Close() //nolint:errcheck
return crane.Export(img, w)
})
eg.Go(func() error {
return archiver.Untar(ctx, r, destination)
})
return eg.Wait()
}