feat: implement the machine categories UI
Some checks are pending
default / default (push) Waiting to run
default / e2e-backups (push) Blocked by required conditions
default / e2e-forced-removal (push) Blocked by required conditions
default / e2e-scaling (push) Blocked by required conditions
default / e2e-short (push) Blocked by required conditions
default / e2e-short-secureboot (push) Blocked by required conditions
default / e2e-templates (push) Blocked by required conditions
default / e2e-upgrades (push) Blocked by required conditions
default / e2e-workload-proxy (push) Blocked by required conditions

It is now possible to filter the machine by the following categories in
the UI:
- Manually joined machines.
- Machines provisioned by the infra providers.
- Machines PXE booted through the metal provider.
- Machines discovered by the metal provider, but not yet accepted by the
  system.

Machine acceptance UI also has two new modal windows for accepting and
rejecting the machines.
Also update controllers to add labels to the not accepted infra
machines.

Fixes: https://github.com/siderolabs/omni/issues/773
Fixes: https://github.com/siderolabs/omni/issues/766

Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
This commit is contained in:
Artem Chernyshev 2024-12-13 19:13:00 +03:00
parent d75ae45d13
commit e8aee8ed86
No known key found for this signature in database
GPG Key ID: E084A2DF1143C14D
18 changed files with 522 additions and 35 deletions

View File

