Skip to content

Commit 307eaac

Browse files
committed
fix: handle single arch images
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. Part of #395 Signed-off-by: Orzelius <33936483+Orzelius@users.noreply.github.com>
1 parent 6b1c855 commit 307eaac

5 files changed

Lines changed: 151 additions & 73 deletions

File tree

internal/artifacts/fetch.go

Lines changed: 9 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -14,76 +14,16 @@ import (
1414
"path/filepath"
1515
"strings"
1616

17-
"github.com/google/go-containerregistry/pkg/crane"
1817
"github.com/google/go-containerregistry/pkg/name"
19-
v1 "github.com/google/go-containerregistry/pkg/v1"
20-
"github.com/google/go-containerregistry/pkg/v1/empty"
21-
"github.com/google/go-containerregistry/pkg/v1/layout"
2218
"github.com/siderolabs/talos/pkg/machinery/imager/quirks"
2319
"go.uber.org/zap"
24-
"golang.org/x/sync/errgroup"
2520

21+
"github.com/siderolabs/image-factory/internal/artifacts/imagehandler"
2622
"github.com/siderolabs/image-factory/internal/image/verify"
2723
)
2824

29-
type imageHandler func(ctx context.Context, logger *zap.Logger, img v1.Image) error
30-
31-
// imageExportHandler exports the image for further processing.
32-
func imageExportHandler(exportHandler func(logger *zap.Logger, r io.Reader) error) imageHandler {
33-
return func(_ context.Context, logger *zap.Logger, img v1.Image) error {
34-
logger.Info("extracting the image")
35-
36-
r, w := io.Pipe()
37-
38-
var eg errgroup.Group
39-
40-
eg.Go(func() error {
41-
defer w.Close() //nolint:errcheck
42-
43-
return crane.Export(img, w)
44-
})
45-
46-
eg.Go(func() error {
47-
err := exportHandler(logger, r)
48-
if err != nil {
49-
r.CloseWithError(err) // signal the exporter to stop
50-
}
51-
52-
return err
53-
})
54-
55-
if err := eg.Wait(); err != nil {
56-
return fmt.Errorf("error extracting the image: %w", err)
57-
}
58-
59-
return nil
60-
}
61-
}
62-
63-
// imageOCIHandler exports the image to the OCI format.
64-
func imageOCIHandler(path string) imageHandler {
65-
return func(_ context.Context, logger *zap.Logger, img v1.Image) error {
66-
if err := os.RemoveAll(path); err != nil {
67-
return fmt.Errorf("error removing the directory %q: %w", path, err)
68-
}
69-
70-
l, err := layout.Write(path, empty.Index)
71-
if err != nil {
72-
return fmt.Errorf("error creating layout: %w", err)
73-
}
74-
75-
logger.Info("exporting the image", zap.String("destination", path))
76-
77-
if err = l.AppendImage(img); err != nil {
78-
return fmt.Errorf("error exporting the image: %w", err)
79-
}
80-
81-
return nil
82-
}
83-
}
84-
8525
// fetchImageByTag contains combined logic of image handling: heading, downloading, verifying signatures, and exporting.
86-
func (m *Manager) fetchImageByTag(imageName, tag string, architecture Arch, imageHandler imageHandler) error {
26+
func (m *Manager) fetchImageByTag(imageName, tag string, architecture Arch, imageHandler imagehandler.Handler) error {
8727
// set a timeout for fetching, but don't bind it to any context, as we want fetch operation to finish
8828
ctx, cancel := context.WithTimeout(context.Background(), FetchTimeout)
8929
defer cancel()
@@ -105,7 +45,7 @@ func (m *Manager) fetchImageByTag(imageName, tag string, architecture Arch, imag
10545
}
10646

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

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

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

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

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

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

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

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

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

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

216156
// fetchTalosctlImage fetches a Talosctl image and exports it to the storage.
217157
func (m *Manager) fetchTalosctlImage(versionTag string, destPath string) error {
218-
if err := m.fetchImageByTag(m.options.TalosctlImage, versionTag, ArchAmd64, imageExportHandler(func(logger *zap.Logger, r io.Reader) error {
158+
if err := m.fetchImageByTag(m.options.TalosctlImage, versionTag, ArchAmd64, imagehandler.Export(func(logger *zap.Logger, r io.Reader) error {
219159
return untarWithPrefix(logger, r, "", destPath+tmpSuffix)
220160
})); err != nil {
221161
return err
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package imagehandler
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"io"
11+
"os"
12+
13+
"github.com/google/go-containerregistry/pkg/crane"
14+
v1 "github.com/google/go-containerregistry/pkg/v1"
15+
"github.com/google/go-containerregistry/pkg/v1/empty"
16+
"github.com/google/go-containerregistry/pkg/v1/layout"
17+
"go.uber.org/zap"
18+
"golang.org/x/sync/errgroup"
19+
)
20+
21+
// Handler processes a fetched image, e.g. exporting it to local storage.
22+
type Handler func(ctx context.Context, logger *zap.Logger, img v1.Image) error
23+
24+
// Export streams the image's flattened filesystem as a tar to exportHandler.
25+
func Export(exportHandler func(logger *zap.Logger, r io.Reader) error) Handler {
26+
return func(_ context.Context, logger *zap.Logger, img v1.Image) error {
27+
logger.Info("extracting the image")
28+
29+
r, w := io.Pipe()
30+
31+
var eg errgroup.Group
32+
33+
eg.Go(func() error {
34+
defer w.Close() //nolint:errcheck
35+
36+
return crane.Export(img, w)
37+
})
38+
39+
eg.Go(func() error {
40+
err := exportHandler(logger, r)
41+
if err != nil {
42+
r.CloseWithError(err) // signal the exporter to stop
43+
}
44+
45+
return err
46+
})
47+
48+
if err := eg.Wait(); err != nil {
49+
return fmt.Errorf("error extracting the image: %w", err)
50+
}
51+
52+
return nil
53+
}
54+
}
55+
56+
// OCI exports the image to an OCI layout at path.
57+
func OCI(path string) Handler {
58+
return func(_ context.Context, logger *zap.Logger, img v1.Image) error {
59+
if err := os.RemoveAll(path); err != nil {
60+
return fmt.Errorf("error removing the directory %q: %w", path, err)
61+
}
62+
63+
l, err := layout.Write(path, empty.Index)
64+
if err != nil {
65+
return fmt.Errorf("error creating layout: %w", err)
66+
}
67+
68+
logger.Info("exporting the image", zap.String("destination", path))
69+
70+
var opts []layout.Option
71+
72+
// go-containerregistry's partial.Descriptor only copies Platform onto the index entry
73+
// when the image was resolved from a multi-arch index. Derive platform from the image's
74+
// config file if present so single arch images keep working.
75+
if cfg, cfgErr := img.ConfigFile(); cfgErr == nil && cfg.Architecture != "" {
76+
opts = append(opts, layout.WithPlatform(v1.Platform{
77+
OS: cfg.OS,
78+
Architecture: cfg.Architecture,
79+
Variant: cfg.Variant,
80+
}))
81+
}
82+
83+
if err = l.AppendImage(img, opts...); err != nil {
84+
return fmt.Errorf("error exporting the image: %w", err)
85+
}
86+
87+
return nil
88+
}
89+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package imagehandler_test
6+
7+
import (
8+
"context"
9+
"testing"
10+
11+
v1 "github.com/google/go-containerregistry/pkg/v1"
12+
"github.com/google/go-containerregistry/pkg/v1/layout"
13+
"github.com/google/go-containerregistry/pkg/v1/mutate"
14+
"github.com/google/go-containerregistry/pkg/v1/random"
15+
"github.com/stretchr/testify/require"
16+
"go.uber.org/zap/zaptest"
17+
18+
"github.com/siderolabs/image-factory/internal/artifacts/imagehandler"
19+
)
20+
21+
// TestOCIRecordsPlatform asserts that a single-arch image is written to the OCI layout with a
22+
// platform descriptor, so the Talos imager can match it to a target architecture.
23+
func TestOCIRecordsPlatform(t *testing.T) {
24+
img, err := random.Image(1024, 1)
25+
require.NoError(t, err)
26+
27+
// Single-arch images (e.g. custom-built extensions) carry their arch only in the config.
28+
img, err = mutate.ConfigFile(img, &v1.ConfigFile{OS: "linux", Architecture: "arm64"})
29+
require.NoError(t, err)
30+
31+
path := t.TempDir() + "/oci"
32+
33+
require.NoError(t, imagehandler.OCI(path)(context.Background(), zaptest.NewLogger(t), img))
34+
35+
idx, err := layout.ImageIndexFromPath(path)
36+
require.NoError(t, err)
37+
38+
manifest, err := idx.IndexManifest()
39+
require.NoError(t, err)
40+
41+
require.Len(t, manifest.Manifests, 1)
42+
require.NotNil(t, manifest.Manifests[0].Platform, "platform descriptor must be recorded")
43+
require.Equal(t, "linux", manifest.Manifests[0].Platform.OS)
44+
require.Equal(t, "arm64", manifest.Manifests[0].Platform.Architecture)
45+
}

internal/artifacts/spdx.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
v1 "github.com/google/go-containerregistry/pkg/v1"
1818
"go.uber.org/zap"
1919
"golang.org/x/sync/errgroup"
20+
21+
"github.com/siderolabs/image-factory/internal/artifacts/imagehandler"
2022
)
2123

2224
const (
@@ -52,7 +54,7 @@ func (m *Manager) ExtractExtensionSPDX(ctx context.Context, arch Arch, ref Exten
5254
}
5355

5456
// spdxExportHandler creates an image handler that extracts SPDX files.
55-
func spdxExportHandler(files *[]SPDXFile, source string) imageHandler {
57+
func spdxExportHandler(files *[]SPDXFile, source string) imagehandler.Handler {
5658
return func(_ context.Context, logger *zap.Logger, img v1.Image) error {
5759
logger.Info("extracting SPDX files from image")
5860

internal/artifacts/versions.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"github.com/siderolabs/gen/xslices"
2121
"go.uber.org/zap"
2222
"go.yaml.in/yaml/v4"
23+
24+
"github.com/siderolabs/image-factory/internal/artifacts/imagehandler"
2325
)
2426

2527
func (m *Manager) fetchTalosVersions() (any, error) {
@@ -134,7 +136,7 @@ type overlaysDescription struct {
134136
func (m *Manager) fetchExtensionList(image, tag string) ([]ExtensionRef, error) {
135137
var extensions []ExtensionRef
136138

137-
err := m.fetchImageByTag(image, tag, ArchAmd64, imageExportHandler(func(_ *zap.Logger, r io.Reader) error {
139+
err := m.fetchImageByTag(image, tag, ArchAmd64, imagehandler.Export(func(_ *zap.Logger, r io.Reader) error {
138140
var extractErr error
139141

140142
extensions, extractErr = extractExtensionList(r, m.imageRegistry)
@@ -151,7 +153,7 @@ func (m *Manager) fetchExtensionList(image, tag string) ([]ExtensionRef, error)
151153
func (m *Manager) fetchOverlayList(tag string) ([]OverlayRef, error) {
152154
var overlays []OverlayRef
153155

154-
err := m.fetchImageByTag(m.options.OverlayManifestImage, tag, ArchAmd64, imageExportHandler(func(_ *zap.Logger, r io.Reader) error {
156+
err := m.fetchImageByTag(m.options.OverlayManifestImage, tag, ArchAmd64, imagehandler.Export(func(_ *zap.Logger, r io.Reader) error {
155157
var extractErr error
156158

157159
overlays, extractErr = extractOverlayList(r)
@@ -168,7 +170,7 @@ func (m *Manager) fetchOverlayList(tag string) ([]OverlayRef, error) {
168170
func (m *Manager) fetchTalosctlTuples(tag string) ([]TalosctlTuple, error) {
169171
var talosctlTuples []TalosctlTuple
170172

171-
err := m.fetchImageByTag(m.options.TalosctlImage, tag, ArchAmd64, imageExportHandler(func(_ *zap.Logger, r io.Reader) error {
173+
err := m.fetchImageByTag(m.options.TalosctlImage, tag, ArchAmd64, imagehandler.Export(func(_ *zap.Logger, r io.Reader) error {
172174
var extractErr error
173175

174176
talosctlTuples, extractErr = extractTalosctlTuples(r)

0 commit comments

Comments
 (0)