From d3592671ecc6cbc20dfb0b5a056e9ec1e72e93dc Mon Sep 17 00:00:00 2001 From: Edward Sammut Alessi Date: Wed, 29 Apr 2026 16:54:31 +0200 Subject: [PATCH] feat: download talosctl directly from factory Download talosctl binaries from factory instead of Github Signed-off-by: Edward Sammut Alessi --- client/api/omni/specs/virtual.pb.go | 75 ++++++- client/api/omni/specs/virtual.proto | 5 + client/api/omni/specs/virtual_vtproto.pb.go | 200 ++++++++++++++++++ client/pkg/omni/resources/virtual/quirk.go | 49 +++++ client/pkg/omni/resources/virtual/virtual.go | 1 + frontend/e2e/talemu/home.spec.ts | 20 ++ frontend/src/api/omni/specs/virtual.pb.ts | 5 + frontend/src/api/resources.ts | 1 + frontend/src/components/Modals/Modal.vue | 5 +- frontend/src/methods/useTalosctlDownloads.ts | 79 ++++--- .../create/confirmation.stories.ts | 50 +++-- .../components/DownloadTalosctl.stories.ts | 109 ++++++---- .../Home/components/DownloadTalosctl.vue | 181 +++++++++++----- .../views/InstallationMedia/Confirmation.vue | 21 +- go.mod | 9 +- go.sum | 24 +-- internal/backend/runtime/omni/state_access.go | 2 + .../backend/runtime/omni/virtual/state.go | 45 ++++ internal/backend/server.go | 162 +++----------- internal/integration/auth_test.go | 5 + 20 files changed, 725 insertions(+), 323 deletions(-) create mode 100644 client/pkg/omni/resources/virtual/quirk.go diff --git a/client/api/omni/specs/virtual.pb.go b/client/api/omni/specs/virtual.pb.go index 2439efaa..38698283 100644 --- a/client/api/omni/specs/virtual.pb.go +++ b/client/api/omni/specs/virtual.pb.go @@ -927,6 +927,58 @@ func (x *SupportSpec) GetOfficeHours() *OfficeHoursConfig { return nil } +type QuirksSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + SupportsUnifiedInstaller bool `protobuf:"varint,1,opt,name=supports_unified_installer,json=supportsUnifiedInstaller,proto3" json:"supports_unified_installer,omitempty"` + SupportsFactoryTalosctl bool `protobuf:"varint,2,opt,name=supports_factory_talosctl,json=supportsFactoryTalosctl,proto3" json:"supports_factory_talosctl,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QuirksSpec) Reset() { + *x = QuirksSpec{} + mi := &file_omni_specs_virtual_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QuirksSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QuirksSpec) ProtoMessage() {} + +func (x *QuirksSpec) ProtoReflect() protoreflect.Message { + mi := &file_omni_specs_virtual_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QuirksSpec.ProtoReflect.Descriptor instead. +func (*QuirksSpec) Descriptor() ([]byte, []int) { + return file_omni_specs_virtual_proto_rawDescGZIP(), []int{9} +} + +func (x *QuirksSpec) GetSupportsUnifiedInstaller() bool { + if x != nil { + return x.SupportsUnifiedInstaller + } + return false +} + +func (x *QuirksSpec) GetSupportsFactoryTalosctl() bool { + if x != nil { + return x.SupportsFactoryTalosctl + } + return false +} + type LabelsCompletionSpec_Values struct { state protoimpl.MessageState `protogen:"open.v1"` Items []string `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` @@ -936,7 +988,7 @@ type LabelsCompletionSpec_Values struct { func (x *LabelsCompletionSpec_Values) Reset() { *x = LabelsCompletionSpec_Values{} - mi := &file_omni_specs_virtual_proto_msgTypes[9] + mi := &file_omni_specs_virtual_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -948,7 +1000,7 @@ func (x *LabelsCompletionSpec_Values) String() string { func (*LabelsCompletionSpec_Values) ProtoMessage() {} func (x *LabelsCompletionSpec_Values) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_virtual_proto_msgTypes[9] + mi := &file_omni_specs_virtual_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1068,7 +1120,11 @@ const file_omni_specs_virtual_proto_rawDesc = "" + "\bmeet_url\x18\b \x01(\tR\ameetUrl\"s\n" + "\vSupportSpec\x12'\n" + "\x0fsupport_enabled\x18\x01 \x01(\bR\x0esupportEnabled\x12;\n" + - "\foffice_hours\x18\x02 \x01(\v2\x18.specs.OfficeHoursConfigR\vofficeHoursB2Z0github.com/siderolabs/omni/client/api/omni/specsb\x06proto3" + "\foffice_hours\x18\x02 \x01(\v2\x18.specs.OfficeHoursConfigR\vofficeHours\"\x86\x01\n" + + "\n" + + "QuirksSpec\x12<\n" + + "\x1asupports_unified_installer\x18\x01 \x01(\bR\x18supportsUnifiedInstaller\x12:\n" + + "\x19supports_factory_talosctl\x18\x02 \x01(\bR\x17supportsFactoryTalosctlB2Z0github.com/siderolabs/omni/client/api/omni/specsb\x06proto3" var ( file_omni_specs_virtual_proto_rawDescOnce sync.Once @@ -1083,7 +1139,7 @@ func file_omni_specs_virtual_proto_rawDescGZIP() []byte { } var file_omni_specs_virtual_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_omni_specs_virtual_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_omni_specs_virtual_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_omni_specs_virtual_proto_goTypes = []any{ (PlatformConfigSpec_BootMethod)(0), // 0: specs.PlatformConfigSpec.BootMethod (PlatformConfigSpec_Arch)(0), // 1: specs.PlatformConfigSpec.Arch @@ -1096,15 +1152,16 @@ var file_omni_specs_virtual_proto_goTypes = []any{ (*SBCConfigSpec)(nil), // 8: specs.SBCConfigSpec (*OfficeHoursConfig)(nil), // 9: specs.OfficeHoursConfig (*SupportSpec)(nil), // 10: specs.SupportSpec - (*LabelsCompletionSpec_Values)(nil), // 11: specs.LabelsCompletionSpec.Values - nil, // 12: specs.LabelsCompletionSpec.ItemsEntry + (*QuirksSpec)(nil), // 11: specs.QuirksSpec + (*LabelsCompletionSpec_Values)(nil), // 12: specs.LabelsCompletionSpec.Values + nil, // 13: specs.LabelsCompletionSpec.ItemsEntry } var file_omni_specs_virtual_proto_depIdxs = []int32{ - 12, // 0: specs.LabelsCompletionSpec.items:type_name -> specs.LabelsCompletionSpec.ItemsEntry + 13, // 0: specs.LabelsCompletionSpec.items:type_name -> specs.LabelsCompletionSpec.ItemsEntry 1, // 1: specs.PlatformConfigSpec.architectures:type_name -> specs.PlatformConfigSpec.Arch 0, // 2: specs.PlatformConfigSpec.boot_methods:type_name -> specs.PlatformConfigSpec.BootMethod 9, // 3: specs.SupportSpec.office_hours:type_name -> specs.OfficeHoursConfig - 11, // 4: specs.LabelsCompletionSpec.ItemsEntry.value:type_name -> specs.LabelsCompletionSpec.Values + 12, // 4: specs.LabelsCompletionSpec.ItemsEntry.value:type_name -> specs.LabelsCompletionSpec.Values 5, // [5:5] is the sub-list for method output_type 5, // [5:5] is the sub-list for method input_type 5, // [5:5] is the sub-list for extension type_name @@ -1123,7 +1180,7 @@ func file_omni_specs_virtual_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_omni_specs_virtual_proto_rawDesc), len(file_omni_specs_virtual_proto_rawDesc)), NumEnums: 2, - NumMessages: 11, + NumMessages: 12, NumExtensions: 0, NumServices: 0, }, diff --git a/client/api/omni/specs/virtual.proto b/client/api/omni/specs/virtual.proto index 2e99e6ce..1570aa10 100644 --- a/client/api/omni/specs/virtual.proto +++ b/client/api/omni/specs/virtual.proto @@ -107,3 +107,8 @@ message SupportSpec { bool support_enabled = 1; OfficeHoursConfig office_hours = 2; } + +message QuirksSpec { + bool supports_unified_installer = 1; + bool supports_factory_talosctl = 2; +} diff --git a/client/api/omni/specs/virtual_vtproto.pb.go b/client/api/omni/specs/virtual_vtproto.pb.go index 029436c1..6fb39f6f 100644 --- a/client/api/omni/specs/virtual_vtproto.pb.go +++ b/client/api/omni/specs/virtual_vtproto.pb.go @@ -259,6 +259,24 @@ func (m *SupportSpec) CloneMessageVT() proto.Message { return m.CloneVT() } +func (m *QuirksSpec) CloneVT() *QuirksSpec { + if m == nil { + return (*QuirksSpec)(nil) + } + r := new(QuirksSpec) + r.SupportsUnifiedInstaller = m.SupportsUnifiedInstaller + r.SupportsFactoryTalosctl = m.SupportsFactoryTalosctl + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *QuirksSpec) CloneMessageVT() proto.Message { + return m.CloneVT() +} + func (this *CurrentUserSpec) EqualVT(that *CurrentUserSpec) bool { if this == that { return true @@ -637,6 +655,28 @@ func (this *SupportSpec) EqualMessageVT(thatMsg proto.Message) bool { } return this.EqualVT(that) } +func (this *QuirksSpec) EqualVT(that *QuirksSpec) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if this.SupportsUnifiedInstaller != that.SupportsUnifiedInstaller { + return false + } + if this.SupportsFactoryTalosctl != that.SupportsFactoryTalosctl { + return false + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *QuirksSpec) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*QuirksSpec) + if !ok { + return false + } + return this.EqualVT(that) +} func (m *CurrentUserSpec) MarshalVT() (dAtA []byte, err error) { if m == nil { return nil, nil @@ -1552,6 +1592,59 @@ func (m *SupportSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *QuirksSpec) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QuirksSpec) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *QuirksSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.SupportsFactoryTalosctl { + i-- + if m.SupportsFactoryTalosctl { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x10 + } + if m.SupportsUnifiedInstaller { + i-- + if m.SupportsUnifiedInstaller { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + func (m *CurrentUserSpec) SizeVT() (n int) { if m == nil { return 0 @@ -1879,6 +1972,22 @@ func (m *SupportSpec) SizeVT() (n int) { return n } +func (m *QuirksSpec) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.SupportsUnifiedInstaller { + n += 2 + } + if m.SupportsFactoryTalosctl { + n += 2 + } + n += len(m.unknownFields) + return n +} + func (m *CurrentUserSpec) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -4108,3 +4217,94 @@ func (m *SupportSpec) UnmarshalVT(dAtA []byte) error { } return nil } +func (m *QuirksSpec) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QuirksSpec: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QuirksSpec: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field SupportsUnifiedInstaller", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.SupportsUnifiedInstaller = bool(v != 0) + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field SupportsFactoryTalosctl", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.SupportsFactoryTalosctl = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} diff --git a/client/pkg/omni/resources/virtual/quirk.go b/client/pkg/omni/resources/virtual/quirk.go new file mode 100644 index 00000000..9cf6b3b1 --- /dev/null +++ b/client/pkg/omni/resources/virtual/quirk.go @@ -0,0 +1,49 @@ +// 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 virtual + +import ( + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "github.com/cosi-project/runtime/pkg/resource/protobuf" + "github.com/cosi-project/runtime/pkg/resource/typed" + + "github.com/siderolabs/omni/client/api/omni/specs" + "github.com/siderolabs/omni/client/pkg/omni/resources" +) + +// NewQuirks creates a new Quirks resource. +func NewQuirks(id string) *Quirks { + return typed.NewResource[QuirksSpec, QuirksExtension]( + resource.NewMetadata(resources.VirtualNamespace, QuirksType, id, resource.VersionUndefined), + protobuf.NewResourceSpec(&specs.QuirksSpec{}), + ) +} + +const ( + // QuirksType is the type of Quirks resource. + // + // tsgen:QuirksType + QuirksType = resource.Type("Quirks.omni.sidero.dev") +) + +// Quirks resource describes the current Stripe subscription plan. +type Quirks = typed.Resource[QuirksSpec, QuirksExtension] + +// QuirksSpec wraps specs.QuirksSpec. +type QuirksSpec = protobuf.ResourceSpec[specs.QuirksSpec, *specs.QuirksSpec] + +// QuirksExtension provides auxiliary methods for Quirks resource. +type QuirksExtension struct{} + +// ResourceDefinition implements [typed.Extension] interface. +func (QuirksExtension) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: QuirksType, + Aliases: []resource.Type{}, + DefaultNamespace: resources.VirtualNamespace, + PrintColumns: []meta.PrintColumn{}, + } +} diff --git a/client/pkg/omni/resources/virtual/virtual.go b/client/pkg/omni/resources/virtual/virtual.go index 228e9b2d..153756d8 100644 --- a/client/pkg/omni/resources/virtual/virtual.go +++ b/client/pkg/omni/resources/virtual/virtual.go @@ -18,4 +18,5 @@ func init() { registry.MustRegisterResource(CloudPlatformConfigType, &CloudPlatformConfig{}) registry.MustRegisterResource(MetalPlatformConfigType, &MetalPlatformConfig{}) registry.MustRegisterResource(SupportType, &Support{}) + registry.MustRegisterResource(QuirksType, &Quirks{}) } diff --git a/frontend/e2e/talemu/home.spec.ts b/frontend/e2e/talemu/home.spec.ts index 12ce9a2a..8903c44e 100644 --- a/frontend/e2e/talemu/home.spec.ts +++ b/frontend/e2e/talemu/home.spec.ts @@ -132,6 +132,26 @@ test('Download omniconfig', async ({ page }, testInfo) => { }) }) +test('Download talosctl', async ({ page }, testInfo) => { + await page.goto('/') + + await page.getByRole('button', { name: 'Download talosctl' }).click() + + const [download] = await Promise.all([ + page.waitForEvent('download'), + page.getByRole('link', { name: 'Download', exact: true }).click(), + ]) + + await expect(page.getByRole('heading', { name: 'Home' })).toBeVisible() + + const filePath = testInfo.outputPath(download.suggestedFilename()) + await download.saveAs(filePath) + + const { size } = await stat(filePath) + + expect(size).toBeGreaterThan(5 * 1024 * 1024) +}) + test('Download omnictl', async ({ page }, testInfo) => { await page.goto('/') diff --git a/frontend/src/api/omni/specs/virtual.pb.ts b/frontend/src/api/omni/specs/virtual.pb.ts index 60e1523f..79aa00a6 100644 --- a/frontend/src/api/omni/specs/virtual.pb.ts +++ b/frontend/src/api/omni/specs/virtual.pb.ts @@ -106,4 +106,9 @@ export type OfficeHoursConfig = { export type SupportSpec = { support_enabled?: boolean office_hours?: OfficeHoursConfig +} + +export type QuirksSpec = { + supports_unified_installer?: boolean + supports_factory_talosctl?: boolean } \ No newline at end of file diff --git a/frontend/src/api/resources.ts b/frontend/src/api/resources.ts index 3a63e0aa..53414f46 100644 --- a/frontend/src/api/resources.ts +++ b/frontend/src/api/resources.ts @@ -287,6 +287,7 @@ export const LabelsCompletionType = "LabelsCompletions.omni.sidero.dev"; export const MetalPlatformConfigType = "MetalPlatformConfigs.omni.sidero.dev"; export const PermissionsID = "permissions"; export const PermissionsType = "Permissions.omni.sidero.dev"; +export const QuirksType = "Quirks.omni.sidero.dev"; export const SBCConfigType = "SBCConfigs.omni.sidero.dev"; export const SupportID = "support"; export const SupportType = "Supports.omni.sidero.dev"; diff --git a/frontend/src/components/Modals/Modal.vue b/frontend/src/components/Modals/Modal.vue index 22217ad4..83b0e77f 100644 --- a/frontend/src/components/Modals/Modal.vue +++ b/frontend/src/components/Modals/Modal.vue @@ -31,6 +31,7 @@ const props = withDefaults( actionLabel?: string cancelLabel?: string actionDisabled?: boolean + actionHref?: string loading?: boolean } >(), @@ -45,6 +46,7 @@ const dialogRootProps = reactiveOmit( 'actionLabel', 'cancelLabel', 'actionDisabled', + 'actionHref', 'loading', ) const forwarded = useForwardPropsEmits(dialogRootProps, emit) @@ -58,7 +60,7 @@ const forwarded = useForwardPropsEmits(dialogRootProps, emit) />
@@ -89,6 +91,7 @@ const forwarded = useForwardPropsEmits(dialogRootProps, emit) v-if="actionLabel" :disabled="actionDisabled || loading" variant="highlighted" + v-bind="actionHref ? { is: 'a', href: actionHref } : { is: 'button' }" @click="$emit('confirm')" > diff --git a/frontend/src/methods/useTalosctlDownloads.ts b/frontend/src/methods/useTalosctlDownloads.ts index 3c1aa538..273eecb3 100644 --- a/frontend/src/methods/useTalosctlDownloads.ts +++ b/frontend/src/methods/useTalosctlDownloads.ts @@ -3,52 +3,47 @@ // Use of this software is governed by the Business Source License // included in the LICENSE file. import { computedAsync } from '@vueuse/core' -import { compareLoose, gte } from 'semver' -import { computed } from 'vue' +import { type MaybeRefOrGetter, ref, toValue } from 'vue' -import { MinTalosVersion } from '@/api/resources' -import { showError } from '@/notification' +import { DefaultTalosVersion } from '@/api/resources' export interface TalosctlDownloadsResponse { status: string - release_data: { - /** - * NOTE: We don't use this response value as it is not correct. - * The backend pops the top of a sorted list, but it is sorted alphabetically, - * which does not correctly sort versions. For example, this sorting: - * - 1.1 - * - 1.11 - * - 1.2 - * - * Giving 1.2 as a more recent version than 1.11. - * - * @deprecated Don't use this as it is not the latest version. - */ - default_version: string - available_versions: Record - } + downloads?: string[] } -export function useTalosctlDownloads() { - const downloads = computedAsync(async () => { - try { - const response = await fetch('/talosctl/downloads') - - const { - release_data: { available_versions }, - }: TalosctlDownloadsResponse = await response.json() - - return new Map( - Object.entries(available_versions) - .filter(([v]) => gte(v, MinTalosVersion)) - .sort(([a], [b]) => compareLoose(a, b)), - ) - } catch (e) { - showError('Error getting latest talos releases', e?.message ?? String(e)) - } - }) - - const defaultVersion = computed(() => downloads.value && Array.from(downloads.value.keys()).pop()) - - return { downloads, defaultVersion } +interface Options { + skip?: boolean +} + +export function useTalosctlDownloads( + talosVersionMaybeRef?: MaybeRefOrGetter, + options?: MaybeRefOrGetter, +) { + const loading = ref(false) + const err = ref() + + const data = computedAsync( + async () => { + try { + if (toValue(options)?.skip) return [] + + const talosVersion = toValue(talosVersionMaybeRef) ?? DefaultTalosVersion + + const response = await fetch(`/talosctl/downloads/${talosVersion}`) + + const { downloads }: TalosctlDownloadsResponse = await response.json() + + return downloads ?? [] + } catch (e) { + err.value = e instanceof Error ? e : new Error(String(e)) + + return [] + } + }, + [], + loading, + ) + + return { data, loading, err } } diff --git a/frontend/src/pages/(authenticated)/machines/installation-media/create/confirmation.stories.ts b/frontend/src/pages/(authenticated)/machines/installation-media/create/confirmation.stories.ts index 89af75a6..321b28c6 100644 --- a/frontend/src/pages/(authenticated)/machines/installation-media/create/confirmation.stories.ts +++ b/frontend/src/pages/(authenticated)/machines/installation-media/create/confirmation.stories.ts @@ -19,6 +19,7 @@ import { type PlatformConfigSpec, PlatformConfigSpecArch, PlatformConfigSpecBootMethod, + type QuirksSpec, type SBCConfigSpec, } from '@/api/omni/specs/virtual.pb' import { @@ -29,6 +30,7 @@ import { LabelsMeta, MetalPlatformConfigType, PlatformMetalID, + QuirksType, SBCConfigType, VirtualNamespace, } from '@/api/resources' @@ -83,26 +85,40 @@ export const Default = { ], }).handler, - http.get('/talosctl/downloads', () => { - const versions = Object.fromEntries( - faker.helpers.multiple(faker.system.semver).map( - (v) => - [ - `v${v}`, - faker.helpers.multiple(faker.hacker.noun, { count: 5 }).map((name) => ({ - name, - url: `https://github.com/siderolabs/talos/releases/download/v${v}/talosctl-${name}-${faker.helpers.arrayElement(['amd64', 'arm64'])}`, - })), - ] as const, - ), - ) + http.post( + '/omni.resources.ResourceService/Get', + async ({ request }) => { + const { id, type, namespace } = await request.clone().json() + + if (type !== QuirksType || namespace !== VirtualNamespace) return + + return HttpResponse.json({ + body: JSON.stringify({ + metadata: { + namespace, + type, + id, + }, + spec: { + supports_unified_installer: true, + supports_factory_talosctl: true, + }, + } satisfies Resource), + }) + }, + ), + + http.get<{ version: string }>('/talosctl/downloads/:version', ({ params: { version } }) => { + const downloads = faker.helpers + .multiple(faker.hacker.noun, { count: 5 }) + .map( + (name) => + `https://factory.talos.dev/talosctl/v${version}/talosctl-${name}-${faker.helpers.arrayElement(['amd64', 'arm64'])}`, + ) return HttpResponse.json({ status: '', - release_data: { - default_version: '', - available_versions: versions, - }, + downloads, }) }), diff --git a/frontend/src/views/Home/components/DownloadTalosctl.stories.ts b/frontend/src/views/Home/components/DownloadTalosctl.stories.ts index 51f89422..efe0eedd 100644 --- a/frontend/src/views/Home/components/DownloadTalosctl.stories.ts +++ b/frontend/src/views/Home/components/DownloadTalosctl.stories.ts @@ -3,68 +3,97 @@ // Use of this software is governed by the Business Source License // included in the LICENSE file. import { faker } from '@faker-js/faker' +import { createWatchStreamHandler } from '@msw/helpers' import type { Meta, StoryObj } from '@storybook/vue3-vite' import { http, HttpResponse } from 'msw' import { compare } from 'semver' +import type { Resource } from '@/api/grpc' +import type { ListRequest, ListResponse } from '@/api/omni/resources/resources.pb' +import type { TalosVersionSpec } from '@/api/omni/specs/omni.pb' +import type { QuirksSpec } from '@/api/omni/specs/virtual.pb' +import { + DefaultNamespace, + DefaultTalosVersion, + QuirksType, + TalosVersionType, + VirtualNamespace, +} from '@/api/resources' +import type { TalosctlDownloadsResponse } from '@/methods/useTalosctlDownloads' + import DownloadTalosctl from './DownloadTalosctl.vue' const meta: Meta = { component: DownloadTalosctl, + args: { + open: true, + }, } export default meta type Story = StoryObj const versions = faker.helpers - .uniqueArray(faker.system.semver, 10) + .uniqueArray( + () => `1.${faker.number.int({ min: 8, max: 13 })}.${faker.number.int({ min: 0, max: 10 })}`, + 40, + ) + .concat(DefaultTalosVersion) .sort(compare) - .map((v) => `v${v}`) export const Default: Story = { parameters: { msw: { handlers: [ - http.get('/talosctl/downloads', () => - HttpResponse.json({ - release_data: { - available_versions: versions.reduce>( - (prev, curr) => ({ - ...prev, - [curr]: [ - { - name: 'Apple', - url: `https://github.com/siderolabs/talos/releases/download/${curr}/talosctl-darwin-amd64`, - }, - { - name: 'Apple Silicon', - url: `https://github.com/siderolabs/talos/releases/download/${curr}/talosctl-darwin-arm64`, - }, - { - name: 'Linux', - url: `https://github.com/siderolabs/talos/releases/download/${curr}/talosctl-linux-amd64`, - }, - { - name: 'Linux ARM', - url: `https://github.com/siderolabs/talos/releases/download/${curr}/talosctl-linux-armv7`, - }, - { - name: 'Linux ARM64', - url: `https://github.com/siderolabs/talos/releases/download/${curr}/talosctl-linux-arm64`, - }, - { - name: 'Windows', - url: `https://github.com/siderolabs/talos/releases/download/${curr}/talosctl-windows-amd64.exe`, - }, - ], - }), - {}, + http.post( + '/omni.resources.ResourceService/List', + async ({ request }) => { + const { type, namespace } = await request.clone().json() + + if (type !== QuirksType || namespace !== VirtualNamespace) return + + return HttpResponse.json({ + total: versions.length, + items: versions.map((version) => + JSON.stringify({ + metadata: { + namespace, + type, + id: version, + }, + spec: { + supports_factory_talosctl: faker.datatype.boolean(), + }, + } satisfies Resource), ), - default_version: versions.at(-1), - }, - status: 'ok', - }), + }) + }, ), + + createWatchStreamHandler({ + expectedOptions: { + type: TalosVersionType, + namespace: DefaultNamespace, + }, + initialResources: versions.map((version) => ({ + spec: { version, deprecated: faker.datatype.boolean() }, + metadata: { id: version }, + })), + }).handler, + + http.get<{ version: string }>('/talosctl/downloads/:version', ({ params: { version } }) => { + const downloads = faker.helpers + .multiple(faker.hacker.noun, { count: 5 }) + .map( + (name) => + `https://factory.talos.dev/talosctl/v${version}/talosctl-${name}-${faker.helpers.arrayElement(['amd64', 'arm64'])}`, + ) + + return HttpResponse.json({ + status: '', + downloads, + }) + }), ], }, }, diff --git a/frontend/src/views/Home/components/DownloadTalosctl.vue b/frontend/src/views/Home/components/DownloadTalosctl.vue index 32697647..f86c97d2 100644 --- a/frontend/src/views/Home/components/DownloadTalosctl.vue +++ b/frontend/src/views/Home/components/DownloadTalosctl.vue @@ -6,60 +6,124 @@ included in the LICENSE file. -->