mirror of
https://github.com/siderolabs/image-factory.git
synced 2025-12-05 09:31:17 +01:00
fix: update Talos version listing
Now Image Factory filters out pre-release versions for all releases but the last one. In the UI, now pre-release versions are shown. Return proper 404 not found when someone requests something for an unsupported version. Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
This commit is contained in:
parent
c993bb6b5b
commit
cde9b3954c
@ -56,3 +56,6 @@ const (
|
||||
)
|
||||
|
||||
const tmpSuffix = "-tmp"
|
||||
|
||||
// ErrNotFoundTag tags the errors when the artifact is not found.
|
||||
type ErrNotFoundTag = struct{}
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -16,6 +17,7 @@ import (
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/siderolabs/gen/xerrors"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
@ -96,6 +98,19 @@ func (m *Manager) Close() error {
|
||||
return os.RemoveAll(m.storagePath)
|
||||
}
|
||||
|
||||
func (m *Manager) validateTalosVersion(ctx context.Context, version semver.Version) error {
|
||||
availableVersion, err := m.GetTalosVersions(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get available Talos versions: %w", err)
|
||||
}
|
||||
|
||||
if !slices.ContainsFunc(availableVersion, version.Equals) {
|
||||
return xerrors.NewTaggedf[ErrNotFoundTag]("version %s is not available", version)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the artifact path for the given version, arch and kind.
|
||||
func (m *Manager) Get(ctx context.Context, versionString string, arch Arch, kind Kind) (string, error) {
|
||||
version, err := semver.Parse(versionString)
|
||||
@ -103,8 +118,8 @@ func (m *Manager) Get(ctx context.Context, versionString string, arch Arch, kind
|
||||
return "", fmt.Errorf("failed to parse version: %w", err)
|
||||
}
|
||||
|
||||
if version.LT(m.options.MinVersion) {
|
||||
return "", fmt.Errorf("version %s is not supported, minimum is %s", version, m.options.MinVersion)
|
||||
if err = m.validateTalosVersion(ctx, version); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tag := "v" + version.String()
|
||||
@ -172,8 +187,8 @@ func (m *Manager) GetOfficialExtensions(ctx context.Context, versionString strin
|
||||
return nil, fmt.Errorf("failed to parse version: %w", err)
|
||||
}
|
||||
|
||||
if version.LT(m.options.MinVersion) {
|
||||
return nil, fmt.Errorf("version %s is not supported, minimum is %s", version, m.options.MinVersion)
|
||||
if err = m.validateTalosVersion(ctx, version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tag := "v" + version.String()
|
||||
@ -213,8 +228,8 @@ func (m *Manager) GetInstallerImage(ctx context.Context, arch Arch, versionStrin
|
||||
return "", fmt.Errorf("failed to parse version: %w", err)
|
||||
}
|
||||
|
||||
if version.LT(m.options.MinVersion) {
|
||||
return "", fmt.Errorf("version %s is not supported, minimum is %s", version, m.options.MinVersion)
|
||||
if err = m.validateTalosVersion(ctx, version); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tag := "v" + version.String()
|
||||
|
||||
@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/siderolabs/gen/xslices"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@ -41,32 +42,41 @@ func (m *Manager) fetchTalosVersions() (any, error) {
|
||||
continue // ignore invalid versions
|
||||
}
|
||||
|
||||
if version.LT(m.options.MinVersion) {
|
||||
continue // ignore versions below minimum
|
||||
}
|
||||
|
||||
// filter out intermediate versions
|
||||
if len(version.Pre) > 0 {
|
||||
if len(version.Pre) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !(version.Pre[0].VersionStr == "alpha" || version.Pre[0].VersionStr == "beta") {
|
||||
continue
|
||||
}
|
||||
|
||||
if !version.Pre[1].IsNumeric() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
versions = append(versions, version)
|
||||
}
|
||||
|
||||
slices.SortFunc(versions, func(a, b semver.Version) int {
|
||||
return a.Compare(b)
|
||||
// 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
|
||||
}
|
||||
|
||||
if !(version.Pre[0].VersionStr == "alpha" || version.Pre[0].VersionStr == "beta") {
|
||||
return false
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@ -16,7 +16,6 @@ import (
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/siderolabs/gen/xslices"
|
||||
|
||||
"github.com/siderolabs/image-factory/internal/artifacts"
|
||||
"github.com/siderolabs/image-factory/internal/version"
|
||||
@ -61,10 +60,6 @@ func (f *Frontend) handleUIVersions(ctx context.Context, w http.ResponseWriter,
|
||||
return err
|
||||
}
|
||||
|
||||
versions = xslices.Filter(versions, func(v semver.Version) bool {
|
||||
return len(v.Pre) == 0
|
||||
})
|
||||
|
||||
slices.Reverse(versions)
|
||||
|
||||
return templates.ExecuteTemplate(w, "versions.html", struct {
|
||||
|
||||
@ -8,6 +8,7 @@ package integration_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/siderolabs/gen/xslices"
|
||||
@ -17,24 +18,6 @@ import (
|
||||
"github.com/siderolabs/image-factory/pkg/client"
|
||||
)
|
||||
|
||||
func getVersions(ctx context.Context, t *testing.T, c *client.Client) []string {
|
||||
t.Helper()
|
||||
|
||||
versions, err := c.Versions(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
return versions
|
||||
}
|
||||
|
||||
func getExtensions(ctx context.Context, t *testing.T, c *client.Client, talosVersion string) []client.ExtensionInfo {
|
||||
t.Helper()
|
||||
|
||||
versions, err := c.ExtensionsVersions(ctx, talosVersion)
|
||||
require.NoError(t, err)
|
||||
|
||||
return versions
|
||||
}
|
||||
|
||||
func testMetaFrontend(ctx context.Context, t *testing.T, baseURL string) {
|
||||
c, err := client.New(baseURL)
|
||||
require.NoError(t, err)
|
||||
@ -42,7 +25,8 @@ func testMetaFrontend(ctx context.Context, t *testing.T, baseURL string) {
|
||||
t.Run("versions", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
versions := getVersions(ctx, t, c)
|
||||
versions, err := c.Versions(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Greater(t, len(versions), 10)
|
||||
})
|
||||
@ -53,13 +37,15 @@ func testMetaFrontend(ctx context.Context, t *testing.T, baseURL string) {
|
||||
talosVersions := []string{
|
||||
"v1.5.0",
|
||||
"v1.5.1",
|
||||
"v1.6.0",
|
||||
}
|
||||
|
||||
for _, talosVersion := range talosVersions {
|
||||
t.Run(talosVersion, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
extensions := getExtensions(ctx, t, c, talosVersion)
|
||||
extensions, err := c.ExtensionsVersions(ctx, talosVersion)
|
||||
require.NoError(t, err)
|
||||
|
||||
names := xslices.Map(extensions, func(ext client.ExtensionInfo) string {
|
||||
return ext.Name
|
||||
@ -70,5 +56,17 @@ func testMetaFrontend(ctx context.Context, t *testing.T, baseURL string) {
|
||||
assert.Contains(t, names, "siderolabs/nvidia-open-gpu-kernel-modules")
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("invalid version", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := c.ExtensionsVersions(ctx, "v1.5.0-alpha.0")
|
||||
require.Error(t, err)
|
||||
|
||||
var httpError *client.HTTPError
|
||||
require.ErrorAs(t, err, &httpError)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, httpError.Code)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -14,26 +14,9 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/siderolabs/gen/xerrors"
|
||||
|
||||
"github.com/siderolabs/image-factory/pkg/schematic"
|
||||
)
|
||||
|
||||
// InvalidSchematicError is parsed from 400 response from the server.
|
||||
type InvalidSchematicError struct {
|
||||
Details string
|
||||
}
|
||||
|
||||
// Error implements error interface.
|
||||
func (e *InvalidSchematicError) Error() string {
|
||||
return fmt.Sprintf("invalid schematic: %s", e.Details)
|
||||
}
|
||||
|
||||
// IsInvalidSchematicError checks if the error is invalid schematic.
|
||||
func IsInvalidSchematicError(err error) bool {
|
||||
return xerrors.TypeIs[*InvalidSchematicError](err)
|
||||
}
|
||||
|
||||
// ExtensionInfo defines extensions versions list response item.
|
||||
type ExtensionInfo struct {
|
||||
Name string `json:"name"`
|
||||
@ -143,20 +126,27 @@ func (c *Client) do(ctx context.Context, method, uri string, requestData []byte,
|
||||
}
|
||||
|
||||
func (c *Client) checkError(resp *http.Response) error {
|
||||
if resp.StatusCode == http.StatusBadRequest {
|
||||
details, err := io.ReadAll(resp.Body)
|
||||
const maxErrorBody = 8192
|
||||
|
||||
if resp.StatusCode < http.StatusBadRequest {
|
||||
return nil
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, maxErrorBody))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = &HTTPError{
|
||||
Code: resp.StatusCode,
|
||||
Message: string(body),
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusBadRequest {
|
||||
return &InvalidSchematicError{
|
||||
Details: string(details),
|
||||
e: err,
|
||||
}
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
return fmt.Errorf("request failed, code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
45
pkg/client/errors.go
Normal file
45
pkg/client/errors.go
Normal file
@ -0,0 +1,45 @@
|
||||
// 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 client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/siderolabs/gen/xerrors"
|
||||
)
|
||||
|
||||
// HTTPError is a generic HTTP error wrapper.
|
||||
type HTTPError struct {
|
||||
Message string
|
||||
Code int
|
||||
}
|
||||
|
||||
// Error implements error interface.
|
||||
func (e *HTTPError) Error() string {
|
||||
return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// IsHTTPErrorCode checks if the error is HTTP error with a specific doe.
|
||||
func IsHTTPErrorCode(err error, code int) bool {
|
||||
var expected *HTTPError
|
||||
|
||||
return errors.As(err, &expected) && expected.Code == code
|
||||
}
|
||||
|
||||
// InvalidSchematicError is parsed from 400 response from the server.
|
||||
type InvalidSchematicError struct {
|
||||
e error
|
||||
}
|
||||
|
||||
// Error implements error interface.
|
||||
func (e *InvalidSchematicError) Error() string {
|
||||
return fmt.Sprintf("invalid schematic: %s", e.e)
|
||||
}
|
||||
|
||||
// IsInvalidSchematicError checks if the error is invalid schematic.
|
||||
func IsInvalidSchematicError(err error) bool {
|
||||
return xerrors.TypeIs[*InvalidSchematicError](err)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user