Skip to content

Commit

Permalink
Merge branch 'main' into fix/editor-list
Browse files Browse the repository at this point in the history
  • Loading branch information
ruibaby authored Sep 13, 2024
2 parents 2908e43 + 8ab8a44 commit 3eca362
Show file tree
Hide file tree
Showing 12 changed files with 406 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
@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";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package run.halo.app.content.comment;

import static org.apache.commons.lang3.BooleanUtils.isTrue;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.isNull;
Expand All @@ -8,8 +9,8 @@
import java.time.Instant;
import java.util.ArrayList;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
import org.springframework.dao.OptimisticLockingFailureException;
Expand All @@ -29,6 +30,7 @@
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.infra.exception.RequestRestrictedException;
import run.halo.app.metrics.CounterService;

/**
Expand All @@ -40,6 +42,9 @@
@Service
public class ReplyServiceImpl extends AbstractCommentService implements ReplyService {

private final Supplier<RequestRestrictedException> requestRestrictedExceptionSupplier =
() -> new RequestRestrictedException("problemDetail.comment.waitingForApproval");

public ReplyServiceImpl(RoleService roleService, ReactiveExtensionClient client,
UserService userService, CounterService counterService) {
super(roleService, client, userService, counterService);
Expand All @@ -48,26 +53,32 @@ public ReplyServiceImpl(RoleService roleService, ReactiveExtensionClient client,
@Override
public Mono<Reply> create(String commentName, Reply reply) {
return client.get(Comment.class, commentName)
.flatMap(comment -> prepareReply(commentName, reply)
.flatMap(client::create)
.flatMap(createdReply -> {
var quotedReply = createdReply.getSpec().getQuoteReply();
if (StringUtils.isBlank(quotedReply)) {
return Mono.just(createdReply);
}
return approveReply(quotedReply)
.thenReturn(createdReply);
})
.flatMap(createdReply -> approveComment(comment)
.thenReturn(createdReply)
)
);
.flatMap(this::approveComment)
.filter(comment -> isTrue(comment.getSpec().getApproved()))
.switchIfEmpty(Mono.error(requestRestrictedExceptionSupplier))
.flatMap(comment -> prepareReply(commentName, reply))
.flatMap(this::doCreateReply);
}

private Mono<Reply> doCreateReply(Reply prepared) {
var quotedReply = prepared.getSpec().getQuoteReply();
if (StringUtils.isBlank(quotedReply)) {
return client.create(prepared);
}
return approveReply(quotedReply)
.filter(reply -> isTrue(reply.getSpec().getApproved()))
.switchIfEmpty(Mono.error(requestRestrictedExceptionSupplier))
.flatMap(approvedQuoteReply -> client.create(prepared));
}

private Mono<Comment> approveComment(Comment comment) {
return hasCommentManagePermission()
.filter(Boolean::booleanValue)
.flatMap(hasPermission -> doApproveComment(comment));
.flatMap(hasPermission -> {
if (hasPermission) {
return doApproveComment(comment);
}
return Mono.just(comment);
});
}

private Mono<Comment> doApproveComment(Comment comment) {
Expand All @@ -81,23 +92,26 @@ private Mono<Comment> doApproveComment(Comment comment) {
e -> updateCommentWithRetry(comment.getMetadata().getName(), updateFunc));
}

private Mono<Void> approveReply(String replyName) {
private Mono<Reply> approveReply(String replyName) {
return hasCommentManagePermission()
.filter(Boolean::booleanValue)
.flatMap(hasPermission -> doApproveReply(replyName));
.flatMap(hasPermission -> {
if (hasPermission) {
return doApproveReply(replyName);
}
return client.get(Reply.class, replyName);
});
}

private Mono<Void> doApproveReply(String replyName) {
return Mono.defer(() -> client.fetch(Reply.class, replyName)
private Mono<Reply> doApproveReply(String replyName) {
return Mono.defer(() -> client.get(Reply.class, replyName)
.flatMap(reply -> {
reply.getSpec().setApproved(true);
reply.getSpec().setApprovedTime(Instant.now());
return client.update(reply);
})
)
.retryWhen(Retry.backoff(8, Duration.ofMillis(100))
.filter(OptimisticLockingFailureException.class::isInstance))
.then();
.filter(OptimisticLockingFailureException.class::isInstance));
}

private Mono<Comment> updateCommentWithRetry(String name, UnaryOperator<Comment> updateFunc) {
Expand All @@ -123,7 +137,7 @@ private Mono<Reply> prepareReply(String commentName, Reply reply) {
if (reply.getSpec().getApproved() == null) {
reply.getSpec().setApproved(false);
}
if (BooleanUtils.isTrue(reply.getSpec().getApproved())
if (isTrue(reply.getSpec().getApproved())
&& reply.getSpec().getApprovedTime() == null) {
reply.getSpec().setApprovedTime(Instant.now());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package run.halo.app.infra.exception;

/**
* <p>{@link RequestRestrictedException} indicates that the client's request was denied because
* it did not meet certain required conditions.</p>
* <p>Typically, this exception is thrown when a user attempts to perform an action that
* requires prior approval or validation, such as replying to a comment that has not yet been
* approved.</p>
* <p>The server understands the request but refuses to process it due to the lack of
* necessary approval.</p>
*
* @author guqing
* @since 2.20.0
*/
public class RequestRestrictedException extends AccessDeniedException {

public RequestRestrictedException(String reason) {
super(reason);
}

public RequestRestrictedException(String reason, String detailCode, Object[] detailArgs) {
super(reason, detailCode, detailArgs);
}
}
Loading

0 comments on commit 3eca362

Please sign in to comment.