Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion enterprise/spdx/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

"github.com/siderolabs/image-factory/enterprise/spdx/storage"
"github.com/siderolabs/image-factory/internal/artifacts"
"github.com/siderolabs/image-factory/internal/artifacts/imagehandler"
"github.com/siderolabs/image-factory/internal/asset"
"github.com/siderolabs/image-factory/internal/ctxlog"
"github.com/siderolabs/image-factory/internal/profile"
Expand Down Expand Up @@ -309,7 +310,7 @@ func (b *Builder) extractSPDXFromInitramfs(bundle *Bundle, bootAsset asset.BootA
return nil
}

if !strings.HasSuffix(path, artifacts.SPDXFileSuffix) {
if !strings.HasSuffix(path, imagehandler.SPDXFileSuffix) {
return nil
}

Expand Down
78 changes: 9 additions & 69 deletions internal/artifacts/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,76 +14,16 @@ import (
"path/filepath"
"strings"

"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/siderolabs/talos/pkg/machinery/imager/quirks"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"

"github.com/siderolabs/image-factory/internal/artifacts/imagehandler"
"github.com/siderolabs/image-factory/internal/image/verify"
)

type imageHandler func(ctx context.Context, logger *zap.Logger, img v1.Image) error

// imageExportHandler exports the image for further processing.
func imageExportHandler(exportHandler func(logger *zap.Logger, r io.Reader) error) imageHandler {
return func(_ context.Context, logger *zap.Logger, img v1.Image) error {
logger.Info("extracting the image")

r, w := io.Pipe()

var eg errgroup.Group

eg.Go(func() error {
defer w.Close() //nolint:errcheck

return crane.Export(img, w)
})

eg.Go(func() error {
err := exportHandler(logger, r)
if err != nil {
r.CloseWithError(err) // signal the exporter to stop
}

return err
})

if err := eg.Wait(); err != nil {
return fmt.Errorf("error extracting the image: %w", err)
}

return nil
}
}

// imageOCIHandler exports the image to the OCI format.
func imageOCIHandler(path string) imageHandler {
return func(_ context.Context, logger *zap.Logger, img v1.Image) error {
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("error removing the directory %q: %w", path, err)
}

l, err := layout.Write(path, empty.Index)
if err != nil {
return fmt.Errorf("error creating layout: %w", err)
}

logger.Info("exporting the image", zap.String("destination", path))

if err = l.AppendImage(img); err != nil {
return fmt.Errorf("error exporting the image: %w", err)
}

return nil
}
}

