Refactor plugin reconciliation for dependency mechanism (#5900)

#### What type of PR is this?

/kind improvement
/area core
/area plugin

#### What this PR does / why we need it:

This PR wholly refactors plugin reconciliation to implement dependency mechanism.

Currently,
- If we disable plugin which has dependents, the plugin must wait for dependents to be disabled.
- If we enable plugin which has dependencies , the plugin must wait for dependencies to be enabled.
- If we upgrade plugin which has dependents, the plugin must request dependents to be unloaded. After the plugin is unloaded, the plugin must cancel unload request for dependents.

#### Which issue(s) this PR fixes:

Fixes #5872 

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?

```release-note
优化被依赖的插件的升级,启用和禁用
```
This commit is contained in:
John Niang
2024-05-27 16:16:56 +08:00
committed by GitHub
parent 769b19c23c
commit 5df51bb715
28 changed files with 993 additions and 407 deletions

View File

@@ -0,0 +1,93 @@
<script lang="ts" setup>
import { VButton, VModal } from "@halo-dev/components";
import type { Plugin } from "@halo-dev/api-client";
import { formatDatetime, relativeTimeTo } from "@/utils/date";
import { ref } from "vue";
withDefaults(defineProps<{ plugin: Plugin }>(), {});
const emit = defineEmits<{
(event: "close"): void;
}>();
const modal = ref();
</script>
<template>
<VModal
ref="modal"
:body-class="['!p-0']"
:title="$t('core.plugin.conditions_modal.title')"
:width="900"
layer-closable
@close="emit('close')"
>
<table class="min-w-full divide-y divide-gray-100">
<thead class="bg-gray-50">
<tr>
<th
class="px-4 py-3 text-left text-sm font-semibold text-gray-900 sm:w-96"
scope="col"
>
{{ $t("core.plugin.conditions_modal.fields.type") }}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-sm font-semibold text-gray-900"
>
{{ $t("core.plugin.conditions_modal.fields.status") }}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-sm font-semibold text-gray-900"
>
{{ $t("core.plugin.conditions_modal.fields.reason") }}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-sm font-semibold text-gray-900"
>
{{ $t("core.plugin.conditions_modal.fields.message") }}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-sm font-semibold text-gray-900"
>
{{ $t("core.plugin.conditions_modal.fields.last_transition_time") }}
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 bg-white">
<tr
v-for="(condition, index) in plugin?.status?.conditions"
:key="index"
>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900">
{{ condition.type }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500">
{{ condition.status }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500">
{{ condition.reason || "-" }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500">
{{ condition.message || "-" }}
</td>
<td
v-tooltip="formatDatetime(condition.lastTransitionTime)"
class="whitespace-nowrap px-4 py-3 text-sm text-gray-500"
>
{{ relativeTimeTo(condition.lastTransitionTime) }}
</td>
</tr>
</tbody>
</table>
<template #footer>
<VButton @click="modal.close()">
{{ $t("core.common.buttons.close") }}
</VButton>
</template>
</VModal>
</template>

View File

@@ -43,7 +43,8 @@ const { plugin } = toRefs(props);
const selectedNames = inject<Ref<string[]>>("selectedNames", ref([]));
const { getStatusMessage, uninstall } = usePluginLifeCycle(plugin);
const { getStatusDotState, getStatusMessage, uninstall } =
usePluginLifeCycle(plugin);
const pluginUpgradeModalVisible = ref(false);
@@ -147,19 +148,11 @@ const { startFields, endFields } = useEntityFieldItemExtensionPoint<Plugin>(
"plugin:list-item:field:create",
plugin,
computed((): EntityFieldItem[] => {
const { enabled } = props.plugin.spec || {};
const { phase } = props.plugin.status || {};
const shouldHideStatusDot =
!enabled || (enabled && phase === PluginStatusPhaseEnum.Started);
const getStatusDotState = () => {
if (enabled && phase === PluginStatusPhaseEnum.Failed) {
return "error";
}
return "default";
};
phase === PluginStatusPhaseEnum.Started ||
phase === PluginStatusPhaseEnum.Disabled;
return [
{

View File

@@ -9,6 +9,7 @@ import { useMutation } from "@tanstack/vue-query";
interface usePluginLifeCycleReturn {
isStarted: ComputedRef<boolean | undefined>;
getStatusDotState: () => string;
getStatusMessage: () => string | undefined;
changeStatus: () => void;
changingStatus: Ref<boolean>;
@@ -27,26 +28,41 @@ export function usePluginLifeCycle(
);
});
const getStatusDotState = () => {
const { phase } = plugin?.value?.status || {};
const { enabled } = plugin?.value?.spec || {};
if (enabled && phase === PluginStatusPhaseEnum.Failed) {
return "error";
}
if (phase === PluginStatusPhaseEnum.Disabling) {
return "warning";
}
return "default";
};
const getStatusMessage = () => {
if (!plugin?.value) return;
const { enabled } = plugin.value.spec || {};
const { phase } = plugin.value.status || {};
// Starting failed
if (enabled && phase === PluginStatusPhaseEnum.Failed) {
if (
phase === PluginStatusPhaseEnum.Failed ||
phase === PluginStatusPhaseEnum.Disabling
) {
const lastCondition = plugin.value.status?.conditions?.[0];
return (
[lastCondition?.reason, lastCondition?.message]
.filter(Boolean)
.join(":") || "Unknown"
.join(": ") || "Unknown"
);
}
// Starting up
if (
enabled &&
phase !== (PluginStatusPhaseEnum.Started || PluginStatusPhaseEnum.Failed)
) {
return t("core.common.status.starting_up");
@@ -153,6 +169,7 @@ export function usePluginLifeCycle(
return {
isStarted,
getStatusDotState,
getStatusMessage,
changeStatus,
changingStatus,

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import {
VAlert,
VButton,
VDescription,
VDescriptionItem,
VSwitch,
@@ -8,12 +9,18 @@ import {
import type { Ref } from "vue";
import { computed, inject } from "vue";
import { apiClient } from "@/utils/api-client";
import type { Plugin, Role } from "@halo-dev/api-client";
import {
PluginStatusPhaseEnum,
type Plugin,
type Role,
} from "@halo-dev/api-client";
import { pluginLabels, roleLabels } from "@/constants/labels";
import { rbacAnnotations } from "@/constants/annotations";
import { usePluginLifeCycle } from "../composables/use-plugin";
import { formatDatetime } from "@/utils/date";
import { useQuery } from "@tanstack/vue-query";
import { ref } from "vue";
import PluginConditionsModal from "../components/PluginConditionsModal.vue";
const plugin = inject<Ref<Plugin | undefined>>("plugin");
const { changeStatus, changingStatus } = usePluginLifeCycle(plugin);
@@ -60,9 +67,30 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
});
return groups;
});
// Error alert
const conditionsModalVisible = ref(false);
const errorAlertVisible = computed(() => {
const { phase } = plugin?.value?.status || {};
return (
phase !== PluginStatusPhaseEnum.Started &&
phase !== PluginStatusPhaseEnum.Disabled
);
});
const lastCondition = computed(() => {
return plugin?.value?.status?.conditions?.[0];
});
</script>
<template>
<PluginConditionsModal
v-if="conditionsModalVisible && plugin"
:plugin="plugin"
@close="conditionsModalVisible = false"
/>
<Transition mode="out-in" name="fade">
<div class="overflow-hidden rounded-b-base">
<div class="flex items-center justify-between bg-white px-4 py-4 sm:px-6">
@@ -80,18 +108,21 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
</div>
</div>
<div
v-if="
plugin?.status?.phase === 'FAILED' &&
plugin?.status?.conditions?.length
"
v-if="errorAlertVisible && lastCondition"
class="w-full px-4 pb-2 sm:px-6"
>
<VAlert
type="error"
:title="plugin?.status?.conditions?.[0].reason"
:description="plugin?.status?.conditions?.[0].message"
:title="lastCondition.reason"
:description="lastCondition.message"
:closable="false"
/>
>
<template #actions>
<VButton size="sm" @click="conditionsModalVisible = true">
{{ $t("core.plugin.detail.operations.view_conditions.button") }}
</VButton>
</template>
</VAlert>
</div>
<div class="border-t border-gray-200">
<VDescription>