Skip to content

Commit 6f39878

Browse files
Merge pull request #3040 from threefoldtech/development_selected_nodes_resources_overlapping
Detect and Resolve nodes' resources overlapping k8s/caprover
2 parents 3334277 + 52b0ca1 commit 6f39878

File tree

10 files changed

+275
-29
lines changed

10 files changed

+275
-29
lines changed

packages/playground/src/components/caprover_worker.vue

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
</input-tooltip>
3333

3434
<TfSelectionDetails
35+
:selected-machines="selectedMachines"
36+
:nodes-lock="nodesLock"
3537
:filters="{
3638
ipv4: true,
3739
certified: $props.modelValue.certified,
@@ -47,8 +49,10 @@
4749
</template>
4850

4951
<script lang="ts">
52+
import type AwaitLock from "await-lock";
5053
import { computed, type PropType } from "vue";
5154
55+
import type { SelectedMachine } from "@/types/nodeSelector";
5256
import { manual } from "@/utils/manual";
5357
5458
import Networks from "../components/networks.vue";
@@ -61,17 +65,50 @@ export function createWorker(name: string = generateName({ prefix: "wr" })): Cap
6165
return { name, mycelium: true };
6266
}
6367
68+
function toMachine(rootFilesystemSize: number, worker?: CaproverWorker): SelectedMachine | undefined {
69+
if (!worker || !worker.selectionDetails || !worker.selectionDetails.node) {
70+
return undefined;
71+
}
72+
73+
return {
74+
nodeId: worker.selectionDetails.node.nodeId,
75+
cpu: worker.solution?.cpu ?? 0,
76+
memory: worker.solution?.memory ?? 0,
77+
disk: (worker.solution?.disk ?? 0) + (rootFilesystemSize ?? 0),
78+
};
79+
}
80+
6481
export default {
6582
name: "CaproverWorker",
6683
components: { SelectSolutionFlavor, Networks },
67-
props: { modelValue: { type: Object as PropType<CaproverWorker>, required: true } },
84+
props: {
85+
modelValue: {
86+
type: Object as PropType<CaproverWorker>,
87+
required: true,
88+
},
89+
otherWorkers: {
90+
type: Array as PropType<CaproverWorker[]>,
91+
default: () => [],
92+
},
93+
nodesLock: Object as PropType<AwaitLock>,
94+
},
6895
setup(props) {
6996
const rootFilesystemSize = computed(() => {
7097
const { cpu = 0, memory = 0 } = props.modelValue.solution || {};
7198
return rootFs(cpu, memory);
7299
});
73100
74-
return { rootFilesystemSize, manual };
101+
const selectedMachines = computed(() => {
102+
return props.otherWorkers.reduce((res, worker) => {
103+
const machine = toMachine(rootFilesystemSize.value, worker);
104+
if (machine) {
105+
res.push(machine);
106+
}
107+
return res;
108+
}, [] as SelectedMachine[]);
109+
});
110+
111+
return { rootFilesystemSize, manual, selectedMachines };
75112
},
76113
};
77114
</script>

packages/playground/src/components/k8s_worker.vue

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@
8484
</input-tooltip>
8585

8686
<TfSelectionDetails
87+
:selected-machines="selectedMachines"
88+
:nodes-lock="nodesLock"
8789
:filters-validators="{
8890
memory: { min: 1024 },
8991
rootFilesystemSize: { min: rootFs($props.modelValue.cpu ?? 0, $props.modelValue.memory ?? 0) },
@@ -103,8 +105,10 @@
103105
</template>
104106

105107
<script lang="ts">
106-
import type { PropType } from "vue";
108+
import type AwaitLock from "await-lock";
109+
import { computed, type PropType } from "vue";
107110
111+
import type { SelectedMachine } from "@/types/nodeSelector";
108112
import { manual } from "@/utils/manual";
109113
110114
import Networks from "../components/networks.vue";
@@ -130,6 +134,19 @@ export function createWorker(name: string = generateName({ prefix: "wr" })): K8S
130134
};
131135
}
132136
137+
function toMachine(worker?: K8SWorker): SelectedMachine | undefined {
138+
if (!worker || !worker.selectionDetails || !worker.selectionDetails.node) {
139+
return undefined;
140+
}
141+
142+
return {
143+
nodeId: worker.selectionDetails.node.nodeId,
144+
cpu: worker.cpu,
145+
memory: worker.memory,
146+
disk: (worker.diskSize ?? 0) + (worker.rootFsSize ?? 0),
147+
};
148+
}
149+
133150
export default {
134151
name: "K8SWorker",
135152
components: { RootFsSize, Networks },
@@ -138,9 +155,24 @@ export default {
138155
type: Object as PropType<K8SWorker>,
139156
required: true,
140157
},
158+
otherWorkers: {
159+
type: Array as PropType<K8SWorker[]>,
160+
default: () => [],
161+
},
162+
nodesLock: Object as PropType<AwaitLock>,
141163
},
142-
setup() {
143-
return { rootFs, manual };
164+
setup(props) {
165+
const selectedMachines = computed(() => {
166+
return props.otherWorkers.reduce((res, worker) => {
167+
const machine = toMachine(worker);
168+
if (machine) {
169+
res.push(machine);
170+
}
171+
return res;
172+
}, [] as SelectedMachine[]);
173+
});
174+
175+
return { rootFs, manual, selectedMachines };
144176
},
145177
};
146178
</script>

