From 07d200b45b67035b8ed5ec1d7957eaf59e24b453 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:13:19 +0800 Subject: [PATCH] Trigger attachment status update on storage policy config change (#6639) --- .../app/core/extension/attachment/Policy.java | 1 + .../PolicyConfigChangeDetector.java | 141 ++++++++++++++++++ .../reconciler/PolicyReconciler.java | 44 ++++++ .../extensions/attachment-local-policy.yaml | 4 +- .../PolicyConfigChangeDetectorTest.java | 65 ++++++++ 5 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 application/src/main/java/run/halo/app/core/attachment/PolicyConfigChangeDetector.java create mode 100644 application/src/main/java/run/halo/app/core/attachment/reconciler/PolicyReconciler.java create mode 100644 application/src/test/java/run/halo/app/core/attachment/PolicyConfigChangeDetectorTest.java diff --git a/api/src/main/java/run/halo/app/core/extension/attachment/Policy.java b/api/src/main/java/run/halo/app/core/extension/attachment/Policy.java index 450548a9f..de90f2927 100644 --- a/api/src/main/java/run/halo/app/core/extension/attachment/Policy.java +++ b/api/src/main/java/run/halo/app/core/extension/attachment/Policy.java @@ -16,6 +16,7 @@ import run.halo.app.extension.GVK; @GVK(group = Constant.GROUP, version = Constant.VERSION, kind = KIND, plural = "policies", singular = "policy") public class Policy extends AbstractExtension { + public static final String POLICY_OWNER_LABEL = "storage.halo.run/policy-owner"; public static final String KIND = "Policy"; diff --git a/application/src/main/java/run/halo/app/core/attachment/PolicyConfigChangeDetector.java b/application/src/main/java/run/halo/app/core/attachment/PolicyConfigChangeDetector.java new file mode 100644 index 000000000..e8c43bbb9 --- /dev/null +++ b/application/src/main/java/run/halo/app/core/attachment/PolicyConfigChangeDetector.java @@ -0,0 +1,141 @@ +package run.halo.app.core.attachment; + +import static run.halo.app.extension.index.query.QueryFactory.equal; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.context.SmartLifecycle; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import run.halo.app.core.extension.attachment.Attachment; +import run.halo.app.core.extension.attachment.Policy; +import run.halo.app.extension.ConfigMap; +import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.ExtensionMatcher; +import run.halo.app.extension.GroupVersionKind; +import run.halo.app.extension.ListOptions; +import run.halo.app.extension.MetadataUtil; +import run.halo.app.extension.controller.Controller; +import run.halo.app.extension.controller.ControllerBuilder; +import run.halo.app.extension.controller.DefaultController; +import run.halo.app.extension.controller.DefaultQueue; +import run.halo.app.extension.controller.Reconciler; +import run.halo.app.extension.controller.RequestQueue; + +/** + *

Detects changes to {@link ConfigMap} that are referenced by {@link Policy} and updates the + * {@link Attachment} with the {@link Policy} reference to reflect the change.

+ *

Without this, the link to the attachment corresponding to the storage policy configuration + * change may not be correctly updated and only the service can be restarted.

+ * + * @author guqing + * @since 2.20.0 + */ +@Component +@RequiredArgsConstructor +public class PolicyConfigChangeDetector implements Reconciler { + static final String POLICY_UPDATED_AT = "storage.halo.run/policy-updated-at"; + private final GroupVersionKind attachmentGvk = GroupVersionKind.fromExtension(Attachment.class); + private final ExtensionClient client; + private final AttachmentUpdateTrigger attachmentUpdateTrigger; + + @Override + public Result reconcile(Request request) { + client.fetch(ConfigMap.class, request.name()) + .ifPresent(configMap -> { + var labels = configMap.getMetadata().getLabels(); + if (labels == null || !labels.containsKey(Policy.POLICY_OWNER_LABEL)) { + return; + } + var policyName = labels.get(Policy.POLICY_OWNER_LABEL); + var attachmentNames = client.indexedQueryEngine() + .retrieveAll(attachmentGvk, ListOptions.builder() + .andQuery(equal("spec.policyName", policyName)) + .build(), + Sort.unsorted() + ); + attachmentUpdateTrigger.addAll(attachmentNames); + }); + return Result.doNotRetry(); + } + + @Override + public Controller setupWith(ControllerBuilder builder) { + ExtensionMatcher matcher = extension -> { + var configMap = (ConfigMap) extension; + var labels = configMap.getMetadata().getLabels(); + return labels != null && labels.containsKey(Policy.POLICY_OWNER_LABEL); + }; + return builder + .extension(new ConfigMap()) + .syncAllOnStart(false) + .onAddMatcher(matcher) + .onUpdateMatcher(matcher) + .onDeleteMatcher(matcher) + .build(); + } + + @Component + static class AttachmentUpdateTrigger implements Reconciler, SmartLifecycle { + private final RequestQueue queue; + + private final Controller controller; + + private volatile boolean running = false; + + private final ExtensionClient client; + + public AttachmentUpdateTrigger(ExtensionClient client) { + this.client = client; + this.queue = new DefaultQueue<>(Instant::now); + this.controller = this.setupWith(null); + } + + @Override + public Result reconcile(String name) { + client.fetch(Attachment.class, name).ifPresent(attachment -> { + var annotations = MetadataUtil.nullSafeAnnotations(attachment); + annotations.put(POLICY_UPDATED_AT, Instant.now().toString()); + client.update(attachment); + }); + return Result.doNotRetry(); + } + + void addAll(List names) { + for (String name : names) { + queue.addImmediately(name); + } + } + + @Override + public Controller setupWith(ControllerBuilder builder) { + return new DefaultController<>( + "PolicyChangeAttachmentUpdater", + this, + queue, + null, + Duration.ofMillis(100), + Duration.ofMinutes(10) + ); + } + + @Override + public void start() { + controller.start(); + running = true; + } + + @Override + public void stop() { + running = false; + controller.dispose(); + } + + @Override + public boolean isRunning() { + return running; + } + } +} diff --git a/application/src/main/java/run/halo/app/core/attachment/reconciler/PolicyReconciler.java b/application/src/main/java/run/halo/app/core/attachment/reconciler/PolicyReconciler.java new file mode 100644 index 000000000..000b5bd10 --- /dev/null +++ b/application/src/main/java/run/halo/app/core/attachment/reconciler/PolicyReconciler.java @@ -0,0 +1,44 @@ +package run.halo.app.core.attachment.reconciler; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import run.halo.app.core.extension.attachment.Policy; +import run.halo.app.extension.ConfigMap; +import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.MetadataUtil; +import run.halo.app.extension.controller.Controller; +import run.halo.app.extension.controller.ControllerBuilder; +import run.halo.app.extension.controller.Reconciler; + +@Component +@RequiredArgsConstructor +public class PolicyReconciler implements Reconciler { + private final ExtensionClient client; + + @Override + public Result reconcile(Request request) { + client.fetch(Policy.class, request.name()) + .ifPresent(this::checkOwnerLabel); + return Result.doNotRetry(); + } + + private void checkOwnerLabel(Policy policy) { + var policyName = policy.getMetadata().getName(); + var configMapName = policy.getSpec().getConfigMapName(); + client.fetch(ConfigMap.class, configMapName) + .ifPresent(configMap -> { + var labels = MetadataUtil.nullSafeLabels(configMap); + labels.put(Policy.POLICY_OWNER_LABEL, policyName); + client.update(configMap); + }); + } + + @Override + public Controller setupWith(ControllerBuilder builder) { + return builder + .extension(new Policy()) + // sync on start for compatible with previous data + .syncAllOnStart(true) + .build(); + } +} diff --git a/application/src/main/resources/extensions/attachment-local-policy.yaml b/application/src/main/resources/extensions/attachment-local-policy.yaml index c4a331c72..faba8b06f 100644 --- a/application/src/main/resources/extensions/attachment-local-policy.yaml +++ b/application/src/main/resources/extensions/attachment-local-policy.yaml @@ -19,6 +19,8 @@ apiVersion: v1alpha1 kind: ConfigMap metadata: name: default-policy-config + labels: + storage.halo.run/policy-owner: default-policy data: default: "{\"location\":\"\"}" --- @@ -38,7 +40,7 @@ spec: - $formkit: text name: maxFileSize label: 最大单文件大小 - validation: [['matches', '/^(0|[1-9]\d*)(?:[KMG]B)?$/']] + validation: [ [ 'matches', '/^(0|[1-9]\d*)(?:[KMG]B)?$/' ] ] validation-visibility: "live" validation-messages: matches: "输入格式错误,遵循:整数 + 大写的单位(KB, MB, GB)" diff --git a/application/src/test/java/run/halo/app/core/attachment/PolicyConfigChangeDetectorTest.java b/application/src/test/java/run/halo/app/core/attachment/PolicyConfigChangeDetectorTest.java new file mode 100644 index 000000000..9954a7962 --- /dev/null +++ b/application/src/test/java/run/halo/app/core/attachment/PolicyConfigChangeDetectorTest.java @@ -0,0 +1,65 @@ +package run.halo.app.core.attachment; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import run.halo.app.core.extension.attachment.Attachment; +import run.halo.app.core.extension.attachment.Policy; +import run.halo.app.extension.ConfigMap; +import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.GroupVersionKind; +import run.halo.app.extension.Metadata; +import run.halo.app.extension.controller.Reconciler; +import run.halo.app.extension.index.IndexedQueryEngine; + +/** + * Tests for {@link PolicyConfigChangeDetector}. + * + * @author guqing + * @since 2.20.0 + */ +@ExtendWith(MockitoExtension.class) +class PolicyConfigChangeDetectorTest { + + @Mock + private PolicyConfigChangeDetector.AttachmentUpdateTrigger updateTrigger; + + @Mock + private ExtensionClient client; + + @InjectMocks + private PolicyConfigChangeDetector policyConfigChangeDetector; + + @Test + void reconcileTest() { + final var spyDetector = spy(policyConfigChangeDetector); + + var configMap = new ConfigMap(); + configMap.setMetadata(new Metadata()); + configMap.getMetadata().setLabels(Map.of(Policy.POLICY_OWNER_LABEL, "fake-policy")); + + when(client.fetch(eq(ConfigMap.class), eq("fake-config"))) + .thenReturn(Optional.of(configMap)); + + var indexQueryEngine = mock(IndexedQueryEngine.class); + when(client.indexedQueryEngine()).thenReturn(indexQueryEngine); + when(indexQueryEngine.retrieveAll(eq(GroupVersionKind.fromExtension(Attachment.class)), + any(), any())).thenReturn(List.of("fake-attachment")); + + spyDetector.reconcile(new Reconciler.Request("fake-config")); + + verify(updateTrigger).addAll(List.of("fake-attachment")); + } +}