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>
365 lines
11 KiB
Go
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()
|
|
}
|