Skip to content

Commit

Permalink
Merge pull request #3040 from threefoldtech/development_selected_node…
Browse files Browse the repository at this point in the history
…s_resources_overlapping

Detect and Resolve nodes' resources overlapping k8s/caprover
  • Loading branch information
MohamedElmdary authored Jul 1, 2024
2 parents 3334277 + 52b0ca1 commit 6f39878
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 29 deletions.
41 changes: 39 additions & 2 deletions packages/playground/src/components/caprover_worker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
</input-tooltip>

<TfSelectionDetails
:selected-machines="selectedMachines"
:nodes-lock="nodesLock"
:filters="{
ipv4: true,
certified: $props.modelValue.certified,
Expand All @@ -47,8 +49,10 @@
</template>

<script lang="ts">
import type AwaitLock from "await-lock";
import { computed, type PropType } from "vue";
import type { SelectedMachine } from "@/types/nodeSelector";
import { manual } from "@/utils/manual";
import Networks from "../components/networks.vue";
Expand All @@ -61,17 +65,50 @@ export function createWorker(name: string = generateName({ prefix: "wr" })): Cap
return { name, mycelium: true };
}
function toMachine(rootFilesystemSize: number, worker?: CaproverWorker): SelectedMachine | undefined {
if (!worker || !worker.selectionDetails || !worker.selectionDetails.node) {
return undefined;
}
return {
nodeId: worker.selectionDetails.node.nodeId,
cpu: worker.solution?.cpu ?? 0,
memory: worker.solution?.memory ?? 0,
disk: (worker.solution?.disk ?? 0) + (rootFilesystemSize ?? 0),
};
}
export default {
name: "CaproverWorker",
components: { SelectSolutionFlavor, Networks },
props: { modelValue: { type: Object as PropType<CaproverWorker>, required: true } },
props: {
modelValue: {
type: Object as PropType<CaproverWorker>,
required: true,
},
otherWorkers: {
type: Array as PropType<CaproverWorker[]>,
default: () => [],
},
nodesLock: Object as PropType<AwaitLock>,
},
setup(props) {
const rootFilesystemSize = computed(() => {
const { cpu = 0, memory = 0 } = props.modelValue.solution || {};
return rootFs(cpu, memory);
});
return { rootFilesystemSize, manual };
const selectedMachines = computed(() => {
return props.otherWorkers.reduce((res, worker) => {
const machine = toMachine(rootFilesystemSize.value, worker);
if (machine) {
res.push(machine);
}
return res;
}, [] as SelectedMachine[]);
});
return { rootFilesystemSize, manual, selectedMachines };
},
};
</script>
38 changes: 35 additions & 3 deletions packages/playground/src/components/k8s_worker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
</input-tooltip>

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

