Skip to content
Draft
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
26 changes: 26 additions & 0 deletions cmd/image-factory/cmd/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,21 @@ func (o *OCIRepositoryOptions) String() string {
return strings.Join(parts, "/")
}

// Image returns the Namespace + Repository string (without the registry).
func (o *OCIRepositoryOptions) Image() string {
parts := []string{}

if o.Namespace != "" {
parts = append(parts, o.Namespace)
}

if o.Repository != "" {
parts = append(parts, o.Repository)
}

return strings.Join(parts, "/")
}

// MetricsOptions holds configuration for exposing Prometheus metrics.
type MetricsOptions struct {
// Addr is the bind address for the metrics HTTP server.
Expand Down Expand Up @@ -502,6 +517,17 @@ type EnterpriseOptions struct {

// VEX contains configuration for VEX data fetching.
VEX VEXOptions `koanf:"vex"`

// ExtraExtensions contains configuration for extra (custom) extensions.
ExtraExtensions ExtraExtensionsOptions `koanf:"extraExtensions"`
}

// ExtraExtensionsOptions configures custom extensions offered alongside the official ones.
type ExtraExtensionsOptions struct {
// Manifest specifies the OCI repository holding the extra extensions manifest image.
//
// It may live in a different registry than the official images.
Manifest OCIRepositoryOptions `koanf:"manifest"`
}

// SPDXOptions configures SPDX document generation and caching.
Expand Down
56 changes: 56 additions & 0 deletions cmd/image-factory/cmd/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,62 @@ func TestOCIRepositoryOptions(t *testing.T) {
})
}
})

t.Run("Image", func(t *testing.T) {
t.Parallel()

for _, tc := range []struct {
expected string
input cmd.OCIRepositoryOptions
}{
{
expected: "library/golang",
input: cmd.OCIRepositoryOptions{
Registry: "docker.io",
Namespace: "library",
Repository: "golang",
},
},
{
expected: "nginx",
input: cmd.OCIRepositoryOptions{
Registry: "127.0.0.1:5000",
Namespace: "",
Repository: "nginx",
},
},
{
expected: "internal/nginx",
input: cmd.OCIRepositoryOptions{
Registry: "example.com",
Namespace: "internal",
Repository: "nginx",
},
},
{
expected: "foo/bar/baz/nginx",
input: cmd.OCIRepositoryOptions{
Registry: "example.com",
Namespace: "foo/bar/baz",
Repository: "nginx",
},
},
{
expected: "library/golang",
input: cmd.OCIRepositoryOptions{
Namespace: "library",
Repository: "golang",
},
},
} {
t.Run(tc.expected, func(t *testing.T) {
t.Parallel()

actual := tc.input.Image()
assert.Equal(t, tc.expected, actual)
})
}
})
}

