mirror of
https://github.com/siderolabs/image-factory.git
synced 2025-12-05 17:41:32 +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"
|
const tmpSuffix = "-tmp"
|
||||||
|
|
||||||
|
// ErrNotFoundTag tags the errors when the artifact is not found.
|
||||||
|
type ErrNotFoundTag = struct{}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ import (
|
|||||||
"github.com/google/go-containerregistry/pkg/name"
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
|
"github.com/siderolabs/gen/xerrors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
)
|
)
|
||||||
@ -96,6 +98,19 @@ func (m *Manager) Close() error {
|
|||||||
return os.RemoveAll(m.storagePath)
|
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.
|
// 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) {
|
func (m *Manager) Get(ctx context.Context, versionString string, arch Arch, kind Kind) (string, error) {
|
||||||
version, err := semver.Parse(versionString)
|
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)
|
return "", fmt.Errorf("failed to parse version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if version.LT(m.options.MinVersion) {
|
if err = m.validateTalosVersion(ctx, version); err != nil {
|
||||||
return "", fmt.Errorf("version %s is not supported, minimum is %s", version, m.options.MinVersion)
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
tag := "v" + version.String()
|
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)
|
return nil, fmt.Errorf("failed to parse version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if version.LT(m.options.MinVersion) {
|
if err = m.validateTalosVersion(ctx, version); err != nil {
|
||||||
return nil, fmt.Errorf("version %s is not supported, minimum is %s", version, m.options.MinVersion)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tag := "v" + version.String()
|
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)
|
return "", fmt.Errorf("failed to parse version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if version.LT(m.options.MinVersion) {
|
if err = m.validateTalosVersion(ctx, version); err != nil {
|
||||||
return "", fmt.Errorf("version %s is not supported, minimum is %s", version, m.options.MinVersion)
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
tag := "v" + version.String()
|
tag := "v" + version.String()
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import (
|
|||||||
|
|
||||||
"github.com/blang/semver/v4"
|
"github.com/blang/semver/v4"
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
|
"github.com/siderolabs/gen/xslices"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -41,32 +42,41 @@ func (m *Manager) fetchTalosVersions() (any, error) {
|
|||||||
continue // ignore invalid versions
|
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)
|
versions = append(versions, version)
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.SortFunc(versions, func(a, b semver.Version) int {
|
// find "current" maximum version
|
||||||
return a.Compare(b)
|
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.talosVersionsMu.Lock()
|
||||||
m.talosVersions, m.talosVersionsTimestamp = versions, time.Now()
|
m.talosVersions, m.talosVersionsTimestamp = versions, time.Now()
|
||||||
m.talosVersionsMu.Unlock()
|
m.talosVersionsMu.Unlock()
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import (
|
|||||||
|
|
||||||
"github.com/blang/semver/v4"
|
"github.com/blang/semver/v4"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"github.com/siderolabs/gen/xslices"
|
|
||||||
|
|
||||||
"github.com/siderolabs/image-factory/internal/artifacts"
|
"github.com/siderolabs/image-factory/internal/artifacts"
|
||||||
"github.com/siderolabs/image-factory/internal/version"
|
"github.com/siderolabs/image-factory/internal/version"
|
||||||
@ -61,10 +60,6 @@ func (f *Frontend) handleUIVersions(ctx context.Context, w http.ResponseWriter,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
versions = xslices.Filter(versions, func(v semver.Version) bool {
|
|
||||||
return len(v.Pre) == 0
|
|
||||||
})
|
|
||||||
|
|
||||||
slices.Reverse(versions)
|
slices.Reverse(versions)
|
||||||
|
|
||||||
return templates.ExecuteTemplate(w, "versions.html", struct {
|
return templates.ExecuteTemplate(w, "versions.html", struct {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ package integration_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/siderolabs/gen/xslices"
|
"github.com/siderolabs/gen/xslices"
|
||||||
@ -17,24 +18,6 @@ import (
|
|||||||
"github.com/siderolabs/image-factory/pkg/client"
|
"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) {
|
func testMetaFrontend(ctx context.Context, t *testing.T, baseURL string) {
|
||||||
c, err := client.New(baseURL)
|
c, err := client.New(baseURL)
|
||||||
require.NoError(t, err)
|
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.Run("versions", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
versions := getVersions(ctx, t, c)
|
versions, err := c.Versions(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Greater(t, len(versions), 10)
|
assert.Greater(t, len(versions), 10)
|
||||||
})
|
})
|
||||||
@ -53,13 +37,15 @@ func testMetaFrontend(ctx context.Context, t *testing.T, baseURL string) {
|
|||||||
talosVersions := []string{
|
talosVersions := []string{
|
||||||
"v1.5.0",
|
"v1.5.0",
|
||||||
"v1.5.1",
|
"v1.5.1",
|
||||||
|
"v1.6.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, talosVersion := range talosVersions {
|
for _, talosVersion := range talosVersions {
|
||||||
t.Run(talosVersion, func(t *testing.T) {
|
t.Run(talosVersion, func(t *testing.T) {
|
||||||
t.Parallel()
|
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 {
|
names := xslices.Map(extensions, func(ext client.ExtensionInfo) string {
|
||||||
return ext.Name
|
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")
|
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/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/siderolabs/gen/xerrors"
|
|
||||||
|
|
||||||
"github.com/siderolabs/image-factory/pkg/schematic"
|
"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.
|
// ExtensionInfo defines extensions versions list response item.
|
||||||
type ExtensionInfo struct {
|
type ExtensionInfo struct {
|
||||||
Name string `json:"name"`
|
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 {
|
func (c *Client) checkError(resp *http.Response) error {
|
||||||
|
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 {
|
if resp.StatusCode == http.StatusBadRequest {
|
||||||
details, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &InvalidSchematicError{
|
return &InvalidSchematicError{
|
||||||
Details: string(details),
|
e: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode >= http.StatusBadRequest {
|
return err
|
||||||
return fmt.Errorf("request failed, code %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
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