Andrey Smirnov 470cb2f0e8
chore: switch to large runners
Rekres to use large runners instead of generic ones, as Image Factory
tests require lots of resources.

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
2026-02-18 17:08:59 +04:00

384 lines
8.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 artifacts
import (
"archive/tar"
"bufio"
"context"
"errors"
"fmt"
"io"
"slices"
"strings"
"time"
"github.com/blang/semver/v4"
"github.com/google/go-containerregistry/pkg/name"
"github.com/siderolabs/gen/xslices"
"go.uber.org/zap"
"go.yaml.in/yaml/v4"
)
func (m *Manager) fetchTalosVersions() (any, error) {
m.logger.Info("fetching available Talos versions")
ctx, cancel := context.WithTimeout(context.Background(), FetchTimeout)
defer cancel()
repository := m.imageRegistry.Repo(m.options.ImagerImage)
candidates, err := m.pullers[ArchAmd64].List(ctx, repository)
if err != nil {
return nil, fmt.Errorf("failed to list Talos versions: %w", err)
}
var versions []semver.Version //nolint:prealloc
for _, candidate := range candidates {
version, err := semver.ParseTolerant(candidate)
if err != nil {
continue // ignore invalid versions
}
versions = append(versions, version)
}
// find "current" maximum version
maxVersion := slices.MaxFunc(versions, semver.Version.Compare)
// allow non-prerelease versions, and allow pre-release for the "latest" release (maxVersion)
versions = xslices.Filter(versions, func(version semver.Version) bool {
if version.LT(m.options.MinVersion) {
return false // ignore versions below minimum
}
if len(version.Pre) > 0 {
if version.Major != maxVersion.Major || version.Minor != maxVersion.Minor {
return false // ignore pre-releases for older versions
}
if len(version.Pre) != 2 {
return false
}
switch version.Pre[0].VersionStr {
case "alpha", "beta", "rc":
// allow
default:
return false // ignore other pre-release versions
}
if !version.Pre[1].IsNumeric() {
return false
}
}
return true
})
slices.SortFunc(versions, semver.Version.Compare)
m.talosVersionsMu.Lock()
m.talosVersions, m.talosVersionsTimestamp = versions, time.Now()
m.talosVersionsMu.Unlock()
return nil, nil //nolint:nilnil
}
// ExtensionRef is a ref to the extension for some Talos version.
type ExtensionRef struct {
TaggedReference name.Tag
Digest string
Description string
Author string
imageDigest string
}
// OverlayRef is a ref to the overlay for some Talos version.
type OverlayRef struct {
Name string
TaggedReference name.Tag
Digest string
}
// TalosctlTuple represents a OS/Arch/Ext tuple for talosctl binaries.
type TalosctlTuple struct {
OS string
Arch string
Ext string
}
type extensionsDescriptions map[string]struct {
Author string `yaml:"author"`
Description string `yaml:"description"`
}
type overlaysDescriptions struct {
Overlays []overlaysDescription `yaml:"overlays"`
}
type overlaysDescription struct {
Name string `yaml:"name"`
Image string `yaml:"image"`
Digest string `yaml:"digest"`
}
func (m *Manager) fetchOfficialExtensions(tag string) error {
var extensions []ExtensionRef
if err := m.fetchImageByTag(m.options.ExtensionManifestImage, tag, ArchAmd64, imageExportHandler(func(_ *zap.Logger, r io.Reader) error {
var extractErr error
extensions, extractErr = extractExtensionList(r)
if extractErr == nil {
m.logger.Info("extracted the image digests", zap.Int("count", len(extensions)))
}
return extractErr
})); err != nil {
return err
}
m.officialExtensionsMu.Lock()
if m.officialExtensions == nil {
m.officialExtensions = make(map[string][]ExtensionRef)
}
m.officialExtensions[tag] = extensions
m.officialExtensionsMu.Unlock()
return nil
}
func (m *Manager) fetchOfficialOverlays(tag string) error {
var overlays []OverlayRef
if err := m.fetchImageByTag(m.options.OverlayManifestImage, tag, ArchAmd64, imageExportHandler(func(_ *zap.Logger, r io.Reader) error {
var extractErr error
overlays, extractErr = extractOverlayList(r)
if extractErr == nil {
m.logger.Info("extracted the image digests", zap.Int("count", len(overlays)))
}
return extractErr
})); err != nil {
return err
}
m.officialOverlaysMu.Lock()
if m.officialOverlays == nil {
m.officialOverlays = make(map[string][]OverlayRef)
}
m.officialOverlays[tag] = overlays
m.officialOverlaysMu.Unlock()
return nil
}
func (m *Manager) fetchTalosctlTuples(tag string) error {
var talosctlTuples []TalosctlTuple
if err := m.fetchImageByTag(m.options.TalosctlImage, tag, ArchAmd64, imageExportHandler(func(_ *zap.Logger, r io.Reader) error {
var extractErr error
talosctlTuples, extractErr = extractTalosctlTuples(r)
if extractErr == nil {
m.logger.Info("extracted the talosctl tuples", zap.Int("count", len(talosctlTuples)))
}
return extractErr
})); err != nil {
return err
}
m.talosctlTuplesMu.Lock()
if m.talosctlTuples == nil {
m.talosctlTuples = make(map[string][]TalosctlTuple)
}
m.talosctlTuples[tag] = talosctlTuples
m.talosctlTuplesMu.Unlock()
return nil
}
//nolint:gocognit
func extractExtensionList(r io.Reader) ([]ExtensionRef, error) {
var extensions []ExtensionRef
tr := tar.NewReader(r)
var descriptions extensionsDescriptions
for {
hdr, err := tr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("error reading tar header: %w", err)
}
if hdr.Name == "descriptions.yaml" {
decoder := yaml.NewDecoder(tr)
if err = decoder.Decode(&descriptions); err != nil {
return nil, fmt.Errorf("error reading descriptions.yaml file: %w", err)
}
}
if hdr.Name == "image-digests" {
scanner := bufio.NewScanner(tr)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
tagged, digest, ok := strings.Cut(line, "@")
if !ok {
continue
}
taggedRef, err := name.NewTag(tagged)
if err != nil {
return nil, fmt.Errorf("failed to parse tagged reference %s: %w", tagged, err)
}
extensions = append(extensions, ExtensionRef{
TaggedReference: taggedRef,
Digest: digest,
imageDigest: line,
})
}
if scanner.Err() != nil {
return nil, fmt.Errorf("error reading image-digests: %w", scanner.Err())
}
}
}
if extensions != nil {
if descriptions != nil {
for i, extension := range extensions {
desc, ok := descriptions[extension.imageDigest]
if !ok {
continue
}
extensions[i].Author = desc.Author
extensions[i].Description = desc.Description
}
}
return extensions, nil
}
return nil, errors.New("failed to find image-digests file")
}
func extractOverlayList(r io.Reader) ([]OverlayRef, error) {
var overlays []OverlayRef
tr := tar.NewReader(r)
var overlayInfo overlaysDescriptions
for {
hdr, err := tr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("error reading tar header: %w", err)
}
if hdr.Name == "overlays.yaml" {
decoder := yaml.NewDecoder(tr)
if err = decoder.Decode(&overlayInfo); err != nil {
return nil, fmt.Errorf("error reading overlays.yaml file: %w", err)
}
for _, overlay := range overlayInfo.Overlays {
taggedRef, err := name.NewTag(overlay.Image)
if err != nil {
return nil, fmt.Errorf("failed to parse tagged reference %s: %w", overlay.Image, err)
}
overlays = append(overlays, OverlayRef{
Name: overlay.Name,
TaggedReference: taggedRef,
Digest: overlay.Digest,
})
}
}
}
if overlays != nil {
return overlays, nil
}
return nil, errors.New("failed to find overlays.yaml file")
}
func extractTalosctlTuples(r io.Reader) ([]TalosctlTuple, error) {
var tuples []TalosctlTuple
tr := tar.NewReader(r)
for {
hdr, err := tr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("error reading tar header: %w", err)
}
if strings.HasPrefix(hdr.Name, "talosctl-") {
rest, _ := strings.CutPrefix(hdr.Name, "talosctl-")
rest, hasExt := strings.CutSuffix(rest, ".exe")
ext := ""
if hasExt {
ext = ".exe"
}
restParts := strings.Split(rest, "-")
if len(restParts) != 2 {
return nil, fmt.Errorf("invalid talosctl file name %s", hdr.Name)
}
os := restParts[0]
arch := restParts[1]
tuples = append(tuples, TalosctlTuple{
OS: os,
Arch: arch,
Ext: ext,
})
}
}
if tuples != nil {
return tuples, nil
}
return nil, errors.New("failed to find talosctl binaries")
}