func TestOptionsValidate(t *testing.T) {
Expand Down
33 changes: 18 additions & 15 deletions cmd/image-factory/cmd/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,21 +469,24 @@ func buildArtifactsManager(logger *zap.Logger, opts Options) (*artifacts.Manager
}

artifactsManager, err := artifacts.NewManager(logger, artifacts.Options{
MinVersion: minVersion,
BrokenVersions: brokenVersions,
ImageRegistry: opts.Artifacts.Core.Registry,
InsecureImageRegistry: opts.Artifacts.Core.Insecure,
ImageVerifyOptions: imageVerifyOptions,
TalosVersionRecheckInterval: opts.Artifacts.TalosVersionRecheckInterval,
RemoteOptions: remoteOptions(),
RegistryRefreshInterval: opts.Artifacts.RefreshInterval,

InstallerBaseImage: opts.Artifacts.Core.Components.InstallerBase,
InstallerImage: opts.Artifacts.Core.Components.Installer,
ImagerImage: opts.Artifacts.Core.Components.Imager,
ExtensionManifestImage: opts.Artifacts.Core.Components.ExtensionManifest,
OverlayManifestImage: opts.Artifacts.Core.Components.OverlayManifest,
TalosctlImage: opts.Artifacts.Core.Components.Talosctl,
MinVersion: minVersion,
BrokenVersions: brokenVersions,
ImageRegistry: opts.Artifacts.Core.Registry,
ExtraExtensionsImageRegistry: opts.Enterprise.ExtraExtensions.Manifest.Registry,
InsecureImageRegistry: opts.Artifacts.Core.Insecure,
InsecureExtraExtensionsRegistry: opts.Enterprise.ExtraExtensions.Manifest.Insecure,
ImageVerifyOptions: imageVerifyOptions,
TalosVersionRecheckInterval: opts.Artifacts.TalosVersionRecheckInterval,
RemoteOptions: remoteOptions(),
RegistryRefreshInterval: opts.Artifacts.RefreshInterval,

InstallerBaseImage: opts.Artifacts.Core.Components.InstallerBase,
InstallerImage: opts.Artifacts.Core.Components.Installer,
ImagerImage: opts.Artifacts.Core.Components.Imager,
ExtensionManifestImage: opts.Artifacts.Core.Components.ExtensionManifest,
ExtraExtensionManifestImage: opts.Enterprise.ExtraExtensions.Manifest.Image(),
OverlayManifestImage: opts.Artifacts.Core.Components.OverlayManifest,
TalosctlImage: opts.Artifacts.Core.Components.Talosctl,

ExternalURL: opts.HTTP.ExternalURL,
})
Expand Down
14 changes: 14 additions & 0 deletions hack/dev/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,23 @@ artifacts:
registry: registry.local:5000
namespace: image-factory
repository: schematic
insecure: true


installer:
# internal registry namespace to push installer images to
internal:
registry: registry.local:5000
namespace: siderolabs
insecure: true

cache:
oci:
# private registry repository for cached assets
registry: registry.local:5000
namespace: image-factory
repository: cache
insecure: true

# path to the ECDSA private key (to sign cached assets)
signingKeyPath: /etc/image-factory/cache-signing-key.key
Expand All @@ -34,13 +38,23 @@ authentication:
enabled: false
htpasswdPath: /etc/image-factory/htpasswd

containerSignature:
disabled: true

enterprise:
extraExtensions:
manifest:
registry: host.docker.internal:5005
namespace: extension-testing
repository: extensions
insecure: true
spdx:
cache:
# private registry repository for cached spdx
registry: registry.local:5000
namespace: image-factory
repository: spdx-cache
insecure: true
vex:
data:
registry: registry.ghcr.io:5000
Expand Down
17 changes: 11 additions & 6 deletions internal/artifacts/artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ type Options struct { //nolint:govet
//
// For official images, this is "ghcr.io".
ImageRegistry string
// Repository to source extra extensions from.
ExtraExtensionsImageRegistry string
// Option to allow using an image registry without TLS.
InsecureImageRegistry bool
// Option to allow using an image registry without TLS for extra extensions.
InsecureExtraExtensionsRegistry bool
// MinVersion is the minimum version of Talos to use.
MinVersion semver.Version
// BrokenVersions are Talos versions that should be rejected when listing available versions.
Expand All @@ -36,12 +40,13 @@ type Options struct { //nolint:govet
RegistryRefreshInterval time.Duration

// Images used by the artifacts manager.
InstallerBaseImage string
InstallerImage string
ImagerImage string
ExtensionManifestImage string
OverlayManifestImage string
TalosctlImage string
InstallerBaseImage string
InstallerImage string
ImagerImage string
ExtensionManifestImage string
ExtraExtensionManifestImage string
OverlayManifestImage string
TalosctlImage string

// External identification.
ExternalURL string
Expand Down
8 changes: 7 additions & 1 deletion internal/artifacts/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,20 @@ import (
)

// fetchImageByTag contains combined logic of image handling: heading, downloading, verifying signatures, and exporting.
// Uses the default image registry.
func (m *Manager) fetchImageByTag(imageName, tag string, architecture Arch, imageHandler imagehandler.Handler) error {
return m.fetchImageByTagWithRepo(imageName, tag, m.imageRegistry, architecture, imageHandler)
}

// fetchImageByTag contains combined logic of image handling: heading, downloading, verifying signatures, and exporting.
func (m *Manager) fetchImageByTagWithRepo(imageName, tag string, reg name.Registry, 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()

// light check first - if the image exists, and resolve the digest
// it's important to do further checks by digest exactly
repoRef := m.imageRegistry.Repo(imageName).Tag(tag)
repoRef := reg.Repo(imageName).Tag(tag)

m.logger.Debug("heading the image", zap.Stringer("image", repoRef))

Expand Down
80 changes: 67 additions & 13 deletions internal/artifacts/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,19 @@ import (
"golang.org/x/sync/singleflight"

"github.com/siderolabs/image-factory/internal/cache"
"github.com/siderolabs/image-factory/internal/regtransport"
"github.com/siderolabs/image-factory/internal/remotewrap"
)

// Manager supports loading, caching and serving Talos release artifacts.
type Manager struct { //nolint:govet
options Options
storagePath string
schematicsPath string
logger *zap.Logger
imageRegistry name.Registry
pullers map[Arch]remotewrap.Puller
options Options
storagePath string
schematicsPath string
logger *zap.Logger
imageRegistry name.Registry
extraExtensionsRegistry name.Registry
pullers map[Arch]remotewrap.Puller

sf singleflight.Group

Expand Down Expand Up @@ -72,6 +74,16 @@ func NewManager(logger *zap.Logger, options Options) (*Manager, error) {
return nil, fmt.Errorf("failed to parse image registry: %w", err)
}

opts = []name.Option{}
if options.InsecureExtraExtensionsRegistry {
opts = append(opts, name.Insecure)
}

extraExtensionsImageRegistry, err := name.NewRegistry(options.ExtraExtensionsImageRegistry, opts...)
if err != nil {
return nil, fmt.Errorf("failed to parse extra extensions image registry: %w", err)
}

pullers := make(map[Arch]remotewrap.Puller, 2)

for _, arch := range []Arch{ArchAmd64, ArchArm64} {
Expand All @@ -93,16 +105,58 @@ func NewManager(logger *zap.Logger, options Options) (*Manager, error) {
}

m := &Manager{
options: options,
storagePath: tmpDir,
schematicsPath: schematicsPath,
logger: logger,
imageRegistry: imageRegistry,
pullers: pullers,
options: options,
storagePath: tmpDir,
schematicsPath: schematicsPath,
logger: logger,
imageRegistry: imageRegistry,
extraExtensionsRegistry: extraExtensionsImageRegistry,
pullers: pullers,
}

m.officialExtensions = cache.NewSingleFlightCache(func(tag string) ([]ExtensionRef, error) {
return m.fetchExtensionList(m.options.ExtensionManifestImage, tag)
officialExtensions, err := m.fetchExtensionList(m.options.ExtensionManifestImage, tag, m.imageRegistry)
if err != nil {
return nil, fmt.Errorf("failed to fetch official extensions: %w", err)
}

if m.options.ExtraExtensionManifestImage == "" {
return officialExtensions, nil
}

extraExtensions, err := m.fetchExtensionList(m.options.ExtraExtensionManifestImage, tag, m.extraExtensionsRegistry)
if err != nil {
if regtransport.IsStatusCodeError(err, http.StatusNotFound) {
logger.Sugar().Warnf("extra extensions not published for talos version %s", tag)

return officialExtensions, nil
}

return nil, fmt.Errorf("failed to fetch extra extensions: %w", err)
}

allExtensions := slices.Concat(extraExtensions)

// prioritize extra extensions if there's a name conflict
for _, official := range officialExtensions {
extraOverride := false

for _, extra := range extraExtensions {
if official.TaggedReference.RepositoryStr() == extra.TaggedReference.RepositoryStr() {
extraOverride = true

break
}
}

if extraOverride {
continue
}

allExtensions = append(allExtensions, official)
}

return allExtensions, nil
})

m.officialOverlays = cache.NewSingleFlightCache(func(tag string) ([]OverlayRef, error) {
Expand Down
6 changes: 3 additions & 3 deletions internal/artifacts/versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,13 @@ type overlaysDescription struct {
Digest string `yaml:"digest"`
}

func (m *Manager) fetchExtensionList(image, tag string) ([]ExtensionRef, error) {
func (m *Manager) fetchExtensionList(image, tag string, registry name.Registry) ([]ExtensionRef, error) {
var extensions []ExtensionRef

err := m.fetchImageByTag(image, tag, ArchAmd64, imagehandler.Export(func(_ *zap.Logger, r io.Reader) error {
err := m.fetchImageByTagWithRepo(image, tag, registry, ArchAmd64, imagehandler.Export(func(_ *zap.Logger, r io.Reader) error {
var extractErr error

extensions, extractErr = extractExtensionList(r, m.imageRegistry)
extensions, extractErr = extractExtensionList(r, registry)
if extractErr == nil {
m.logger.Info("extracted the image digests", zap.Int("count", len(extensions)))
}
Expand Down