// fetchImageByTag contains combined logic of image handling: heading, downloading, verifying signatures, and exporting.
func (m *Manager) fetchImageByTag(imageName, tag string, architecture Arch, imageHandler imageHandler) error {
func (m *Manager) fetchImageByTag(imageName, tag string, architecture Arch, imageHandler imagehandler.Handler) error {
// set a timeout for fetching, but don't bind it to any context, as we want fetch operation to finish
ctx, cancel := context.WithTimeout(context.Background(), FetchTimeout)
defer cancel()
Expand All @@ -105,7 +45,7 @@ func (m *Manager) fetchImageByTag(imageName, tag string, architecture Arch, imag
}

// fetchImageByDigest fetches an image by digest, verifies signatures, and exports it to the storage.
func (m *Manager) fetchImageByDigest(digestRef name.Digest, architecture Arch, imageHandler imageHandler) error {
func (m *Manager) fetchImageByDigest(digestRef name.Digest, architecture Arch, imageHandler imagehandler.Handler) error {
var err error
// set a timeout for fetching, but don't bind it to any context, as we want fetch operation to finish
ctx, cancel := context.WithTimeout(context.Background(), FetchTimeout)
Expand Down Expand Up @@ -149,7 +89,7 @@ func (m *Manager) fetchImageByDigest(digestRef name.Digest, architecture Arch, i
func (m *Manager) fetchImager(tag string) error {
destinationPath := filepath.Join(m.storagePath, tag)

if err := m.fetchImageByTag(m.options.ImagerImage, tag, ArchAmd64, imageExportHandler(func(logger *zap.Logger, r io.Reader) error {
if err := m.fetchImageByTag(m.options.ImagerImage, tag, ArchAmd64, imagehandler.Export(func(logger *zap.Logger, r io.Reader) error {
return untarWithPrefix(logger, r, usrInstallPrefix, destinationPath+tmpSuffix)
})); err != nil {
return err
Expand All @@ -164,7 +104,7 @@ func (m *Manager) extractOverlay(arch Arch, ref OverlayRef) error {

destinationPath := filepath.Join(m.storagePath, string(arch)+"-"+ref.Digest+"-overlay")

if err := m.fetchImageByDigest(imageRef, arch, imageExportHandler(func(logger *zap.Logger, r io.Reader) error {
if err := m.fetchImageByDigest(imageRef, arch, imagehandler.Export(func(logger *zap.Logger, r io.Reader) error {
return untarWithPrefix(logger, r, overlaysPrefix, destinationPath+tmpSuffix)
})); err != nil {
return err
Expand All @@ -177,7 +117,7 @@ func (m *Manager) extractOverlay(arch Arch, ref OverlayRef) error {
func (m *Manager) fetchExtensionImage(arch Arch, ref ExtensionRef, destPath string) error {
imageRef := ref.TaggedReference.Digest(ref.Digest)

if err := m.fetchImageByDigest(imageRef, arch, imageOCIHandler(destPath+tmpSuffix)); err != nil {
if err := m.fetchImageByDigest(imageRef, arch, imagehandler.OCI(destPath+tmpSuffix)); err != nil {
return err
}

Expand All @@ -188,7 +128,7 @@ func (m *Manager) fetchExtensionImage(arch Arch, ref ExtensionRef, destPath stri
func (m *Manager) fetchOverlayImage(arch Arch, ref OverlayRef, destPath string) error {
imageRef := m.imageRegistry.Repo(ref.TaggedReference.RepositoryStr()).Digest(ref.Digest)

if err := m.fetchImageByDigest(imageRef, arch, imageOCIHandler(destPath+tmpSuffix)); err != nil {
if err := m.fetchImageByDigest(imageRef, arch, imagehandler.OCI(destPath+tmpSuffix)); err != nil {
return err
}

Expand All @@ -206,7 +146,7 @@ func (m *Manager) InstallerImageName(versionTag string) string {

// fetchInstallerImage fetches a Talos installer image and exports it to the storage.
func (m *Manager) fetchInstallerImage(arch Arch, versionTag string, destPath string) error {
if err := m.fetchImageByTag(m.InstallerImageName(versionTag), versionTag, arch, imageOCIHandler(destPath+tmpSuffix)); err != nil {
if err := m.fetchImageByTag(m.InstallerImageName(versionTag), versionTag, arch, imagehandler.OCI(destPath+tmpSuffix)); err != nil {
return err
}

Expand All @@ -215,7 +155,7 @@ func (m *Manager) fetchInstallerImage(arch Arch, versionTag string, destPath str

// fetchTalosctlImage fetches a Talosctl image and exports it to the storage.
func (m *Manager) fetchTalosctlImage(versionTag string, destPath string) error {
if err := m.fetchImageByTag(m.options.TalosctlImage, versionTag, ArchAmd64, imageExportHandler(func(logger *zap.Logger, r io.Reader) error {
if err := m.fetchImageByTag(m.options.TalosctlImage, versionTag, ArchAmd64, imagehandler.Export(func(logger *zap.Logger, r io.Reader) error {
return untarWithPrefix(logger, r, "", destPath+tmpSuffix)
})); err != nil {
return err
Expand Down
89 changes: 89 additions & 0 deletions internal/artifacts/imagehandler/imagehandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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 imagehandler

import (
"context"
"fmt"
"io"
"os"

"github.com/google/go-containerregistry/pkg/crane"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/layout"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
)

// Handler processes a fetched image, e.g. exporting it to local storage.
type Handler func(ctx context.Context, logger *zap.Logger, img v1.Image) error

// Export streams the image's flattened filesystem as a tar to exportHandler.
func Export(exportHandler func(logger *zap.Logger, r io.Reader) error) Handler {
return func(_ context.Context, logger *zap.Logger, img v1.Image) error {
logger.Info("extracting the image")

r, w := io.Pipe()

var eg errgroup.Group

eg.Go(func() error {
defer w.Close() //nolint:errcheck

return crane.Export(img, w)
})

eg.Go(func() error {
err := exportHandler(logger, r)
if err != nil {
r.CloseWithError(err) // signal the exporter to stop
}

return err
})

if err := eg.Wait(); err != nil {
return fmt.Errorf("error extracting the image: %w", err)
}

return nil
}
}

// OCI exports the image to an OCI layout at path.
func OCI(path string) Handler {
return func(_ context.Context, logger *zap.Logger, img v1.Image) error {
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("error removing the directory %q: %w", path, err)
}

l, err := layout.Write(path, empty.Index)
if err != nil {
return fmt.Errorf("error creating layout: %w", err)
}

logger.Info("exporting the image", zap.String("destination", path))

var opts []layout.Option

// go-containerregistry's partial.Descriptor only copies Platform onto the index entry
// when the image was resolved from a multi-arch index. Derive platform from the image's
// config file if present so single arch images keep working.
if cfg, cfgErr := img.ConfigFile(); cfgErr == nil && cfg.Architecture != "" {
opts = append(opts, layout.WithPlatform(v1.Platform{
OS: cfg.OS,
Architecture: cfg.Architecture,
Variant: cfg.Variant,
}))
}
Comment on lines +72 to +81

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This here is the only change


if err = l.AppendImage(img, opts...); err != nil {
return fmt.Errorf("error exporting the image: %w", err)
}

return nil
}
}
46 changes: 46 additions & 0 deletions internal/artifacts/imagehandler/imagehandler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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 imagehandler_test

import (
"testing"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"

"github.com/siderolabs/image-factory/internal/artifacts/imagehandler"
)

// TestOCIRecordsPlatform asserts that a single-arch image is written to the OCI layout with a
// platform descriptor, so the Talos imager can match it to a target architecture.
func TestOCIRecordsPlatform(t *testing.T) {
t.Parallel()

img, err := random.Image(1024, 1)
Comment thread
Orzelius marked this conversation as resolved.
require.NoError(t, err)

// Single-arch images (e.g. custom-built extensions) carry their arch only in the config.
img, err = mutate.ConfigFile(img, &v1.ConfigFile{OS: "linux", Architecture: "arm64"})
require.NoError(t, err)

path := t.TempDir() + "/oci"

require.NoError(t, imagehandler.OCI(path)(t.Context(), zaptest.NewLogger(t), img))

idx, err := layout.ImageIndexFromPath(path)
require.NoError(t, err)

manifest, err := idx.IndexManifest()
require.NoError(t, err)

require.Len(t, manifest.Manifests, 1)
require.NotNil(t, manifest.Manifests[0].Platform, "platform descriptor must be recorded")
require.Equal(t, "linux", manifest.Manifests[0].Platform.OS)
require.Equal(t, "arm64", manifest.Manifests[0].Platform.Architecture)
}
Loading