packages/playground/src/components/node_selector/TfAutoNodeSelector.vue

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -144,22 +144,25 @@
144144
<script lang="ts">
145145
import type { FarmInfo, FilterOptions, NodeInfo } from "@threefold/grid_client";
146146
import { RequestError } from "@threefold/types";
147+
import type AwaitLock from "await-lock";
147148
import equals from "lodash/fp/equals.js";
148-
import sample from "lodash/fp/sample.js";
149149
import { computed, nextTick, onMounted, onUnmounted, type PropType, ref } from "vue";
150150
// eslint-disable-next-line @typescript-eslint/no-unused-vars
151151
import type { VCard } from "vuetify/components/VCard";
152152
153153
import { useAsync, usePagination, useWatchDeep } from "../../hooks";
154154
import { ValidatorStatus } from "../../hooks/form_validator";
155155
import { useGrid } from "../../stores";
156-
import type { SelectedLocation, SelectionDetailsFilters } from "../../types/nodeSelector";
156+
import type { SelectedLocation, SelectedMachine, SelectionDetailsFilters } from "../../types/nodeSelector";
157157
import {
158158
checkNodeCapacityPool,
159159
getNodePageCount,
160+
isNodeValid,
160161
loadValidNodes,
161162
normalizeNodeFilters,
162163
normalizeNodeOptions,
164+
release,
165+
selectValidNode,
163166
validateRentContract,
164167
} from "../../utils/nodeSelector";
165168
import TfNodeDetailsCard from "./TfNodeDetailsCard.vue";
@@ -177,39 +180,57 @@ export default {
177180
location: Object as PropType<SelectedLocation>,
178181
farm: Object as PropType<FarmInfo>,
179182
status: String as PropType<ValidatorStatus>,
183+
selectedMachines: {
184+
type: Array as PropType<SelectedMachine[]>,
185+
required: true,
186+
},
187+
nodesLock: Object as PropType<AwaitLock>,
180188
},
181189
emits: {
182190
"update:model-value": (node?: NodeInfo) => true || node,
183191
"update:status": (status: ValidatorStatus) => true || status,
184192
},
185193
setup(props, ctx) {
186194
const gridStore = useGrid();
187-
const loadedNodes = ref<NodeInfo[]>([]);
195+
const _loadedNodes = ref<NodeInfo[]>([]);
196+
const loadedNodes = computed(() => {
197+
return _loadedNodes.value.filter(
198+
node => node.nodeId === props.modelValue?.nodeId || isNodeValid(node, props.selectedMachines, filters.value),
199+
);
200+
});
188201
const nodesTask = useAsync(loadValidNodes, {
189202
shouldRun: () => props.validFilters,
190203
onBeforeTask() {
191204
const oldNode = props.modelValue;
192205
bindModelValue();
193206
return oldNode?.nodeId;
194207
},
195-
onAfterTask({ data }, oldNodeId: number) {
196-
loadedNodes.value = loadedNodes.value.concat(data as NodeInfo[]);
208+
async onAfterTask({ data }, oldNodeId: number) {
209+
_loadedNodes.value = _loadedNodes.value.concat(data as NodeInfo[]);
197210
198-
const node = loadedNodes.value.find(n => n.nodeId === oldNodeId) || sample(loadedNodes.value);
199-
node && bindModelValue(node);
200-
node && nodeInputValidateTask.value.run(node);
211+
await _setValidNode(oldNodeId);
201212
pagination.value.next();
202213
},
203214
default: [],
204215
});
205216
217+
async function _setValidNode(oldNodeId?: number) {
218+
const node = await selectValidNode(_loadedNodes.value, props.selectedMachines, filters.value, oldNodeId);
219+
if (node) {
220+
bindModelValue(node);
221+
nodeInputValidateTask.value.run(node);
222+
} else {
223+
release(props.nodesLock);
224+
}
225+
}
226+
206227
const pageCountTask = useAsync(getNodePageCount, { default: 1, shouldRun: () => props.validFilters });
207228
const pagination = usePagination();
208229
209230
const options = computed(() => normalizeNodeOptions(gridStore, props.location, pagination, props.farm));
210231
const filters = computed(() => normalizeNodeFilters(props.filters, options.value));
211232
212-
const reloadNodes = () => nodesTask.value.run(gridStore, props.filters, filters.value, pagination);
233+
const reloadNodes = () => nodesTask.value.run(gridStore, props.filters, filters.value, pagination, props.nodesLock);
213234
const loadingError = computed(() => {
214235
if (!nodesTask.value.error) return "";
215236
if (nodesTask.value.error instanceof RequestError) return "Failed to fetch nodes due to a network error";
@@ -259,7 +280,7 @@ export default {
259280
await pageCountTask.value.run(gridStore, filters.value);
260281
pagination.value.reset(pageCountTask.value.data as number);
261282
await nextTick();
262-
loadedNodes.value = [];
283+
_loadedNodes.value = [];
263284
return reloadNodes();
264285
}
265286
@@ -274,6 +295,7 @@ export default {
274295
shouldRun: () => props.validFilters,
275296
onBeforeTask: () => bindStatus(ValidatorStatus.Pending),
276297
onAfterTask({ data }) {
298+
release(props.nodesLock);
277299
bindStatus(data ? ValidatorStatus.Valid : ValidatorStatus.Invalid);
278300
const container = nodesContainer.value as HTMLDivElement;
279301
if (container) {
@@ -314,6 +336,18 @@ export default {
314336
315337
const nodesContainer = ref<HTMLDivElement>();
316338
339+
useWatchDeep(
340+
() => props.selectedMachines.map(m => m.nodeId),
341+
() => {
342+
if (props.modelValue || nodesTask.value.loading) {
343+
return;
344+
}
345+
346+
_setValidNode();
347+
},
348+
{ debounce: 1000 },
349+
);
350+
317351
return {
318352
pageCountTask,
319353
nodesTask,

packages/playground/src/components/node_selector/TfManualNodeSelector.vue

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<template>
22
<div>
33
<TfNodeDetailsCard
4+
:selected-machines="selectedMachines.filter(m => m.nodeId === nodeId)"
45
v-show="modelValue || placeholderNode"
56
:key="modelValue?.rentedByTwinId"
67
flat
@@ -52,16 +53,17 @@
5253

5354
<script lang="ts">
5455
import type { NodeInfo } from "@threefold/grid_client";
56+
import type AwaitLock from "await-lock";
5557
import isInt from "validator/lib/isInt";
5658
import { onUnmounted, type PropType, ref, watch } from "vue";
5759
5860
import { gridProxyClient } from "../../clients";
5961
import { useAsync, useWatchDeep } from "../../hooks";
6062
import { ValidatorStatus } from "../../hooks/form_validator";
6163
import { useGrid } from "../../stores";
62-
import type { SelectionDetailsFilters } from "../../types/nodeSelector";
64+
import type { SelectedMachine, SelectionDetailsFilters } from "../../types/nodeSelector";
6365
import { normalizeError } from "../../utils/helpers";
64-
import { checkNodeCapacityPool, resolveAsync, validateRentContract } from "../../utils/nodeSelector";
66+
import { checkNodeCapacityPool, release, resolveAsync, validateRentContract } from "../../utils/nodeSelector";
6567
import TfNodeDetailsCard from "./TfNodeDetailsCard.vue";
6668
6769
const _defaultError =
@@ -78,6 +80,11 @@ export default {
7880
required: true,
7981
},
8082
status: String as PropType<ValidatorStatus>,
83+
selectedMachines: {
84+
type: Array as PropType<SelectedMachine[]>,
85+
required: true,
86+
},
87+
nodesLock: Object as PropType<AwaitLock>,
8188
},
8289
emits: {
8390
"update:model-value": (node?: NodeInfo) => true || node,
@@ -159,7 +166,14 @@ export default {
159166
}
160167
161168
const { cru, mru, sru } = resources;
162-
const { cpu = 0, memory = 0, ssdDisks = [], solutionDisk = 0, rootFilesystemSize = 0 } = props.filters;
169+
const { ssdDisks = [], rootFilesystemSize = 0 } = props.filters;
170+
171+
const machinesWithSameNode = props.selectedMachines.filter(machine => machine.nodeId === nodeId);
172+
let { cpu = 0, memory = 0, solutionDisk = 0 } = props.filters;
173+
174+
cpu += machinesWithSameNode.reduce((res, machine) => res + machine.cpu, 0);
175+
memory += machinesWithSameNode.reduce((res, machine) => res + machine.memory, 0);
176+
solutionDisk += machinesWithSameNode.reduce((res, machine) => res + machine.disk, 0);
163177
164178
const memorySize = memory / 1024;
165179
const requiredMru = Math.ceil(Math.round(memorySize) * 1024 ** 3);
@@ -190,16 +204,25 @@ export default {
190204
tries: 1,
191205
onReset: bindStatus,
192206
shouldRun: () => props.validFilters,
193-
onBeforeTask: () => bindStatus(ValidatorStatus.Pending),
207+
async onBeforeTask() {
208+
await props.nodesLock?.acquireAsync();
209+
bindStatus(ValidatorStatus.Pending);
210+
},
194211
onAfterTask: ({ data }) => {
195212
bindStatus(data ? ValidatorStatus.Valid : ValidatorStatus.Invalid);
213+
release(props.nodesLock);
196214
},
197215
},
198216
);
199217
200218
// reset validation to prevent form from being valid
201219
useWatchDeep(() => props.filters, validationTask.value.reset);
202220
useWatchDeep(nodeId, validationTask.value.reset);
221+
useWatchDeep(
222+
() => props.selectedMachines.map(m => m.nodeId),
223+
() => nodeId.value && validationTask.value.run(nodeId.value),
224+
{ debounce: 1000 },
225+
);
203226
204227
// revalidate if filters updated
205228
useWatchDeep(

0 commit comments

Comments
 (0)