mirror of
https://github.com/siderolabs/image-factory.git
synced 2025-09-21 13:51:08 +02:00
In v0.7.5 there is a bug - if the final page of the wizard is reloaded, the 'internal server error' is raised. This was due to the type mismatch on the `wizardFinal` return type and `extractParams` expected input type. This is bad development habbit to have two types which should be kept in sync, so instead just drop this function completely, as we don't need it. Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
781 lines
20 KiB
Go
781 lines
20 KiB
Go
// 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 http
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/blang/semver/v4"
|
|
"github.com/julienschmidt/httprouter"
|
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
"github.com/siderolabs/gen/maps"
|
|
"github.com/siderolabs/gen/value"
|
|
"github.com/siderolabs/gen/xslices"
|
|
"github.com/siderolabs/talos/pkg/machinery/constants"
|
|
"github.com/siderolabs/talos/pkg/machinery/imager/quirks"
|
|
"github.com/siderolabs/talos/pkg/machinery/platforms"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"github.com/siderolabs/image-factory/internal/artifacts"
|
|
"github.com/siderolabs/image-factory/internal/version"
|
|
"github.com/siderolabs/image-factory/pkg/schematic"
|
|
)
|
|
|
|
var templateFuncs template.FuncMap
|
|
|
|
func init() {
|
|
templateFuncs = template.FuncMap{
|
|
"dict": func(values ...any) (map[string]any, error) {
|
|
if len(values)%2 != 0 {
|
|
return nil, errors.New("invalid dict call")
|
|
}
|
|
|
|
dict := make(map[string]any, len(values)/2)
|
|
|
|
for i := 0; i < len(values); i += 2 {
|
|
key, ok := values[i].(string)
|
|
if !ok {
|
|
return nil, errors.New("dict keys must be strings")
|
|
}
|
|
|
|
dict[key] = values[i+1]
|
|
}
|
|
|
|
return dict, nil
|
|
},
|
|
"short_version": func(version string) string {
|
|
v, err := semver.ParseTolerant(version)
|
|
if err != nil {
|
|
return version
|
|
}
|
|
|
|
return fmt.Sprintf("v%d.%d", v.Major, v.Minor)
|
|
},
|
|
"in": func(haystack []string, needle string) bool {
|
|
return slices.Index(haystack, needle) != -1
|
|
},
|
|
"dynamic_template": func(name string, in any) (template.HTML, error) {
|
|
var out bytes.Buffer
|
|
|
|
if err := getTemplates().ExecuteTemplate(&out, name, in); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return template.HTML(out.String()), nil
|
|
},
|
|
"version_less": func(a, b string) (bool, error) {
|
|
av, err := semver.ParseTolerant(a)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error parsing version %q: %w", a, err)
|
|
}
|
|
|
|
bv, err := semver.ParseTolerant(b)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error parsing version %q: %w", b, err)
|
|
}
|
|
|
|
return av.LT(bv), nil
|
|
},
|
|
"t": func(localizer *i18n.Localizer, key string) string {
|
|
translated, err := localizer.Localize(&i18n.LocalizeConfig{MessageID: key})
|
|
if err != nil {
|
|
return "missing translation"
|
|
}
|
|
|
|
return translated
|
|
},
|
|
}
|
|
}
|
|
|
|
// Target constants.
|
|
const (
|
|
TargetMetal = "metal"
|
|
TargetCloud = "cloud"
|
|
TargetSBC = "sbc"
|
|
)
|
|
|
|
// handleUI handles '/'.
|
|
func (f *Frontend) handleUI(ctx context.Context, w http.ResponseWriter, r *http.Request, _ httprouter.Params) error {
|
|
if r.Method == http.MethodHead {
|
|
return nil
|
|
}
|
|
|
|
if r.URL.Query().Has("lang") {
|
|
lang := r.URL.Query().Get("lang")
|
|
|
|
if lang == "" {
|
|
http.Error(w, "missing lang param", http.StatusBadRequest)
|
|
|
|
return nil
|
|
}
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "lang",
|
|
Value: lang,
|
|
Path: "/",
|
|
MaxAge: 60 * 60 * 24 * 365,
|
|
HttpOnly: true,
|
|
})
|
|
|
|
returnURL := r.URL
|
|
query := returnURL.Query()
|
|
query.Del("lang")
|
|
returnURL.RawQuery = query.Encode()
|
|
|
|
w.Header().Set("Hx-Redirect", returnURL.String())
|
|
|
|
return nil
|
|
}
|
|
|
|
templateName, data, _, err := f.wizard(ctx, r, f.getLocalizer(r))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
|
|
if err = getTemplates().ExecuteTemplate(&buf, templateName+".html", data); err != nil {
|
|
return err
|
|
}
|
|
|
|
return getTemplates().ExecuteTemplate(w, "index.html", struct {
|
|
Version string
|
|
WizardHTML template.HTML
|
|
Localizer *i18n.Localizer
|
|
Bundle *i18n.Bundle
|
|
Lang string
|
|
}{
|
|
Version: version.Tag,
|
|
WizardHTML: template.HTML(buf.String()),
|
|
Localizer: f.getLocalizer(r),
|
|
Bundle: getLocalizerBundle(),
|
|
Lang: getCurrentLang(r),
|
|
})
|
|
}
|
|
|
|
// WizardParams encapsulates the parameters of the wizard.
|
|
//
|
|
// Some fields might be not set if we haven't reached that step yet.
|
|
type WizardParams struct { //nolint:govet
|
|
Target string
|
|
Version string
|
|
Arch string
|
|
Platform string
|
|
Board string
|
|
SecureBoot string
|
|
Extensions []string
|
|
Cmdline string
|
|
CmdlineSet bool
|
|
OverlayOptions string
|
|
|
|
SelectedTarget string
|
|
SelectedVersion string
|
|
SelectedArch string
|
|
SelectedPlatform string
|
|
SelectedBoard string
|
|
SelectedSecureBoot string
|
|
SelectedExtensions []string
|
|
SelectedCmdline string
|
|
SelectedOverlayOptions string
|
|
|
|
// Dynamically set fields.
|
|
PlatformMeta platforms.Platform
|
|
BoardMeta platforms.SBC
|
|
TalosctlMeta Talosctl
|
|
|
|
// Localizer
|
|
Localizer *i18n.Localizer
|
|
}
|
|
|
|
// Talosctl provides methods to generate paths for talosctl binaries.
|
|
type Talosctl struct{}
|
|
|
|
// TalosctlPaths generates paths for talosctl binaries based on the provided tuples.
|
|
func (Talosctl) TalosctlPaths(tuples []artifacts.TalosctlTuple) []string {
|
|
paths := make([]string, 0, len(tuples))
|
|
|
|
for _, tuple := range tuples {
|
|
path := fmt.Sprintf("talosctl-%s-%s%s", tuple.OS, tuple.Arch, tuple.Ext)
|
|
paths = append(paths, path)
|
|
}
|
|
|
|
slices.Sort(paths)
|
|
|
|
return paths
|
|
}
|
|
|
|
func getCurrentLang(r *http.Request) string {
|
|
lang := r.URL.Query().Get("lang")
|
|
|
|
if lang == "" {
|
|
if cookie, err := r.Cookie("lang"); err == nil {
|
|
lang = cookie.Value
|
|
}
|
|
}
|
|
|
|
if lang == "" {
|
|
lang = "en"
|
|
}
|
|
|
|
return lang
|
|
}
|
|
|
|
// WizardParamsFromRequest extracts the wizard parameters from the request.
|
|
func WizardParamsFromRequest(r *http.Request) WizardParams {
|
|
params := WizardParams{
|
|
Target: r.FormValue("target"),
|
|
Version: r.FormValue("version"),
|
|
Arch: r.FormValue("arch"),
|
|
Platform: r.FormValue("platform"),
|
|
Board: r.FormValue("board"),
|
|
SecureBoot: r.FormValue("secureboot"),
|
|
Extensions: r.Form["extensions"],
|
|
Cmdline: strings.TrimSpace(r.FormValue("cmdline")),
|
|
CmdlineSet: r.FormValue("cmdline-set") != "",
|
|
OverlayOptions: strings.TrimSpace(r.FormValue("overlay-options")),
|
|
|
|
SelectedTarget: r.FormValue("selected-target"),
|
|
SelectedVersion: r.FormValue("selected-version"),
|
|
SelectedArch: r.FormValue("selected-arch"),
|
|
SelectedPlatform: r.FormValue("selected-platform"),
|
|
SelectedBoard: r.FormValue("selected-board"),
|
|
SelectedSecureBoot: r.FormValue("selected-secureboot"),
|
|
SelectedExtensions: r.Form["selected-extensions"],
|
|
SelectedCmdline: r.FormValue("selected-cmdline"),
|
|
SelectedOverlayOptions: r.FormValue("selected-overlay-options"),
|
|
}
|
|
|
|
switch {
|
|
case params.Target == TargetMetal:
|
|
params.Platform = constants.PlatformMetal
|
|
params.PlatformMeta = platforms.MetalPlatform()
|
|
case params.Target == TargetSBC:
|
|
params.Platform = constants.PlatformMetal
|
|
|
|
if params.Board != "" {
|
|
if idx := slices.IndexFunc(platforms.SBCs(), func(p platforms.SBC) bool {
|
|
return p.Name == params.Board
|
|
}); idx != -1 {
|
|
params.BoardMeta = platforms.SBCs()[idx]
|
|
}
|
|
|
|
if params.Arch == "" {
|
|
if params.SelectedArch != "" {
|
|
params.SelectedBoard, params.Board = params.Board, ""
|
|
} else {
|
|
params.Arch = string(artifacts.ArchArm64)
|
|
}
|
|
}
|
|
}
|
|
case params.Target == TargetCloud && params.Platform != "":
|
|
if idx := slices.IndexFunc(platforms.CloudPlatforms(), func(p platforms.Platform) bool {
|
|
return p.Name == params.Platform
|
|
}); idx != -1 {
|
|
params.PlatformMeta = platforms.CloudPlatforms()[idx]
|
|
|
|
if len(params.PlatformMeta.Architectures) == 1 && params.Arch == "" {
|
|
if params.SelectedArch != "" {
|
|
// going back, reset platform choice
|
|
params.SelectedPlatform, params.Platform = params.Platform, ""
|
|
} else {
|
|
params.Arch = params.PlatformMeta.Architectures[0]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return params
|
|
}
|
|
|
|
// URLValues returns the URL values of the wizard parameters.
|
|
func (p WizardParams) URLValues() url.Values {
|
|
values := url.Values{}
|
|
|
|
if p.Target != "" {
|
|
values.Set("target", p.Target)
|
|
}
|
|
|
|
if p.Version != "" {
|
|
values.Set("version", p.Version)
|
|
}
|
|
|
|
if p.Arch != "" {
|
|
values.Set("arch", p.Arch)
|
|
}
|
|
|
|
if p.Platform != "" {
|
|
values.Set("platform", p.Platform)
|
|
}
|
|
|
|
if p.Board != "" {
|
|
values.Set("board", p.Board)
|
|
}
|
|
|
|
if p.SecureBoot != "" {
|
|
values.Set("secureboot", p.SecureBoot)
|
|
}
|
|
|
|
if len(p.Extensions) > 0 {
|
|
values["extensions"] = p.Extensions
|
|
}
|
|
|
|
if p.Cmdline != "" {
|
|
values.Set("cmdline", p.Cmdline)
|
|
}
|
|
|
|
if p.CmdlineSet {
|
|
values.Set("cmdline-set", "true")
|
|
}
|
|
|
|
if p.OverlayOptions != "" {
|
|
values.Set("overlay-options", p.OverlayOptions)
|
|
}
|
|
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return values
|
|
}
|
|
|
|
// wizardVersions handles the 'pick Talos version' step.
|
|
func (f *Frontend) wizardVersions(ctx context.Context, params WizardParams) (string, any, url.Values, error) {
|
|
versions, err := f.getTalosVersions(ctx, params.SelectedVersion)
|
|
if err != nil {
|
|
return "", nil, nil, err
|
|
}
|
|
|
|
return "wizard-versions",
|
|
struct {
|
|
WizardParams
|
|
Versions any
|
|
}{
|
|
WizardParams: params,
|
|
Versions: versions,
|
|
},
|
|
params.URLValues(),
|
|
nil
|
|
}
|
|
|
|
// wizardClouds handles the 'pick cloud platform' step.
|
|
func (f *Frontend) wizardClouds(_ context.Context, params WizardParams) (string, any, url.Values, error) {
|
|
if params.SelectedPlatform == "" {
|
|
params.SelectedPlatform = "aws"
|
|
}
|
|
|
|
talosVersion, _ := semver.ParseTolerant(params.Version) //nolint:errcheck
|
|
|
|
allPlatforms := platforms.CloudPlatforms()
|
|
|
|
allPlatforms = xslices.Filter(allPlatforms, func(p platforms.Platform) bool {
|
|
if value.IsZero(&p.MinVersion) {
|
|
return true
|
|
}
|
|
|
|
return talosVersion.GTE(p.MinVersion)
|
|
})
|
|
|
|
return "wizard-cloud",
|
|
struct {
|
|
WizardParams
|
|
Platforms []platforms.Platform
|
|
}{
|
|
WizardParams: params,
|
|
Platforms: allPlatforms,
|
|
},
|
|
params.URLValues(),
|
|
nil
|
|
}
|
|
|
|
// wizardSBCs handles the 'pick SBC' step.
|
|
func (f *Frontend) wizardSBCs(_ context.Context, params WizardParams) (string, any, url.Values, error) {
|
|
if params.SelectedBoard == "" {
|
|
params.SelectedBoard = "rpi_generic"
|
|
}
|
|
|
|
talosVersion, _ := semver.ParseTolerant(params.Version) //nolint:errcheck
|
|
|
|
allSBCs := platforms.SBCs()
|
|
|
|
allSBCs = xslices.Filter(allSBCs, func(p platforms.SBC) bool {
|
|
if value.IsZero(&p.MinVersion) {
|
|
return true
|
|
}
|
|
|
|
return talosVersion.GTE(p.MinVersion)
|
|
})
|
|
|
|
return "wizard-sbc",
|
|
struct {
|
|
WizardParams
|
|
SBCs []platforms.SBC
|
|
}{
|
|
WizardParams: params,
|
|
SBCs: allSBCs,
|
|
},
|
|
params.URLValues(),
|
|
nil
|
|
}
|
|
|
|
// wizardArch handles the 'pick architecture' step.
|
|
func (f *Frontend) wizardArch(_ context.Context, params WizardParams) (string, any, url.Values, error) {
|
|
talosVersion, _ := semver.ParseTolerant(params.Version) //nolint:errcheck
|
|
|
|
if params.SelectedArch == "" {
|
|
params.SelectedArch = "amd64"
|
|
}
|
|
|
|
return "wizard-arch",
|
|
struct {
|
|
WizardParams
|
|
SecureBootSupported bool
|
|
}{
|
|
WizardParams: params,
|
|
SecureBootSupported: talosVersion.GTE(semver.MustParse("1.5.0")) && (params.Target == TargetMetal || params.PlatformMeta.SecureBootSupported),
|
|
},
|
|
params.URLValues(),
|
|
nil
|
|
}
|
|
|
|
// wizardExtensions handles the 'pick extensions' step.
|
|
func (f *Frontend) wizardExtensions(ctx context.Context, params WizardParams) (string, any, url.Values, error) {
|
|
extensions, err := f.getOfficialExtensions(ctx, params.Version)
|
|
if err != nil {
|
|
return "", nil, nil, err
|
|
}
|
|
|
|
return "wizard-extensions",
|
|
struct {
|
|
WizardParams
|
|
AvailableExtensions []artifacts.ExtensionRef
|
|
}{
|
|
WizardParams: params,
|
|
AvailableExtensions: extensions,
|
|
},
|
|
params.URLValues(),
|
|
nil
|
|
}
|
|
|
|
// wizardCmdline handles the 'pick cmdline & overlay options' step.
|
|
func (f *Frontend) wizardCmdline(_ context.Context, params WizardParams) (string, any, url.Values, error) {
|
|
return "wizard-cmdline",
|
|
struct {
|
|
WizardParams
|
|
|
|
OverlayOptionsEnabled bool
|
|
}{
|
|
WizardParams: params,
|
|
|
|
OverlayOptionsEnabled: params.Target == TargetSBC && quirks.New(params.Version).SupportsOverlay(),
|
|
},
|
|
params.URLValues(),
|
|
nil
|
|
}
|
|
|
|
// wizardFinal handles the 'final' step.
|
|
func (f *Frontend) wizardFinal(ctx context.Context, params WizardParams) (string, any, url.Values, error) {
|
|
talosVersion, _ := semver.ParseTolerant(params.Version) //nolint:errcheck
|
|
|
|
// every parameter is set now, create the schematic
|
|
var extraArgs []string
|
|
|
|
if params.Cmdline != "" {
|
|
extraArgs = strings.Split(params.Cmdline, " ")
|
|
}
|
|
|
|
extensions := xslices.Filter(params.Extensions, func(ext string) bool {
|
|
return ext != "-"
|
|
})
|
|
|
|
slices.Sort(extensions)
|
|
|
|
var overlay schematic.Overlay
|
|
|
|
if params.Target == TargetSBC && quirks.New(params.Version).SupportsOverlay() {
|
|
overlay.Name = params.BoardMeta.OverlayName
|
|
overlay.Image = params.BoardMeta.OverlayImage
|
|
|
|
var overlayOptsParsed map[string]any
|
|
|
|
if err := yaml.Unmarshal([]byte(params.OverlayOptions), &overlayOptsParsed); err != nil {
|
|
return "", nil, nil, fmt.Errorf("error parsing overlay options: %w", err)
|
|
}
|
|
|
|
overlay.Options = overlayOptsParsed
|
|
}
|
|
|
|
requestedSchematic := schematic.Schematic{
|
|
Overlay: overlay,
|
|
Customization: schematic.Customization{
|
|
ExtraKernelArgs: extraArgs,
|
|
SystemExtensions: schematic.SystemExtensions{
|
|
OfficialExtensions: extensions,
|
|
},
|
|
},
|
|
}
|
|
|
|
schematicID, err := f.schematicFactory.Put(ctx, &requestedSchematic)
|
|
if err != nil {
|
|
return "", nil, nil, err
|
|
}
|
|
|
|
marshaled, err := requestedSchematic.Marshal()
|
|
if err != nil {
|
|
return "", nil, nil, err
|
|
}
|
|
|
|
version := "v" + params.Version
|
|
|
|
installerImage := fmt.Sprintf("%s/installer/%s:%s", f.options.ExternalURL.Host, schematicID, version)
|
|
secureBootInstallerImage := fmt.Sprintf("%s/installer-secureboot/%s:%s", f.options.ExternalURL.Host, schematicID, version)
|
|
|
|
if quirks.New(version).SupportsUnifiedInstaller() {
|
|
installerImage = fmt.Sprintf("%s/%s-installer/%s:%s", f.options.ExternalURL.Host, params.Platform, schematicID, version)
|
|
secureBootInstallerImage = fmt.Sprintf("%s/%s-installer-secureboot/%s:%s", f.options.ExternalURL.Host, params.Platform, schematicID, version)
|
|
}
|
|
|
|
var talosctlTuples []artifacts.TalosctlTuple
|
|
if talosVersion.GTE(semver.MustParse("1.11.0-alpha.3")) {
|
|
talosctlTuples, err = f.getTalosctlTuples(ctx, params.Version)
|
|
if err != nil {
|
|
return "", nil, nil, err
|
|
}
|
|
}
|
|
|
|
return "wizard-final",
|
|
struct {
|
|
WizardParams
|
|
|
|
Schematic string
|
|
Marshaled string
|
|
|
|
ImageBaseURL *url.URL
|
|
PXEBaseURL *url.URL
|
|
TalosctlBaseURL *url.URL
|
|
InstallerImage string
|
|
SecureBootInstallerImage string
|
|
|
|
TalosctlTuples []artifacts.TalosctlTuple
|
|
|
|
TroubleshootingGuideAvailable bool
|
|
ProductionGuideAvailable bool
|
|
TalosctlAvailable bool
|
|
}{
|
|
WizardParams: params,
|
|
|
|
Schematic: schematicID,
|
|
Marshaled: string(marshaled),
|
|
|
|
ImageBaseURL: f.options.ExternalURL.JoinPath("image", schematicID, version),
|
|
PXEBaseURL: f.options.ExternalPXEURL.JoinPath("pxe", schematicID, version),
|
|
TalosctlBaseURL: f.options.ExternalURL.JoinPath("talosctl", version),
|
|
|
|
InstallerImage: installerImage,
|
|
SecureBootInstallerImage: secureBootInstallerImage,
|
|
|
|
TalosctlTuples: talosctlTuples,
|
|
|
|
TroubleshootingGuideAvailable: talosVersion.GTE(semver.MustParse("1.6.0")),
|
|
ProductionGuideAvailable: talosVersion.GTE(semver.MustParse("1.5.0")),
|
|
TalosctlAvailable: talosVersion.GTE(semver.MustParse("1.11.0-alpha.3")),
|
|
},
|
|
params.URLValues(),
|
|
nil
|
|
}
|
|
|
|
func (f *Frontend) wizard(ctx context.Context, r *http.Request, localizer *i18n.Localizer) (string, any, url.Values, error) {
|
|
params := WizardParamsFromRequest(r)
|
|
params.Localizer = localizer
|
|
|
|
switch {
|
|
case params.Target == "":
|
|
if params.SelectedTarget == "" {
|
|
params.SelectedTarget = TargetMetal
|
|
}
|
|
|
|
return "wizard-start", params, nil, nil
|
|
case params.Version == "":
|
|
return f.wizardVersions(ctx, params)
|
|
case params.Target == TargetCloud && params.Platform == "":
|
|
return f.wizardClouds(ctx, params)
|
|
case params.Target == TargetSBC && params.Board == "":
|
|
return f.wizardSBCs(ctx, params)
|
|
case params.Arch == "":
|
|
return f.wizardArch(ctx, params)
|
|
case len(params.Extensions) == 0:
|
|
return f.wizardExtensions(ctx, params)
|
|
case !params.CmdlineSet:
|
|
return f.wizardCmdline(ctx, params)
|
|
default:
|
|
return f.wizardFinal(ctx, params)
|
|
}
|
|
}
|
|
|
|
// handleUIWizard handles '/ui/wizard'.
|
|
func (f *Frontend) handleUIWizard(ctx context.Context, w http.ResponseWriter, r *http.Request, _ httprouter.Params) error {
|
|
templateName, data, query, err := f.wizard(ctx, r, f.getLocalizer(r))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if query != nil {
|
|
w.Header().Set("Hx-Push-Url", "/?"+query.Encode())
|
|
} else {
|
|
w.Header().Set("Hx-Push-Url", "/")
|
|
}
|
|
|
|
return getTemplates().ExecuteTemplate(w, templateName+".html", data)
|
|
}
|
|
|
|
// handleUIWizard handles '/ui/extensions-list'.
|
|
func (f *Frontend) handleUIExtensionsList(ctx context.Context, w http.ResponseWriter, r *http.Request, _ httprouter.Params) error {
|
|
version := r.FormValue("version")
|
|
filter := r.FormValue("search")
|
|
extensions := r.Form["extensions"]
|
|
|
|
extensionList, err := f.getOfficialExtensions(ctx, version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if filter != "" {
|
|
extensionList = xslices.Filter(extensionList, func(ext artifacts.ExtensionRef) bool {
|
|
if slices.Index(extensions, ext.TaggedReference.RepositoryStr()) != -1 {
|
|
// selected
|
|
return true
|
|
}
|
|
|
|
if strings.Contains(strings.ToLower(ext.TaggedReference.String()), strings.ToLower(filter)) {
|
|
return true
|
|
}
|
|
|
|
if strings.Contains(strings.ToLower(ext.Description), strings.ToLower(filter)) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
})
|
|
}
|
|
|
|
return getTemplates().ExecuteTemplate(w, "extensions-list.html", struct {
|
|
SelectedExtensions []string
|
|
AvailableExtensions []artifacts.ExtensionRef
|
|
}{
|
|
SelectedExtensions: extensions,
|
|
AvailableExtensions: extensionList,
|
|
})
|
|
}
|
|
|
|
func (f *Frontend) getTalosVersions(ctx context.Context, selectedVersion string) (any, error) {
|
|
versions, err := f.artifactsManager.GetTalosVersions(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
versions = slices.Clone(versions)
|
|
slices.Reverse(versions)
|
|
|
|
var latestStable semver.Version
|
|
|
|
for _, v := range versions {
|
|
if len(v.Pre) == 0 {
|
|
latestStable = v
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if selectedVersion == "" {
|
|
selectedVersion = latestStable.String()
|
|
}
|
|
|
|
type versionGroup struct {
|
|
Label string
|
|
Versions []string
|
|
}
|
|
|
|
type versionList struct {
|
|
DefaultVersion string
|
|
LatestStable string
|
|
Groups []versionGroup
|
|
}
|
|
|
|
versionGroupLabel := func(v semver.Version) string {
|
|
if len(v.Pre) > 0 {
|
|
return fmt.Sprintf("%d.%d-pre", v.Major, v.Minor)
|
|
}
|
|
|
|
return fmt.Sprintf("%d.%d", v.Major, v.Minor)
|
|
}
|
|
|
|
groups := map[string][]semver.Version{}
|
|
|
|
for _, v := range versions {
|
|
label := versionGroupLabel(v)
|
|
|
|
groups[label] = append(groups[label], v)
|
|
}
|
|
|
|
groupLabels := maps.Keys(groups)
|
|
slices.SortFunc(groupLabels, func(a, b string) int {
|
|
va, _ := semver.ParseTolerant(a) //nolint:errcheck
|
|
vb, _ := semver.ParseTolerant(b) //nolint:errcheck
|
|
|
|
return -va.Compare(vb)
|
|
})
|
|
|
|
return versionList{
|
|
DefaultVersion: selectedVersion,
|
|
LatestStable: latestStable.String(),
|
|
Groups: xslices.Map(groupLabels, func(label string) versionGroup {
|
|
return versionGroup{
|
|
Label: label,
|
|
Versions: xslices.Map(groups[label], semver.Version.String),
|
|
}
|
|
}),
|
|
}, nil
|
|
}
|
|
|
|
// handleUIVersionDoc handles '/ui/version-doc'.
|
|
func (f *Frontend) handleUIVersionDoc(_ context.Context, w http.ResponseWriter, r *http.Request, _ httprouter.Params) error {
|
|
version := r.FormValue("version")
|
|
|
|
return getTemplates().ExecuteTemplate(w, "version-doc.html", struct {
|
|
Localizer *i18n.Localizer
|
|
Version string
|
|
}{
|
|
Version: version,
|
|
Localizer: f.getLocalizer(r),
|
|
})
|
|
}
|
|
|
|
func (f *Frontend) getOfficialExtensions(ctx context.Context, version string) ([]artifacts.ExtensionRef, error) {
|
|
extensions, err := f.artifactsManager.GetOfficialExtensions(ctx, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return xslices.Filter(extensions, func(ext artifacts.ExtensionRef) bool {
|
|
return ext.TaggedReference.Context().RepositoryStr() != "siderolabs/metal-agent" // hide the internal metal-agent extension on the UI
|
|
}), nil
|
|
}
|
|
|
|
func (f *Frontend) getTalosctlTuples(ctx context.Context, version string) ([]artifacts.TalosctlTuple, error) {
|
|
talosctlTuples, err := f.artifactsManager.GetTalosctlTuples(ctx, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return talosctlTuples, nil
|
|
}
|