<script lang="ts">
import type { PropType } from "vue";
import type AwaitLock from "await-lock";
import { computed, type PropType } from "vue";
import type { SelectedMachine } from "@/types/nodeSelector";
import { manual } from "@/utils/manual";
import Networks from "../components/networks.vue";
Expand All @@ -130,6 +134,19 @@ export function createWorker(name: string = generateName({ prefix: "wr" })): K8S
};
}
function toMachine(worker?: K8SWorker): SelectedMachine | undefined {
if (!worker || !worker.selectionDetails || !worker.selectionDetails.node) {
return undefined;
}
return {
nodeId: worker.selectionDetails.node.nodeId,
cpu: worker.cpu,
memory: worker.memory,
disk: (worker.diskSize ?? 0) + (worker.rootFsSize ?? 0),
};
}
export default {
name: "K8SWorker",
components: { RootFsSize, Networks },
Expand All @@ -138,9 +155,24 @@ export default {
type: Object as PropType<K8SWorker>,
required: true,
},
otherWorkers: {
type: Array as PropType<K8SWorker[]>,
default: () => [],
},
nodesLock: Object as PropType<AwaitLock>,
},
setup() {
return { rootFs, manual };
setup(props) {
const selectedMachines = computed(() => {
return props.otherWorkers.reduce((res, worker) => {
const machine = toMachine(worker);
if (machine) {
res.push(machine);
}
return res;
}, [] as SelectedMachine[]);
});
return { rootFs, manual, selectedMachines };
},
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -144,22 +144,25 @@
<script lang="ts">
import type { FarmInfo, FilterOptions, NodeInfo } from "@threefold/grid_client";
import { RequestError } from "@threefold/types";
import type AwaitLock from "await-lock";
import equals from "lodash/fp/equals.js";
import sample from "lodash/fp/sample.js";
import { computed, nextTick, onMounted, onUnmounted, type PropType, ref } from "vue";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { VCard } from "vuetify/components/VCard";
import { useAsync, usePagination, useWatchDeep } from "../../hooks";
import { ValidatorStatus } from "../../hooks/form_validator";
import { useGrid } from "../../stores";
import type { SelectedLocation, SelectionDetailsFilters } from "../../types/nodeSelector";
import type { SelectedLocation, SelectedMachine, SelectionDetailsFilters } from "../../types/nodeSelector";
import {
checkNodeCapacityPool,
getNodePageCount,
isNodeValid,
loadValidNodes,
normalizeNodeFilters,
normalizeNodeOptions,
release,
selectValidNode,
validateRentContract,
} from "../../utils/nodeSelector";
import TfNodeDetailsCard from "./TfNodeDetailsCard.vue";
Expand All @@ -177,39 +180,57 @@ export default {
location: Object as PropType<SelectedLocation>,
farm: Object as PropType<FarmInfo>,
status: String as PropType<ValidatorStatus>,
selectedMachines: {
type: Array as PropType<SelectedMachine[]>,
required: true,
},
nodesLock: Object as PropType<AwaitLock>,
},
emits: {
"update:model-value": (node?: NodeInfo) => true || node,
"update:status": (status: ValidatorStatus) => true || status,
},
setup(props, ctx) {
const gridStore = useGrid();
const loadedNodes = ref<NodeInfo[]>([]);
const _loadedNodes = ref<NodeInfo[]>([]);
const loadedNodes = computed(() => {
return _loadedNodes.value.filter(
node => node.nodeId === props.modelValue?.nodeId || isNodeValid(node, props.selectedMachines, filters.value),
);
});
const nodesTask = useAsync(loadValidNodes, {
shouldRun: () => props.validFilters,
onBeforeTask() {
const oldNode = props.modelValue;
bindModelValue();
return oldNode?.nodeId;
},
onAfterTask({ data }, oldNodeId: number) {
loadedNodes.value = loadedNodes.value.concat(data as NodeInfo[]);
async onAfterTask({ data }, oldNodeId: number) {
_loadedNodes.value = _loadedNodes.value.concat(data as NodeInfo[]);
const node = loadedNodes.value.find(n => n.nodeId === oldNodeId) || sample(loadedNodes.value);
node && bindModelValue(node);
node && nodeInputValidateTask.value.run(node);
await _setValidNode(oldNodeId);
pagination.value.next();
},
default: [],
});
async function _setValidNode(oldNodeId?: number) {
const node = await selectValidNode(_loadedNodes.value, props.selectedMachines, filters.value, oldNodeId);
if (node) {
bindModelValue(node);
nodeInputValidateTask.value.run(node);
} else {
release(props.nodesLock);
}
}
const pageCountTask = useAsync(getNodePageCount, { default: 1, shouldRun: () => props.validFilters });
const pagination = usePagination();
const options = computed(() => normalizeNodeOptions(gridStore, props.location, pagination, props.farm));
const filters = computed(() => normalizeNodeFilters(props.filters, options.value));
const reloadNodes = () => nodesTask.value.run(gridStore, props.filters, filters.value, pagination);
const reloadNodes = () => nodesTask.value.run(gridStore, props.filters, filters.value, pagination, props.nodesLock);
const loadingError = computed(() => {
if (!nodesTask.value.error) return "";
if (nodesTask.value.error instanceof RequestError) return "Failed to fetch nodes due to a network error";
Expand Down Expand Up @@ -259,7 +280,7 @@ export default {
await pageCountTask.value.run(gridStore, filters.value);
pagination.value.reset(pageCountTask.value.data as number);
await nextTick();
loadedNodes.value = [];
_loadedNodes.value = [];
return reloadNodes();
}
Expand All @@ -274,6 +295,7 @@ export default {
shouldRun: () => props.validFilters,
onBeforeTask: () => bindStatus(ValidatorStatus.Pending),
onAfterTask({ data }) {
release(props.nodesLock);
bindStatus(data ? ValidatorStatus.Valid : ValidatorStatus.Invalid);
const container = nodesContainer.value as HTMLDivElement;
if (container) {
Expand Down Expand Up @@ -314,6 +336,18 @@ export default {
const nodesContainer = ref<HTMLDivElement>();
useWatchDeep(
() => props.selectedMachines.map(m => m.nodeId),
() => {
if (props.modelValue || nodesTask.value.loading) {
return;
}
_setValidNode();
},
{ debounce: 1000 },
);
return {
pageCountTask,
nodesTask,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<div>
<TfNodeDetailsCard
:selected-machines="selectedMachines.filter(m => m.nodeId === nodeId)"
v-show="modelValue || placeholderNode"
:key="modelValue?.rentedByTwinId"
flat
Expand Down Expand Up @@ -52,16 +53,17 @@

<script lang="ts">
import type { NodeInfo } from "@threefold/grid_client";
import type AwaitLock from "await-lock";
import isInt from "validator/lib/isInt";
import { onUnmounted, type PropType, ref, watch } from "vue";
import { gridProxyClient } from "../../clients";
import { useAsync, useWatchDeep } from "../../hooks";
import { ValidatorStatus } from "../../hooks/form_validator";
import { useGrid } from "../../stores";
import type { SelectionDetailsFilters } from "../../types/nodeSelector";
import type { SelectedMachine, SelectionDetailsFilters } from "../../types/nodeSelector";
import { normalizeError } from "../../utils/helpers";
import { checkNodeCapacityPool, resolveAsync, validateRentContract } from "../../utils/nodeSelector";
import { checkNodeCapacityPool, release, resolveAsync, validateRentContract } from "../../utils/nodeSelector";
import TfNodeDetailsCard from "./TfNodeDetailsCard.vue";
const _defaultError =
Expand All @@ -78,6 +80,11 @@ export default {
required: true,
},
status: String as PropType<ValidatorStatus>,
selectedMachines: {
type: Array as PropType<SelectedMachine[]>,
required: true,
},
nodesLock: Object as PropType<AwaitLock>,
},
emits: {
"update:model-value": (node?: NodeInfo) => true || node,
Expand Down Expand Up @@ -159,7 +166,14 @@ export default {
}
const { cru, mru, sru } = resources;
const { cpu = 0, memory = 0, ssdDisks = [], solutionDisk = 0, rootFilesystemSize = 0 } = props.filters;
const { ssdDisks = [], rootFilesystemSize = 0 } = props.filters;
const machinesWithSameNode = props.selectedMachines.filter(machine => machine.nodeId === nodeId);
let { cpu = 0, memory = 0, solutionDisk = 0 } = props.filters;
cpu += machinesWithSameNode.reduce((res, machine) => res + machine.cpu, 0);
memory += machinesWithSameNode.reduce((res, machine) => res + machine.memory, 0);
solutionDisk += machinesWithSameNode.reduce((res, machine) => res + machine.disk, 0);
const memorySize = memory / 1024;
const requiredMru = Math.ceil(Math.round(memorySize) * 1024 ** 3);
Expand Down Expand Up @@ -190,16 +204,25 @@ export default {
tries: 1,
onReset: bindStatus,
shouldRun: () => props.validFilters,
onBeforeTask: () => bindStatus(ValidatorStatus.Pending),
async onBeforeTask() {
await props.nodesLock?.acquireAsync();
bindStatus(ValidatorStatus.Pending);
},
onAfterTask: ({ data }) => {
bindStatus(data ? ValidatorStatus.Valid : ValidatorStatus.Invalid);
release(props.nodesLock);
},
},
);
// reset validation to prevent form from being valid
useWatchDeep(() => props.filters, validationTask.value.reset);
useWatchDeep(nodeId, validationTask.value.reset);
useWatchDeep(
() => props.selectedMachines.map(m => m.nodeId),
() => nodeId.value && validationTask.value.run(nodeId.value),
{ debounce: 1000 },
);
// revalidate if filters updated
useWatchDeep(
Expand Down
Loading

0 comments on commit 6f39878

Please sign in to comment.