Skip to content

Commit 592f916

Browse files
committed
feat: don't allow downgrades of the machines when adding to a cluster
Make `MachineClass` selector skip them. Disable them in the UI. Do not allow adding them as the resources, validate on the backend level. Fixes: #46 Signed-off-by: Artem Chernyshev <[email protected]>
1 parent 2e015a9 commit 592f916

File tree

7 files changed

+179
-48
lines changed

7 files changed

+179
-48
lines changed

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@
6666
"postcss": "^8.4.14",
6767
"tailwindcss": "^3.1.4",
6868
"ts-jest": "^27.1.5",
69-
"ts-loader": "^9.3.1",
7069
"typescript": "^4.7.4",
70+
"ts-loader": "^9.3.1",
7171
"vue-cli-plugin-tailwind": "^3.0.0"
7272
}
7373
}

frontend/src/views/cluster/Config/Patches.vue

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,7 @@ type Props = {
109109
currentCluster?: ResourceTyped<ClusterSpec>,
110110
};
111111
112-
const props = defineProps<Props>();
113-
114-
console.log("PATCHES currentCluster", props.currentCluster);
115-
112+
defineProps<Props>();
116113
117114
const updateSelectors = () => {
118115
patchListSelectors.value = [`!${LabelSystemPatch}`];

frontend/src/views/omni/Clusters/Management/ClusterCreate.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ included in the LICENSE file.
9898
<template #default="{ items, searchQuery }">
9999
<cluster-machine-item
100100
v-for="item in items"
101+
:talos-version-not-allowed="!canAddMachine(item)"
101102
:key="itemID(item)"
102103
:reset="reset"
103104
:item="item"
@@ -140,7 +141,7 @@ import {
140141
PatchBaseWeightMachineSet,
141142
PatchBaseWeightCluster,
142143
} from "@/api/resources";
143-
import { TalosVersionSpec } from "@/api/omni/specs/omni.pb";
144+
import { MachineStatusSpec, TalosVersionSpec } from "@/api/omni/specs/omni.pb";
144145
import WatchResource, { itemID } from "@/api/watch";
145146
import { showError, showSuccess } from "@/notification";
146147
import { Resource, ResourceTyped } from "@/api/grpc";
@@ -261,6 +262,13 @@ const createCluster = async () => {
261262
}
262263
};
263264
265+
const canAddMachine = (machine: Resource<MachineStatusSpec>) => {
266+
const clusterVersion = semver.parse(state.value.cluster.talosVersion);
267+
const machineVersion = semver.parse(machine.spec.talos_version);
268+
269+
return machineVersion.major <= clusterVersion.major && machineVersion.minor <= clusterVersion.minor;
270+
}
271+
264272
const createCluster_ = async (untaint: boolean) => {
265273
if (typeof state.value.controlPlanesCount === 'number' && (state.value.controlPlanesCount - 1) % 2 !== 0) {
266274
showError(

frontend/src/views/omni/Clusters/Management/ClusterMachineItem.vue

Lines changed: 70 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,44 @@ included in the LICENSE file.
77
<template>
88
<t-list-item>
99
<template #default>
10-
<div class="flex items-center text-naturals-N13">
11-
<div class="truncate flex-1 flex items-center gap-2">
12-
<span class="font-bold pr-2">
13-
<word-highlighter
14-
:query="(searchQuery ?? '')"
15-
:textToHighlight="item?.spec?.network?.hostname ?? item?.metadata?.id"
16-
split-by-space
17-
highlightClass="bg-naturals-N14"/>
18-
</span>
19-
<machine-item-labels :resource="item" :add-label-func="addMachineLabels" :remove-label-func="removeMachineLabels" @filter-label="e => $emit('filterLabel', e)"/>
20-
</div>
21-
<div class="flex justify-end flex-initial w-128 gap-4 items-center">
22-
<template v-if="machineSetIndex !== undefined">
23-
<div v-if="systemDiskPath" class="pr-8 pl-3 py-1.5 text-naturals-N11 rounded border border-naturals-N6 cursor-not-allowed">
24-
Install Disk: {{ systemDiskPath }}
25-
</div>
26-
<div v-else>
27-
<t-select-list
28-
class="h-7"
29-
title="Install Disk"
30-
@checkedValue="setInstallDisk"
31-
:values="disks"
32-
:defaultValue="disks[0]"/>
33-
</div>
34-
</template>
35-
<div>
36-
<machine-set-picker :options="options" :machine-set-index="machineSetIndex" @update:machineSetIndex="value => machineSetIndex = value"/>
10+
<div class="flex items-center text-naturals-N13">
11+
<div class="truncate flex-1 flex items-center gap-2">
12+
<span class="font-bold pr-2">
13+
<word-highlighter
14+
:query="(searchQuery ?? '')"
15+
:textToHighlight="item?.spec?.network?.hostname ?? item?.metadata?.id"
16+
split-by-space
17+
highlightClass="bg-naturals-N14"/>
18+
</span>
19+
<machine-item-labels :resource="item" :add-label-func="addMachineLabels" :remove-label-func="removeMachineLabels" @filter-label="e => $emit('filterLabel', e)"/>
20+
</div>
21+
<div class="flex justify-end flex-initial w-128 gap-4 items-center">
22+
<template v-if="machineSetIndex !== undefined">
23+
<div v-if="systemDiskPath" class="pr-8 pl-3 py-1.5 text-naturals-N11 rounded border border-naturals-N6 cursor-not-allowed">
24+
Install Disk: {{ systemDiskPath }}
3725
</div>
38-
<div class="flex items-center">
39-
<icon-button
40-
class="text-naturals-N14 my-auto"
41-
@click="openPatchConfig"
42-
:id="machineSetIndex !== undefined ? options?.[machineSetIndex]?.id : undefined"
43-
:disabled="machineSetIndex === undefined || options?.[machineSetIndex]?.disabled"
44-
:icon="machineSetNode.patches[machinePatchID] && machineSetIndex ? 'settings-toggle': 'settings'"/>
26+
<div v-else>
27+
<t-select-list
28+
class="h-7"
29+
title="Install Disk"
30+
@checkedValue="setInstallDisk"
31+
:values="disks"
32+
:defaultValue="disks[0]"/>
4533
</div>
34+
</template>
35+
<div>
36+
<machine-set-picker :options="options" :machine-set-index="machineSetIndex" @update:machineSetIndex="value => machineSetIndex = value"/>
37+
</div>
38+
<div class="flex items-center">
39+
<icon-button
40+
class="text-naturals-N14 my-auto"
41+
@click="openPatchConfig"
42+
:id="machineSetIndex !== undefined ? options?.[machineSetIndex]?.id : undefined"
43+
:disabled="machineSetIndex === undefined || options?.[machineSetIndex]?.disabled"
44+
:icon="machineSetNode.patches[machinePatchID] && machineSetIndex ? 'settings-toggle': 'settings'"/>
4645
</div>
4746
</div>
47+
</div>
4848
</template>
4949
<template #details>
5050
<div class="pl-6 grid grid-cols-5">
@@ -121,10 +121,11 @@ defineEmits(['filterLabel']);
121121
const props = defineProps<{
122122
item: ResourceTyped<MachineStatusSpec & SiderolinkSpec & MachineConfigGenOptionsSpec>,
123123
reset?: number,
124-
searchQuery?: string
124+
searchQuery?: string,
125+
talosVersionNotAllowed: boolean
125126
}>();
126127
127-
const { item, reset } = toRefs(props);
128+
const { item, reset, talosVersionNotAllowed } = toRefs(props);
128129
129130
const machineSetNode = ref<MachineSetNode>({
130131
patches: {},
@@ -133,7 +134,7 @@ const machineSetIndex = ref<number | undefined>();
133134
const systemDiskPath: Ref<string | undefined> = ref();
134135
const disks: Ref<string[]> = ref([]);
135136
136-
const computeDisks = () => {
137+
const computeState = () => {
137138
const bds: MachineStatusSpecHardwareStatusBlockDevice[] = item?.value?.spec?.hardware?.blockdevices || [];
138139
const diskPaths: string[] = [];
139140
@@ -147,11 +148,36 @@ const computeDisks = () => {
147148
}
148149
149150
disks.value = diskPaths;
151+
152+
computeMachineAssignment()
150153
}
151154
152-
computeDisks();
155+
const computeMachineAssignment = () => {
156+
for (var i = 0; i < state.value.machineSets.length; i++) {
157+
const machineSet = state.value.machineSets[i];
153158
154-
watch(item, computeDisks);
159+
for(const id in machineSet.machines) {
160+
if (item.value.metadata.id === id) {
161+
machineSetIndex.value = i;
162+
163+
return;
164+
}
165+
}
166+
}
167+
168+
machineSetIndex.value = undefined;
169+
}
170+
171+
computeState();
172+
173+
watch(item, computeState);
174+
watch(state.value, computeMachineAssignment);
175+
176+
watch(talosVersionNotAllowed, (value: boolean) => {
177+
if (value) {
178+
machineSetIndex.value = undefined;
179+
}
180+
});
155181
156182
watch(machineSetIndex, (val?: number, old?: number) => {
157183
if (val !== undefined) {
@@ -214,6 +240,11 @@ const options: Ref<PickerOption[]> = computed(() => {
214240
tooltip = `The machine class ${ms.id} is using machine class so no manual allocation is possible`
215241
}
216242
243+
if (talosVersionNotAllowed.value) {
244+
disabled = true;
245+
tooltip = `The machine has newer Talos version installed: downgrade is not allowed. Upgrade the machine or change Talos cluster version`
246+
}
247+
217248
return {
218249
id: ms.id,
219250
disabled: disabled,

internal/backend/runtime/omni/controllers/omni/machine_set_node.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import (
1010
"fmt"
1111
"math"
1212
"slices"
13+
"strings"
1314

15+
"github.com/blang/semver/v4"
1416
"github.com/cosi-project/runtime/pkg/controller"
1517
"github.com/cosi-project/runtime/pkg/resource"
1618
"github.com/cosi-project/runtime/pkg/safe"
@@ -42,6 +44,11 @@ func (ctrl *MachineSetNodeController) Inputs() []controller.Input {
4244
Type: omni.MachineSetType,
4345
Kind: controller.InputWeak,
4446
},
47+
{
48+
Namespace: resources.DefaultNamespace,
49+
Type: omni.ClusterType,
50+
Kind: controller.InputWeak,
51+
},
4552
{
4653
Namespace: resources.DefaultNamespace,
4754
Type: omni.MachineType,
@@ -222,6 +229,21 @@ func (ctrl *MachineSetNodeController) createNodes(
222229

223230
created := 0
224231

232+
clusterName, ok := machineSet.Metadata().Labels().Get(omni.LabelCluster)
233+
if !ok {
234+
return fmt.Errorf("failed to get cluster name of the machine set %q", machineSet.Metadata().ID())
235+
}
236+
237+
cluster, err := safe.ReaderGetByID[*omni.Cluster](ctx, r, clusterName)
238+
if err != nil {
239+
return err
240+
}
241+
242+
clusterVersion, err := semver.Parse(cluster.TypedSpec().Value.TalosVersion)
243+
if err != nil {
244+
return fmt.Errorf("failed to parse talos version of the cluster %w", err)
245+
}
246+
225247
for _, selector := range selectors {
226248
selector.Terms = append(selector.Terms, resource.LabelTerm{
227249
Key: omni.MachineStatusLabelAvailable,
@@ -231,7 +253,22 @@ func (ctrl *MachineSetNodeController) createNodes(
231253
availableMachineClassMachines := allMachineStatuses.FilterLabelQuery(resource.RawLabelQuery(selector))
232254

233255
for i := 0; i < availableMachineClassMachines.Len(); i++ {
234-
id := availableMachineClassMachines.Get(i).Metadata().ID()
256+
machine := availableMachineClassMachines.Get(i)
257+
258+
var machineVersion semver.Version
259+
260+
machineVersion, err = semver.Parse(strings.TrimPrefix(machine.TypedSpec().Value.TalosVersion, "v"))
261+
if err != nil {
262+
continue
263+
}
264+
265+
// do not try to allocate the machine if it's Talos major or minor version is greater than cluster Talos version
266+
// this way we don't allow downgrading the machines while allocating them
267+
if machineVersion.Major > clusterVersion.Major || machineVersion.Minor > clusterVersion.Minor {
268+
continue
269+
}
270+
271+
id := machine.Metadata().ID()
235272

236273
if err := r.Create(ctx, omni.NewMachineSetNode(resources.DefaultNamespace, id, machineSet)); err != nil {
237274
if state.IsConflictError(err) {

internal/backend/runtime/omni/controllers/omni/machine_set_node_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ func (suite *MachineSetNodeSuite) createMachines(labels ...map[string]string) []
5555
}
5656
})
5757

58+
machineStatus.TypedSpec().Value.TalosVersion = "v1.6.0"
59+
5860
res = append(res, machineStatus)
5961

6062
suite.Require().NoError(suite.state.Create(suite.ctx, machineStatus))
@@ -109,7 +111,12 @@ func (suite *MachineSetNodeSuite) TestReconcile() {
109111
)
110112
}
111113

112-
machineSet.Metadata().Labels().Set(omni.LabelCluster, "cluster1")
114+
cluster := omni.NewCluster(resources.DefaultNamespace, "cluster1")
115+
cluster.TypedSpec().Value.TalosVersion = "1.6.0"
116+
117+
suite.Require().NoError(suite.state.Create(suite.ctx, cluster))
118+
119+
machineSet.Metadata().Labels().Set(omni.LabelCluster, cluster.Metadata().ID())
113120
machineSet.Metadata().Labels().Set(omni.LabelWorkerRole, "")
114121

115122
machineClass := newMachineClass(fmt.Sprintf("%s==amd64", omni.MachineStatusLabelArch))

internal/backend/runtime/omni/state_validation.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ func validateBootstrapSpec(ctx context.Context, st state.State, etcdBackupStoreF
401401

402402
// machineSetNodeValidationOptions returns the validation options for the machine set node resource.
403403
//
404-
//nolint:gocognit
404+
//nolint:gocognit,gocyclo,cyclop
405405
func machineSetNodeValidationOptions(st state.State) []validated.StateOption {
406406
getMachineSet := func(ctx context.Context, res *omni.MachineSetNode) (*omni.MachineSet, error) {
407407
machineSetName, ok := res.Metadata().Labels().Get(omni.LabelMachineSet)
@@ -421,6 +421,53 @@ func machineSetNodeValidationOptions(st state.State) []validated.StateOption {
421421
return machineSet, nil
422422
}
423423

424+
validateTalosVersion := func(ctx context.Context, res *omni.MachineSetNode) error {
425+
clusterName, ok := res.Metadata().Labels().Get(omni.LabelCluster)
426+
if !ok {
427+
return nil
428+
}
429+
430+
cluster, err := safe.ReaderGetByID[*omni.Cluster](ctx, st, clusterName)
431+
if err != nil {
432+
if state.IsNotFoundError(err) {
433+
return nil
434+
}
435+
436+
return err
437+
}
438+
439+
machineStatus, err := safe.ReaderGetByID[*omni.MachineStatus](ctx, st, res.Metadata().ID())
440+
if err != nil {
441+
if state.IsNotFoundError(err) {
442+
return nil
443+
}
444+
445+
return err
446+
}
447+
448+
machineTalosVersion, err := semver.Parse(strings.TrimLeft(machineStatus.TypedSpec().Value.TalosVersion, "v"))
449+
if err != nil {
450+
// ignore version check if it's not possible to parse machine Talos version
451+
return nil //nolint:nilerr
452+
}
453+
454+
clusterTalosVersion, err := semver.Parse(cluster.TypedSpec().Value.TalosVersion)
455+
if err != nil {
456+
return err
457+
}
458+
459+
if machineTalosVersion.Major > clusterTalosVersion.Major || machineTalosVersion.Minor > clusterTalosVersion.Minor {
460+
return fmt.Errorf(
461+
"cannot add machine set node to the cluster %s as it will trigger Talos downgrade on the node (%s -> %s)",
462+
clusterName,
463+
machineTalosVersion.String(),
464+
clusterTalosVersion.String(),
465+
)
466+
}
467+
468+
return nil
469+
}
470+
424471
return []validated.StateOption{
425472
validated.WithCreateValidations(validated.NewCreateValidationForType(func(ctx context.Context, res *omni.MachineSetNode, _ ...state.CreateOption) error {
426473
machineSet, err := getMachineSet(ctx, res)
@@ -436,6 +483,10 @@ func machineSetNodeValidationOptions(st state.State) []validated.StateOption {
436483
return fmt.Errorf("adding machine set node to the machine set %q is not allowed: the machine set is using machine classes", machineSet.Metadata().ID())
437484
}
438485

486+
if err = validateTalosVersion(ctx, res); err != nil {
487+
return err
488+
}
489+
439490
return validateNotControlplane(machineSet, res)
440491
})),
441492
validated.WithUpdateValidations(validated.NewUpdateValidationForType(func(ctx context.Context, res *omni.MachineSetNode, newRes *omni.MachineSetNode, _ ...state.UpdateOption) error {

0 commit comments

Comments
 (0)