Trigger attachment status update on storage policy config change (#6639)

pull/6648/head
guqing 2024-09-12 17:13:19 +08:00 committed by GitHub
parent 6a5e9c4932
commit 07d200b45b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 254 additions and 1 deletions

View File

@ -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";

View File

@ -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;
/**
* <p>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.</p>
* <p>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.</p>
*
* @author guqing
* @since 2.20.0
*/
@Component
@RequiredArgsConstructor
public class PolicyConfigChangeDetector implements Reconciler<Reconciler.Request> {
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<String>, SmartLifecycle {
private final RequestQueue<String> 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<String> 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;
}
}
}

View File

@ -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<Reconciler.Request> {
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();
}
}

View File

@ -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"

View File

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