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"));
+ }
+}