Skip to content

Commit 58e4be9

Browse files
committed
feat: allow excluding Talos releases
In case of broken Talos release, we keep serving it in the UI/API. This can lead to issues when customers download broken release and try to run it. We should be able to exclude the broken versions without creating new release of Image Factory, purely by specifying them in config. For API completeness, we will also serve /versions?broken=true endpoint that lists the broken releases that should not be used. Signed-off-by: Mateusz Urbanek <mateusz.urbanek@siderolabs.com>
1 parent 147a3e8 commit 58e4be9

12 files changed

Lines changed: 249 additions & 18 deletions

File tree

cmd/image-factory/cmd/options.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ type AssetBuilderOptions struct {
8686
// MinTalosVersion specifies the minimum supported Talos version for assets.
8787
MinTalosVersion string `koanf:"minTalosVersion"`
8888

89+
// BrokenTalosVersions lists Talos versions that should be considered broken and avoided when building assets.
90+
// Those are versions that are known to have critical issues that prevent them from working correctly, such as bugs in Talos that cause build failures or runtime errors.
91+
BrokenTalosVersions []string `koanf:"brokenTalosVersions"`
92+
8993
// MaxConcurrency sets the maximum number of simultaneous asset build operations.
9094
MaxConcurrency int `koanf:"maxConcurrency"`
9195
}

cmd/image-factory/cmd/service.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,22 @@ func buildArtifactsManager(logger *zap.Logger, opts Options) (*artifacts.Manager
449449
return nil, fmt.Errorf("failed to parse minimum Talos version: %w", err)
450450
}
451451