@ -55,6 +55,7 @@ const (
LabelExposedServiceAlias = SystemLabelPrefix + "exposed-service-alias"
// LabelInfraProviderID is the infra provider ID for the resources managed by infra providers, e.g., infra.MachineRequest, infra.MachineRequestStatus.
// tsgen:LabelInfraProviderID
LabelInfraProviderID = SystemLabelPrefix + "infra-provider-id"
// LabelIsStaticInfraProvider is set on the infra.ProviderStatus resources to mark them as static providers - they do not work with MachineRequests to
@ -82,7 +83,12 @@ const (
LabelNoManualAllocation = SystemLabelPrefix + "no-manual-allocation"
// LabelIsManagedByStaticInfraProvider is set on the machines managed by static infra providers.
// tsgen:LabelIsManagedByStaticInfraProvider
LabelIsManagedByStaticInfraProvider = SystemLabelPrefix + "is-managed-by-static-infra-provider"
// LabelMachinePendingAccept is added to the InfraMachine and is used to filter out the machines which are pending acceptance.
// tsgen:LabelMachinePendingAccept
LabelMachinePendingAccept = SystemLabelPrefix + "accept-pending"
)
const (

View File

@ -125,9 +125,12 @@ export const LabelClusterMachine = "omni.sidero.dev/cluster-machine";
export const LabelMachine = "omni.sidero.dev/machine";
export const LabelSystemPatch = "omni.sidero.dev/system-patch";
export const LabelExposedServiceAlias = "omni.sidero.dev/exposed-service-alias";
export const LabelInfraProviderID = "omni.sidero.dev/infra-provider-id";
export const LabelMachineRequest = "omni.sidero.dev/machine-request";
export const LabelMachineRequestSet = "omni.sidero.dev/machine-request-set";
export const LabelNoManualAllocation = "omni.sidero.dev/no-manual-allocation";
export const LabelIsManagedByStaticInfraProvider = "omni.sidero.dev/is-managed-by-static-infra-provider";
export const LabelMachinePendingAccept = "omni.sidero.dev/accept-pending";
export const MachineStatusLabelConnected = "omni.sidero.dev/connected";
export const MachineStatusLabelDisconnected = "omni.sidero.dev/disconnected";
export const MachineStatusLabelInvalidState = "omni.sidero.dev/invalid-state";

View File

@ -15,7 +15,8 @@ included in the LICENSE file.
:icon="item.icon"
:label="item.label"
:label-color="item.labelColor"
/>
>
</t-menu-item>
</div>
</nav>
</template>
@ -30,7 +31,7 @@ export type SideBarItem = {
route: string | RouteLocationRaw,
icon?: IconType,
label?: string | number,
labelColor?: string
labelColor?: string,
}
type Props = {

View File

@ -31,6 +31,7 @@ import {
ServerIcon,
ServerStackIcon,
CalendarIcon,
CpuChipIcon,
} from "@heroicons/vue/24/outline";
import {
@ -114,6 +115,7 @@ const icons = {
"rollback": defineAsyncComponent(() => import("../../icons/IconRollback.vue")),
"extensions": defineAsyncComponent(() => import("../../icons/IconExtensions.vue")),
"extensions-toggle": defineAsyncComponent(() => import("../../icons/IconExtensionsToggle.vue")),
"server-network": defineAsyncComponent(() => import("../../icons/IconServerNetwork.vue")),
"document": DocumentIcon,
"power": PowerIcon,
"users": UsersIcon,
@ -135,6 +137,7 @@ const icons = {
"server-stack": ServerStackIcon,
"lifebuoy": LifebuoyIcon,
"calendar": CalendarIcon,
"cpu-chip": CpuChipIcon,
};
const getComponent = (icon: string): Component | undefined => {

View File

@ -6,11 +6,22 @@ included in the LICENSE file.
-->
<template>
<component :is="componentType" v-bind="componentAttributes">
<div class="item">
<t-icon v-if="icon" class="item__icon" :icon="icon" :svg-base64="iconSvgBase64"/>
<p class="item__name truncate">{{ name }}</p>
<div v-if="label" class="rounded-full text-naturals-N13 bg-naturals-N4 text-xs p-1 w-6 h-6 text-center" :class="labelColor ? 'text-' + labelColor : ''">
{{ label }}
<div :class="{'border-naturals-N5': expanded, 'border-transparent': !expanded}" class="border-b border-t divide-naturals-N5 flex flex-col">
<div class="item" :class="{'sub-item': subItem}">
<t-icon v-if="icon" class="item-icon" :icon="icon" :svg-base64="iconSvgBase64"/>
<p class="item-name truncate">{{ name }}</p>
<div v-if="label" class="rounded-full text-naturals-N13 bg-naturals-N4 text-xs p-1 w-6 h-6 text-center" :class="labelColor ? 'text-' + labelColor : ''">
{{ label }}
</div>
<t-icon v-if="hasSubItems"
icon="arrow-up"
class="w-4 h-4 hover:text-naturals-N13 transition-color transition-transform duration-250"
:class="{'rotate-180': expanded}"
@click.stop.prevent="() => expanded = !expanded"
/>
</div>
<div @click.stop.prevent v-if="expanded">
<slot/>
</div>
</div>
</component>
@ -18,6 +29,7 @@ included in the LICENSE file.
<script setup lang="ts">
import TIcon, { IconType } from "@/components/common/Icon/TIcon.vue";
import { ref } from "vue";
type Props = {
route: string | object,
@ -26,42 +38,49 @@ type Props = {
icon?: IconType,
iconSvgBase64?: string,
label?: string | number,
labelColor?: string
labelColor?: string,
hasSubItems?: boolean
subItem?: boolean
};
const props = defineProps<Props>();
const expanded = ref(false);
const componentType = props.regularLink ? "a" : "router-link";
const componentAttributes = props.regularLink ?
{ href: props.route, target: "_blank" } :
{ to: props.route, activeClass: "item__active" };
{ to: props.route, activeClass: "item-active" };
</script>
<style scoped>
.item {
@apply flex gap-4 border-l-2 border-transparent justify-start items-center py-1.5 my-1 px-6 transition-all duration-200 hover:bg-naturals-N4;
@apply flex gap-4 border-l-2 border-transparent justify-start items-center py-1.5 my-0.5 px-6 transition-all duration-200 hover:bg-naturals-N4;
}
.item:hover .item__icon {
.item:hover .item-icon {
@apply text-naturals-N13;
}
.item:hover .item__name {
.item:hover .item-name {
@apply text-naturals-N13;
}
.item__active .item {
.item-active .item {
@apply border-primary-P3;
}
.item__active .item__icon {
.item-active .item-icon {
@apply text-naturals-N13;
}
.item__active .item__name {
.item-active .item-name {
@apply text-naturals-N13;
}
.item__icon {
.item-icon {
@apply text-naturals-N11 transition-all duration-200;
width: 16px;
height: 16px;
}
.item__name {
.item-name {
@apply text-xs text-naturals-N11 transition-all duration-200 flex-1;
}
.item.sub-item {
@apply pl-12;
}
</style>

View File

@ -0,0 +1,34 @@
<!--
Copyright (c) 2024 Sidero Labs, Inc.
Use of this software is governed by the Business Source License
included in the LICENSE file.
-->
<template>
<svg viewBox="0.403 0.767 23.49 18.6225" width="21.49" height="16.6225" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M 5.833 4.267 C 4.776 4.267 3.903 5.133 3.903 6.197 L 3.903 8.827 C 3.903 9.885 4.769 10.757 5.833 10.757 L 20.463 10.757 C 21.519 10.757 22.393 9.883 22.393 8.827 L 22.393 6.197 C 22.393 5.139 21.527 4.267 20.463 4.267 L 5.833 4.267 Z M 2.403 6.197 C 2.403 4.301 3.951 2.767 5.833 2.767 L 20.463 2.767 C 22.359 2.767 23.893 4.315 23.893 6.197 L 23.893 8.827 C 23.893 10.711 22.347 12.257 20.463 12.257 L 5.833 12.257 C 3.937 12.257 2.403 10.709 2.403 8.827 L 2.403 6.197 Z"
fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M 7.143 5.757 C 7.558 5.757 7.893 6.093 7.893 6.507 L 7.893 8.507 C 7.893 8.921 7.558 9.257 7.143 9.257 C 6.729 9.257 6.393 8.921 6.393 8.507 L 6.393 6.507 C 6.393 6.093 6.729 5.757 7.143 5.757 Z"
fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M 11.143 5.757 C 11.558 5.757 11.893 6.093 11.893 6.507 L 11.893 8.507 C 11.893 8.921 11.558 9.257 11.143 9.257 C 10.729 9.257 10.393 8.921 10.393 8.507 L 10.393 6.507 C 10.393 6.093 10.729 5.757 11.143 5.757 Z"
fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M 14.393 7.507 C 14.393 7.093 14.729 6.757 15.143 6.757 L 19.143 6.757 C 19.557 6.757 19.893 7.093 19.893 7.507 C 19.893 7.921 19.557 8.257 19.143 8.257 L 15.143 8.257 C 14.729 8.257 14.393 7.921 14.393 7.507 Z"
fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M 13.148 10.89 C 13.562 10.89 13.898 11.225 13.898 11.64 L 13.898 14.64 C 13.898 15.054 13.562 15.39 13.148 15.39 C 12.734 15.39 12.398 15.054 12.398 14.64 L 12.398 11.64 C 12.398 11.225 12.734 10.89 13.148 10.89 Z"
fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M 13.148 15.39 C 12.458 15.39 11.898 15.949 11.898 16.64 C 11.898 17.33 12.458 17.89 13.148 17.89 C 13.839 17.89 14.398 17.33 14.398 16.64 C 14.398 15.949 13.839 15.39 13.148 15.39 Z M 10.398 16.64 C 10.398 15.121 11.629 13.89 13.148 13.89 C 14.667 13.89 15.898 15.121 15.898 16.64 C 15.898 18.158 14.667 19.39 13.148 19.39 C 11.629 19.39 10.398 18.158 10.398 16.64 Z"
fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M 14.398 16.64 C 14.398 16.225 14.734 15.89 15.148 15.89 L 19.148 15.89 C 19.562 15.89 19.898 16.225 19.898 16.64 C 19.898 17.054 19.562 17.39 19.148 17.39 L 15.148 17.39 C 14.734 17.39 14.398 17.054 14.398 16.64 Z"
fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M 6.398 16.64 C 6.398 16.225 6.734 15.89 7.148 15.89 L 11.148 15.89 C 11.562 15.89 11.898 16.225 11.898 16.64 C 11.898 17.054 11.562 17.39 11.148 17.39 L 7.148 17.39 C 6.734 17.39 6.398 17.054 6.398 16.64 Z"
fill="currentColor" />
</svg>
</template>

View File

@ -7,9 +7,9 @@ import { Runtime } from "@/api/common/omni.pb";
import { Code } from "@/api/google/rpc/code.pb";
import { Resource, ResourceService } from "@/api/grpc";
import { MachineLabelsSpec } from "@/api/omni/specs/omni.pb";
import { InfraMachineConfigSpec, InfraMachineConfigSpecAcceptanceStatus, MachineLabelsSpec } from "@/api/omni/specs/omni.pb";
import { withContext, withRuntime } from "@/api/options";
import { DefaultNamespace, MachineLabelsType, MachineLocked, MachineSetNodeType, MachineStatusType, SiderolinkResourceType, SystemLabelPrefix } from "@/api/resources";
import { DefaultNamespace, InfraMachineConfigType, MachineLabelsType, MachineLocked, MachineSetNodeType, MachineStatusType, SiderolinkResourceType, SystemLabelPrefix } from "@/api/resources";
import { MachineService } from "@/api/talos/machine/machine.pb";
import { destroyResources, getMachineConfigPatchesToDelete } from "@/methods/cluster";
import { parseLabels } from "@/methods/labels";
@ -164,3 +164,49 @@ export const updateTalosMaintenance = async (machine: string, talosVersion: stri
nodes: [machine]
}));
}
export const rejectMachine = async (machine: string) => {
await updateInfraMachineConfig(machine, (r: Resource<InfraMachineConfigSpec>) => {
r.spec.acceptance_status = InfraMachineConfigSpecAcceptanceStatus.REJECTED;
});
};
export const acceptMachine = async (machine: string) => {
await updateInfraMachineConfig(machine, (r: Resource<InfraMachineConfigSpec>) => {
r.spec.acceptance_status = InfraMachineConfigSpecAcceptanceStatus.ACCEPTED;
});
};
export const updateInfraMachineConfig = async (machine: string, modify: (r: Resource<InfraMachineConfigSpec>) => void) => {
const metadata = {
id: machine,
namespace: DefaultNamespace,
type: InfraMachineConfigType,
};
try {
const resource: Resource<InfraMachineConfigSpec> = await ResourceService.Get(metadata, withRuntime(Runtime.Omni));
modify(resource);
ResourceService.Update(resource, resource.metadata.version, withRuntime(Runtime.Omni))
} catch (e) {
if (e.code === Code.NOT_FOUND) {
const resource: Resource<InfraMachineConfigSpec> = {
metadata,
spec: {}
};
modify(resource);
await ResourceService.Create<Resource<InfraMachineConfigSpec>>(resource, withRuntime(Runtime.Omni));
}
}
}
export enum MachineFilterOption {
Manual = "manual",
Unaccepted = "unaccepted",
Provisioned = "provisioned",
PXE = "pxe",
};

View File

@ -20,6 +20,7 @@ import OmniSettings from "@/views/omni/Settings/Settings.vue";
import Authenticate from "@/views/omni/Auth/Authenticate.vue";
import OmniMachineClasses from "@/views/omni/MachineClasses/MachineClasses.vue";
import OmniMachineClass from "@/views/omni/MachineClasses/MachineClass.vue";
import OmniMachinesPending from "@/views/omni/Machines/MachinesPending.vue";
import OmniBackupStorageSettings from "@/views/omni/Settings/BackupStorage.vue";
import OIDC from "@/views/omni/Auth/OIDC.vue";
@ -72,6 +73,8 @@ import UserCreate from "@/views/omni/Modals/UserCreate.vue";
import RoleEdit from "@/views/omni/Modals/RoleEdit.vue";
import ServiceAccountCreate from "@/views/omni/Modals/ServiceAccountCreate.vue";
import ServiceAccountRenew from "@/views/omni/Modals/ServiceAccountRenew.vue";
import MachineAccept from "@/views/omni/Modals/MachineAccept.vue";
import MachineReject from "@/views/omni/Modals/MachineReject.vue";
import { current } from "@/context";
import { authGuard } from "@auth0/auth0-vue";
@ -79,13 +82,17 @@ import { AuthType, authType } from "@/methods";
import { getAuthCookies, isAuthorized } from "@/methods/key";
import { refreshTitle } from "@/methods/title";
import { loadCurrentUser } from "@/methods/auth";
import { MachineFilterOption } from "@/methods/machine";
export const FrontendAuthFlow = "frontend";
const withPrefix = (prefix: string, routes: RouteRecordRaw[], meta?: Record<string, any>) =>
routes.map((route) => {
if (meta && !route.meta) {
route.meta = meta;
if (meta) {
route.meta = {
...route.meta,
...meta,
}
}
if (!route.beforeEnter) {
@ -207,6 +214,35 @@ const routes: RouteRecordRaw[] = [
name: "Machines",
component: OmniMachines,
},
{
path: "/machines/manual",
name: "MachinesManual",
component: OmniMachines,
props: {
filter: MachineFilterOption.Manual,
},
},
{
path: "/machines/provisioned",
name: "MachinesProvisioned",
component: OmniMachines,
props: {
filter: MachineFilterOption.Provisioned,
},
},
{
path: "/machines/pxe",
name: "MachinesPXE",
component: OmniMachines,
props: {
filter: MachineFilterOption.PXE,
},
},
{
path: "/machines/pending",
name: "MachinesPending",
component: OmniMachinesPending,
},
{
path: "/machine-classes",
name: "MachineClasses",
@ -458,6 +494,8 @@ const modals = {
serviceAccountCreate: ServiceAccountCreate,
serviceAccountRenew: ServiceAccountRenew,
roleEdit: RoleEdit,
machineAccept: MachineAccept,
machineReject: MachineReject,
updateExtensions: UpdateExtensions,
};

View File

@ -9,7 +9,7 @@ included in the LICENSE file.
<div class="w-5 pointer-events-none"/>
<div class="flex-1 grid grid-cols-4 -mr-3 items-center" @click="openNodeInfo">
<div class="col-span-2 flex items-center gap-2">
<t-icon :icon="Object.keys(machine.spec.provision_status ?? {}).length ? 'cloud-connection' : 'server'" class="w-4 h-4 ml-2"/>
<t-icon :icon="icon" class="w-4 h-4 ml-2"/>
<router-link :to="{ name: 'NodeOverview', params: { cluster: clusterName, machine: machine.metadata.id }}" class="list-item-link truncate">
{{ nodeName }}
</router-link>
@ -37,7 +37,7 @@ included in the LICENSE file.
</template>
<script setup lang="ts">
import { LabelHostname, LabelCluster, MachineLocked, LabelWorkerRole, UpdateLocked } from "@/api/resources";
import { LabelHostname, LabelCluster, MachineLocked, LabelWorkerRole, UpdateLocked, LabelIsManagedByStaticInfraProvider } from "@/api/resources";
import { useRouter } from "vue-router";
import { computed, toRefs } from "vue";
import { Resource } from "@/api/grpc";
@ -61,6 +61,14 @@ const props = defineProps<{
const { machine, machineSet } = toRefs(props);
const icon = computed(() => {
if (machine.value.metadata.labels?.[LabelIsManagedByStaticInfraProvider] !== undefined) {
return "server-network";
}
return Object.keys(machine.value.spec.provision_status ?? {}).length ? "cloud-connection" : "server";
});
const locked = computed(() => {
return machine.value?.metadata?.annotations?.[MachineLocked] !== undefined;
});

View File

@ -36,6 +36,7 @@ const { resource } = toRefs(props);
defineEmits(['filterLabel']);
const labelOrder = {
"is-managed-by-static-infra-provider": -1,
"machine-request-set": -1,
"no-manual-allocation": -1,
"machine-request": -1,

View File

@ -0,0 +1,51 @@
<!--
Copyright (c) 2024 Sidero Labs, Inc.
Use of this software is governed by the Business Source License
included in the LICENSE file.
-->
<template>
<tabs-header class="border-b border-naturals-N4 pb-3.5 mb-6">
<tab-button is="router-link"
v-for="route in routes"
:key="route.name"
:to="route.to"
:selected="$route.name === route.to.name"
>
{{ route.name }}
</tab-button>
</tabs-header>
<router-view name="inner"/>
</template>
<script setup lang="ts">
import TabsHeader from "@/components/common/Tabs/TabsHeader.vue";
import TabButton from "@/components/common/Tabs/TabButton.vue";
import { RouteLocationRaw } from "vue-router";
import { computed } from "vue";
const routes = computed((): {name: string, to: RouteLocationRaw }[] => {
return [
{
name: "All",
to: { name: "Machines" },
},
{
name: "Manual",
to: { name: "MachinesManual" },
},
{
name: "Autoprovisioned",
to: { name: "MachinesProvisioned" },
},
{
name: "PXE Booted",
to: { name: "MachinesPXE" },
},
{
name: "Pending",
to: { name: "MachinesPending" },
},
];
});
</script>

View File

@ -15,7 +15,7 @@ included in the LICENSE file.
>
<template #header="{ itemsCount, filtered }">
<div class="flex gap-4">
<page-header title="Machines">
<page-header title="Machines" v-if="!filter">
<div class="flex gap-6 items-center">
<stats-item pluralized-text="Machine" :count="itemsCount" icon="nodes" :text="filtered ? ' Found' : ' Total'"/>
</div>
@ -28,7 +28,11 @@ included in the LICENSE file.
</template>
</watch>
</page-header>
<page-header title="Manually Joined Machines" v-else-if="filter === MachineFilterOption.Manual"/>
<page-header title="Machines Provisioned by the Infra Providers" v-else-if="filter === MachineFilterOption.Provisioned"/>
<page-header title="Machines Managed by the Bare Metal Providers" v-else-if="filter === MachineFilterOption.PXE"/>
</div>
<machine-tabs/>
</template>
<template #input>
<labels-input :completions-resource="{
@ -49,7 +53,7 @@ included in the LICENSE file.
<script setup lang="ts">
import { Runtime } from "@/api/common/omni.pb";
import { MetricsNamespace, MachineStatusLinkType, LabelsCompletionType, VirtualNamespace, MachineStatusType, MachineStatusMetricsType, MachineStatusMetricsID, EphemeralNamespace } from "@/api/resources";
import { MetricsNamespace, MachineStatusLinkType, LabelsCompletionType, VirtualNamespace, MachineStatusType, MachineStatusMetricsType, MachineStatusMetricsID, EphemeralNamespace, LabelIsManagedByStaticInfraProvider, LabelMachineRequest } from "@/api/resources";
import { itemID, WatchOptions } from "@/api/watch";
import TList from "@/components/common/List/TList.vue";
@ -57,20 +61,43 @@ import MachineItem from "@/views/omni/Machines/MachineItem.vue";
import PageHeader from "@/components/common/PageHeader.vue";
import { computed, ref } from "vue";
import LabelsInput from "@/views/omni/ItemLabels/LabelsInput.vue";
import { Label, addLabel, selectors } from "@/methods/labels";
import { Label, addLabel, selectors as labelsToSelectors } from "@/methods/labels";
import Watch from "@/components/common/Watch/Watch.vue";
import { Resource } from "@/api/grpc";
import { MachineStatusMetricsSpec } from "@/api/omni/specs/omni.pb";
import StatsItem from "@/components/common/Stats/StatsItem.vue";
import { MachineFilterOption } from "@/methods/machine";
import { toRefs } from "vue";
import MachineTabs from "./MachineTabs.vue";
const props = defineProps<{
filter?: MachineFilterOption,
}>();
const { filter } = toRefs(props);
const watchOpts = computed<WatchOptions>(() => {
const selectors = labelsToSelectors(filterLabels.value) ?? [];
switch (filter.value) {
case MachineFilterOption.Manual:
selectors.push(`!${LabelMachineRequest}`, `!${LabelIsManagedByStaticInfraProvider}`);
break;
case MachineFilterOption.Provisioned:
selectors.push(`${LabelMachineRequest}`);
break;
case MachineFilterOption.PXE:
selectors.push(`${LabelIsManagedByStaticInfraProvider}`);
break;
}
return {
runtime: Runtime.Omni,
resource: {
type: MachineStatusLinkType,
namespace: MetricsNamespace,
},
selectors: selectors(filterLabels.value),
selectors,
}
});

View File

@ -0,0 +1,92 @@
<!--
Copyright (c) 2024 Sidero Labs, Inc.
Use of this software is governed by the Business Source License
included in the LICENSE file.
-->
<template>
<div>
<t-list
:opts="watchOpts"
search
pagination
>
<template #header="{ itemsCount, filtered }">
<div class="flex gap-4">
<page-header title="Pending Machines">
<stats-item pluralized-text="Machine" :count="itemsCount" icon="nodes" :text="filtered ? ' Found' : ' Total'"/>
</page-header>
</div>
<machine-tabs/>
</template>
<template #default="{ items, searchQuery }">
<div class="header">
<p>ID</p>
<p>Provider</p>
</div>
<t-list-item v-for="item in items" :key="itemID(item)">
<div class="flex gap-2 items-center">
<div class="grid grid-cols-2 flex-1 gap-2">
<WordHighlighter
:query="searchQuery"
:textToHighlight="item.metadata.id"
highlightClass="bg-naturals-N14"
/>
<span>{{ item.metadata.labels?.[LabelInfraProviderID] }}</span>
</div>
<t-button icon="check" type="highlighted" @click="() => acceptMachine(item)">Accept</t-button>
<t-button icon="close" @click="() => rejectMachine(item)">Reject</t-button>
</div>
</t-list-item>
</template>
</t-list>
</div>
</template>
<script setup lang="ts">
import { Runtime } from "@/api/common/omni.pb";
import { InfraMachineType, InfraProviderNamespace, LabelMachinePendingAccept, LabelInfraProviderID } from "@/api/resources";
import { itemID, WatchOptions } from "@/api/watch";
import TList from "@/components/common/List/TList.vue";
import PageHeader from "@/components/common/PageHeader.vue";
import { computed } from "vue";
import StatsItem from "@/components/common/Stats/StatsItem.vue";
import TListItem from "@/components/common/List/TListItem.vue";
import { Resource } from "@/api/grpc";
import TButton from "@/components/common/Button/TButton.vue";
import WordHighlighter from "vue-word-highlighter";
import { useRouter } from "vue-router";
import MachineTabs from "./MachineTabs.vue";
const router = useRouter();
const watchOpts = computed<WatchOptions>(() => {
return {
runtime: Runtime.Omni,
resource: {
type: InfraMachineType,
namespace: InfraProviderNamespace,
},
selectors: [`${LabelMachinePendingAccept}`],
};
});
const acceptMachine = (item: Resource) => {
router.push({
query: { modal: "machineAccept", machine: item.metadata.id },
});
};
const rejectMachine = (item: Resource) => {
router.push({
query: { modal: "machineReject", machine: item.metadata.id },
});
};
</script>
<style scoped>
.header {
@apply grid grid-cols-2 p-2 gap-2 bg-naturals-N2 pr-56 pl-3 text-xs text-naturals-N13;
}
</style>

View File

@ -0,0 +1,68 @@
<!--
Copyright (c) 2024 Sidero Labs, Inc.
Use of this software is governed by the Business Source License
included in the LICENSE file.
-->
<template>
<div class="modal-window">
<div class="heading">
<h3 class="text-base text-naturals-N14">
Accept the Machine {{ $route.query.machine }} ?
</h3>
<close-button @click="close"/>
</div>
<p class="text-xs py-2">Please confirm the action.</p>
<div class="flex justify-end gap-4 mt-8">
<t-button @click="reject" class="w-32 h-9" icon="check" iconPosition="left">
Accept
</t-button>
</div>
</div>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from "vue-router";
import { showError, showSuccess } from "@/notification";
import CloseButton from "@/views/omni/Modals/CloseButton.vue";
import TButton from "@/components/common/Button/TButton.vue";
import { acceptMachine } from "@/methods/machine";
const router = useRouter();
const route = useRoute();
let closed = false;
const close = () => {
if (closed) {
return;
}
closed = true;
router.go(-1);
};
const reject = async () => {
try {
await acceptMachine(route.query.machine as string);
} catch (e) {
showError(`Failed to Accept the Machine ${route.query.machine}`, e.message)
}
close();
showSuccess(
`The Machine ${route.query.machine} was Accepted`,
);
}
</script>
<style scoped>
.heading {
@apply flex justify-between items-center mb-5 text-xl text-naturals-N14;
}
</style>

View File

@ -0,0 +1,69 @@
<!--
Copyright (c) 2024 Sidero Labs, Inc.
Use of this software is governed by the Business Source License
included in the LICENSE file.
-->
<template>
<div class="modal-window">
<div class="heading">
<h3 class="text-base text-naturals-N14">
Reject the Machine {{ $route.query.machine }} ?
</h3>
<close-button @click="close"/>
</div>
<p class="text-xs py-2">Please confirm the action.</p>
<p class="text-xs py-2 text-primary-P2">Rejected machine will not appear in the UI anymore. You can use omnictl to accept it again.</p>
<div class="flex justify-end gap-4 mt-8">
<t-button @click="reject" class="w-32 h-9" icon="close" iconPosition="left">
Reject
</t-button>
</div>
</div>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from "vue-router";
import { showError, showSuccess } from "@/notification";
import CloseButton from "@/views/omni/Modals/CloseButton.vue";
import TButton from "@/components/common/Button/TButton.vue";
import { rejectMachine } from "@/methods/machine";
const router = useRouter();
const route = useRoute();
let closed = false;
const close = () => {
if (closed) {
return;
}
closed = true;
router.go(-1);
};
const reject = async () => {
try {
await rejectMachine(route.query.machine as string);
} catch (e) {
showError(`Failed to Reject the Machine ${route.query.machine}`, e.message)
}
close();
showSuccess(
`The Machine ${route.query.machine} was Rejected`,
);
}
</script>
<style scoped>
.heading {
@apply flex justify-between items-center mb-5 text-xl text-naturals-N14;
}
</style>

View File

@ -92,6 +92,12 @@ func NewClusterMachineStatusController() *ClusterMachineStatusController {
return err
}
if _, ok := machineStatus.Metadata().Labels().Get(omni.LabelIsManagedByStaticInfraProvider); ok {
clusterMachineStatus.Metadata().Labels().Set(omni.LabelIsManagedByStaticInfraProvider, "")
} else {
clusterMachineStatus.Metadata().Labels().Delete(omni.LabelIsManagedByStaticInfraProvider)
}
if err = updateMachineProvisionStatus(ctx, r, machineStatus, cmsVal); err != nil {
return err
}
@ -255,13 +261,12 @@ func NewClusterMachineStatusController() *ClusterMachineStatusController {
func updateMachineProvisionStatus(ctx context.Context, r controller.Reader, machineStatus *omni.MachineStatus, cmsVal *specs.ClusterMachineStatusSpec) error {
machineRequestID, ok := machineStatus.Metadata().Labels().Get(omni.LabelMachineRequest)
if !ok {
cmsVal.ProvisionStatus = nil
}
cmsVal.ProvisionStatus = &specs.ClusterMachineStatusSpec_ProvisionStatus{}
cmsVal.ProvisionStatus.RequestId = machineRequestID
if ok {
cmsVal.ProvisionStatus.RequestId = machineRequestID
}
machineRequestStatus, err := safe.ReaderGetByID[*infra.MachineRequestStatus](ctx, r, machineRequestID)
if err != nil && !state.IsNotFoundError(err) {

View File

@ -25,6 +25,7 @@ import (
"github.com/siderolabs/omni/client/pkg/omni/resources/infra"
"github.com/siderolabs/omni/client/pkg/omni/resources/omni"
"github.com/siderolabs/omni/client/pkg/omni/resources/siderolink"
"github.com/siderolabs/omni/internal/backend/runtime/omni/controllers/helpers"
)
// InfraMachineControllerName is the name of the controller.
@ -164,13 +165,18 @@ func (h *infraMachineControllerHelper) finalizerRemovalExtraOutput(ctx context.C
return err
}
_, err = helpers.HandleInput[*omni.InfraMachineConfig](ctx, r, InfraMachineControllerName, link)
if err != nil {
return err
}
return nil
}
// applyInfraMachineConfig applies the user-managed configuration from the omni.InfraMachineConfig resource into the infra.Machine.
func (h *infraMachineControllerHelper) applyInfraMachineConfig(ctx context.Context, r controller.Reader, link *siderolink.Link, infraMachine *infra.Machine) error {
config, err := safe.ReaderGetByID[*omni.InfraMachineConfig](ctx, r, link.Metadata().ID())
if err != nil && !state.IsNotFoundError(err) {
func (h *infraMachineControllerHelper) applyInfraMachineConfig(ctx context.Context, r controller.ReaderWriter, link *siderolink.Link, infraMachine *infra.Machine) error {
config, err := helpers.HandleInput[*omni.InfraMachineConfig](ctx, r, InfraMachineControllerName, link)
if err != nil {
return err
}
@ -180,9 +186,13 @@ func (h *infraMachineControllerHelper) applyInfraMachineConfig(ctx context.Conte
infraMachine.TypedSpec().Value.PreferredPowerState = defaultPreferredPowerState
infraMachine.TypedSpec().Value.ExtraKernelArgs = ""
pendingAccept := config == nil
if config != nil { // apply user configuration: acceptance, preferred power state, extra kernel args
infraMachine.TypedSpec().Value.AcceptanceStatus = config.TypedSpec().Value.AcceptanceStatus
pendingAccept = infraMachine.TypedSpec().Value.AcceptanceStatus == specs.InfraMachineConfigSpec_PENDING
switch config.TypedSpec().Value.PowerState {
case specs.InfraMachineConfigSpec_POWER_STATE_OFF:
infraMachine.TypedSpec().Value.PreferredPowerState = specs.InfraMachineSpec_POWER_STATE_OFF
@ -197,6 +207,12 @@ func (h *infraMachineControllerHelper) applyInfraMachineConfig(ctx context.Conte
infraMachine.TypedSpec().Value.ExtraKernelArgs = config.TypedSpec().Value.ExtraKernelArgs
}
if pendingAccept {
infraMachine.Metadata().Labels().Set(omni.LabelMachinePendingAccept, "")
} else {
infraMachine.Metadata().Labels().Delete(omni.LabelMachinePendingAccept)
}
return nil
}

View File

@ -1 +1 @@
v0.45.0-beta.0
v0.45.0-beta.0