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:
Andrey Smirnov 2023-12-20 21:18:27 +04:00
parent c993bb6b5b
commit cde9b3954c
No known key found for this signature in database
GPG Key ID: FE042E3D4085A811
7 changed files with 136 additions and 80 deletions

View File

@ -56,3 +56,6 @@ const (
)
const tmpSuffix = "-tmp"
// ErrNotFoundTag tags the errors when the artifact is not found.
type ErrNotFoundTag = struct{}

View File

@ -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()

View File

@ -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()

View File

@ -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 {

View File

@ -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)
})
})
}

View File

@ -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
View 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)
}