452+
brokenVersions := make([]semver.Version, 0, len(opts.Build.BrokenTalosVersions))
453+
454+
for _, v := range opts.Build.BrokenTalosVersions {
455+
var parsed semver.Version
456+
457+
parsed, err = semver.Parse(v)
458+
if err != nil {
459+
return nil, fmt.Errorf("failed to parse broken Talos version %q: %w", v, err)
460+
}
461+
462+
brokenVersions = append(brokenVersions, parsed)
463+
}
464+
452465
artifactsManager, err := artifacts.NewManager(logger, artifacts.Options{
453466
MinVersion: minVersion,
467+
BrokenVersions: brokenVersions,
454468
ImageRegistry: opts.Artifacts.Core.Registry,
455469
InsecureImageRegistry: opts.Artifacts.Core.Insecure,
456470
ImageVerifyOptions: imageVerifyOptions,

cmd/image-factory/flags/config.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package flags
66

77
import (
8+
stdjson "encoding/json"
89
"errors"
910
"fmt"
1011
"path/filepath"
@@ -116,7 +117,35 @@ func splitProvider(s string) (string, string) {
116117

117118
func newEnvConfig(prefix string) Config {
118119
return Config{
119-
Provider: env.Provider(".", env.Opt{Prefix: prefix}),
120+
Provider: env.Provider(".", env.Opt{
121+
Prefix: prefix,
122+
TransformFunc: envTransform(prefix),
123+
}),
124+
}
125+
}
126+
127+
// envTransform maps a prefixed environment variable to a koanf-style dotted, lower-cased key,
128+
// and JSON-decodes values that look like a JSON array or object so that complex types
129+
// (e.g. []string, []struct{...}) can be set from a single environment variable.
130+
//
131+
// Examples:
132+
// - IF_HTTP_EXTERNALURL=https://x → http.externalurl = "https://x"
133+
// - IF_HTTP_ALLOWEDORIGINS=["*","foo"] → http.allowedorigins = []any{"*","foo"}
134+
// - IF_BUILD_BROKENTALOSVERSIONS=[{...}] → build.brokentalosversions = []any{map[...]}
135+
func envTransform(prefix string) func(k, v string) (string, any) {
136+
return func(k, v string) (string, any) {
137+
key := strings.ToLower(strings.ReplaceAll(strings.TrimPrefix(k, prefix), "_", "."))
138+
139+
trimmed := strings.TrimSpace(v)
140+
if trimmed != "" && (trimmed[0] == '[' || trimmed[0] == '{') {
141+
var parsed any
142+
143+
if err := stdjson.Unmarshal([]byte(trimmed), &parsed); err == nil {
144+
return key, parsed
145+
}
146+
}
147+
148+
return key, v
120149
}
121150
}
122151

docs/configuration.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ MinTalosVersion specifies the minimum supported Talos version for assets.
9999

100100
---
101101

102+
### `build.brokenTalosVersions`
103+
104+
- **Type:** `[]string`
105+
- **Env:** `BUILD_BROKENTALOSVERSIONS`
106+
107+
BrokenTalosVersions lists Talos versions that should be considered broken and avoided when building assets.
108+
Those are versions that are known to have critical issues that prevent them from working correctly, such as bugs in Talos that cause build failures or runtime errors.
109+
110+
---
111+
102112
### `build.maxConcurrency`
103113

104114
- **Type:** `int`
@@ -1045,6 +1055,7 @@ authentication:
10451055
enabled: false
10461056
htpasswdPath: ""
10471057
build:
1058+
brokenTalosVersions: []
10481059
maxConcurrency: 6
10491060
minTalosVersion: 1.2.0
10501061
cache:
@@ -1156,6 +1167,7 @@ IF_ARTIFACTS_SCHEMATIC_REPOSITORY=schematics
11561167
IF_ARTIFACTS_TALOSVERSIONRECHECKINTERVAL=15m0s
11571168
IF_AUTHENTICATION_ENABLED=false
11581169
IF_AUTHENTICATION_HTPASSWDPATH=
1170+
IF_BUILD_BROKENTALOSVERSIONS=[]
11591171
IF_BUILD_MAXCONCURRENCY=6
11601172
IF_BUILD_MINTALOSVERSION=1.2.0
11611173
IF_CACHE_CDN_ENABLED=false
@@ -1197,7 +1209,7 @@ IF_ENTERPRISE_VEX_DATA_INSECURE=false
11971209
IF_ENTERPRISE_VEX_DATA_NAMESPACE=siderolabs/talos-vex
11981210
IF_ENTERPRISE_VEX_DATA_REGISTRY=ghcr.io
11991211
IF_ENTERPRISE_VEX_DATA_REPOSITORY=talos-vex-data
1200-
IF_HTTP_ALLOWEDORIGINS=[*]
1212+
IF_HTTP_ALLOWEDORIGINS=["*"]
12011213
IF_HTTP_CERTFILE=
12021214
IF_HTTP_EXTERNALPXEURL=
12031215
IF_HTTP_EXTERNALURL=https://localhost/

hack/dev/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ artifacts:
1515
internal:
1616
registry: registry.local:5000
1717
namespace: siderolabs
18+
1819
cache:
1920
oci:
2021
# private registry repository for cached assets

internal/artifacts/artifacts.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ type Options struct { //nolint:govet
2424
InsecureImageRegistry bool
2525
// MinVersion is the minimum version of Talos to use.
2626
MinVersion semver.Version
27+
// BrokenVersions are Talos versions that should be rejected when listing available versions.
28+
BrokenVersions []semver.Version
2729
// ImageVerifyOptions are the options for verifying the image signature.
2830
ImageVerifyOptions ImageVerifyOptions
2931
// TalosVersionRecheckInterval is the interval for rechecking Talos versions.

internal/artifacts/manager.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ func (m *Manager) Get(ctx context.Context, versionString string, arch Arch, kind
165165
return path, nil
166166
}
167167

168+
// GetBrokenTalosVersions returns the list of Talos versions marked as broken in the configuration.
169+
func (m *Manager) GetBrokenTalosVersions() []semver.Version {
170+
return slices.Clone(m.options.BrokenVersions)
171+
}
172+
168173
// GetTalosVersions returns a list of Talos versions available.
169174
func (m *Manager) GetTalosVersions(ctx context.Context) ([]semver.Version, error) {
170175
m.talosVersionsMu.Lock()

internal/artifacts/versions.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ func (m *Manager) fetchTalosVersions() (any, error) {
5555
return false // ignore versions below minimum
5656
}
5757

58+
if slices.ContainsFunc(m.options.BrokenVersions, version.EQ) {
59+
return false // ignore versions marked as broken
60+
}
61+
5862
if len(version.Pre) > 0 {
5963
if version.Major != maxVersion.Major || version.Minor != maxVersion.Minor {
6064
return false // ignore pre-releases for older versions

internal/frontend/http/meta.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,15 @@ import (
2121
)
2222

2323
// handleVersions handles list of Talos versions available.
24-
func (f *Frontend) handleVersions(ctx context.Context, w http.ResponseWriter, _ *http.Request, _ httprouter.Params) error {
24+
func (f *Frontend) handleVersions(ctx context.Context, w http.ResponseWriter, r *http.Request, _ httprouter.Params) error {
25+
if r.URL.Query().Get("broken") == "true" {
26+
return json.NewEncoder(w).Encode(
27+
xslices.Map(f.artifactsManager.GetBrokenTalosVersions(), func(v semver.Version) string {
28+
return "v" + v.String()
29+
}),
30+
)
31+
}
32+
2533
versions, err := f.artifactsManager.GetTalosVersions(ctx)
2634
if err != nil {
2735
return err
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
//go:build integration
6+
7+
package integration_test
8+
9+
import (
10+
"testing"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
15+
"github.com/siderolabs/image-factory/cmd/image-factory/cmd"
16+
"github.com/siderolabs/image-factory/pkg/client"
17+
)
18+
19+
// TestBrokenVersions verifies that versions listed in BrokenTalosVersions are
20+
// excluded from the /versions response and reported by /versions?broken=true.
21+
func TestBrokenVersions(t *testing.T) {
22+
t.Parallel()
23+
24+
const brokenVersion = "v1.13.1"
25+
26+
options := cmd.DefaultOptions
27+
options.Cache.OCI = cacheRepository.OCIRepositoryOptions
28+
options.Metrics.Namespace = "test_broken_versions"
29+
options.Build.BrokenTalosVersions = []string{brokenVersion[1:]}
30+
31+
ctx, listenAddr, _ := setupFactory(t, options)
32+
baseURL := "http://" + listenAddr
33+
34+
c, err := client.New(baseURL, clientAuthCredentials()...)
35+
require.NoError(t, err)
36+
37+
broken, err := c.BrokenVersions(ctx)
38+
require.NoError(t, err)
39+
40+
assert.Contains(t, broken, brokenVersion)
41+
42+
versions, err := c.Versions(ctx)
43+
require.NoError(t, err)
44+
45+
assert.NotContains(t, versions, brokenVersion)
46+
}

0 commit comments

Comments
 (0)