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,
|
@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = KIND,
|
||||||
plural = "policies", singular = "policy")
|
plural = "policies", singular = "policy")
|
||||||
public class Policy extends AbstractExtension {
|
public class Policy extends AbstractExtension {
|
||||||
|
public static final String POLICY_OWNER_LABEL = "storage.halo.run/policy-owner";
|
||||||
|
|
||||||
public static final String KIND = "Policy";
|
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
|
kind: ConfigMap
|
||||||
metadata:
|
metadata:
|
||||||
name: default-policy-config
|
name: default-policy-config
|
||||||
|
labels:
|
||||||
|
storage.halo.run/policy-owner: default-policy
|
||||||
data:
|
data:
|
||||||
default: "{\"location\":\"\"}"
|
default: "{\"location\":\"\"}"
|
||||||
---
|
---
|
||||||
|
@ -38,7 +40,7 @@ spec:
|
||||||
- $formkit: text
|
- $formkit: text
|
||||||
name: maxFileSize
|
name: maxFileSize
|
||||||
label: 最大单文件大小
|
label: 最大单文件大小
|
||||||
validation: [['matches', '/^(0|[1-9]\d*)(?:[KMG]B)?$/']]
|
validation: [ [ 'matches', '/^(0|[1-9]\d*)(?:[KMG]B)?$/' ] ]
|
||||||
validation-visibility: "live"
|
validation-visibility: "live"
|
||||||
validation-messages:
|
validation-messages:
|
||||||
matches: "输入格式错误,遵循:整数 + 大写的单位(KB, MB, GB)"
|
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