mirror of https://github.com/halo-dev/halo
Trigger attachment status update on storage policy config change (#6639)
parent
6a5e9c4932
commit
07d200b45b
|
@ -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";
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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)"
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue