This is an automated email from the ASF dual-hosted git repository. ilgrosso pushed a commit to branch 4_0_X in repository https://gitbox.apache.org/repos/asf/syncope.git
commit 4b23ba5e1a18eadc2e8e70582a53748dd0d96c37 Author: Francesco Chicchiriccò <[email protected]> AuthorDate: Tue Feb 3 10:43:29 2026 +0100 [SYNCOPE-1947] Job actuator endpoint --- .../syncope/client/console/widgets/JobWidget.java | 7 +- .../java/job/SyncopeTaskScheduler.java | 10 ++- .../src/test/resources/core-debug.properties | 2 +- .../core/starter/SyncopeCoreApplication.java | 8 ++ .../syncope/core/starter/actuate/JobEndpoint.java | 87 ++++++++++++++++++++++ core/starter/src/main/resources/core.properties | 2 +- .../src/main/resources/core-embedded.properties | 2 +- .../asciidoc/reference-guide/concepts/routes.adoc | 4 +- .../asciidoc/reference-guide/usage/actuator.adoc | 7 +- 9 files changed, 115 insertions(+), 14 deletions(-) diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/widgets/JobWidget.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/widgets/JobWidget.java index bce5877aa9..635a45017f 100644 --- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/widgets/JobWidget.java +++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/widgets/JobWidget.java @@ -213,10 +213,7 @@ public class JobWidget extends BaseWidget { List<JobTO> updatedAvailable = new ArrayList<>(); if (SyncopeConsoleSession.get().owns(IdRepoEntitlement.NOTIFICATION_LIST)) { - JobTO notificationJob = notificationRestClient.getJob(); - if (notificationJob != null) { - updatedAvailable.add(notificationJob); - } + Optional.ofNullable(notificationRestClient.getJob()).ifPresent(updatedAvailable::add); } if (SyncopeConsoleSession.get().owns(IdRepoEntitlement.TASK_LIST)) { updatedAvailable.addAll(taskRestClient.listJobs()); @@ -425,6 +422,8 @@ public class JobWidget extends BaseWidget { taskType = TaskType.SCHEDULED; } else if (jobTO.getRefDesc().startsWith("PULL")) { taskType = TaskType.PULL; + } else if (jobTO.getRefDesc().startsWith("LIVE_SYNC")) { + taskType = TaskType.LIVE_SYNC; } else if (jobTO.getRefDesc().startsWith("PUSH")) { taskType = TaskType.PUSH; } else if (jobTO.getRefDesc().startsWith("MACRO")) { diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/SyncopeTaskScheduler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/SyncopeTaskScheduler.java index 804a0323ed..413f2335dd 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/SyncopeTaskScheduler.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/SyncopeTaskScheduler.java @@ -39,16 +39,14 @@ import org.springframework.scheduling.support.CronTrigger; public class SyncopeTaskScheduler { - protected record Key(String domain, String job) { + public record Key(String domain, String job) { } - protected record Value(Job job, Optional<ScheduledFuture<?>> instant, Optional<ScheduledFuture<?>> cron) { + public record Value(Job job, Optional<ScheduledFuture<?>> instant, Optional<ScheduledFuture<?>> cron) { } - public static final String CACHE = "jobCache"; - protected static final Logger LOG = LoggerFactory.getLogger(SyncopeTaskScheduler.class); protected final TaskScheduler scheduler; @@ -152,4 +150,8 @@ public class SyncopeTaskScheduler { public List<String> getJobNames(final String domain) { return jobs.keySet().stream().filter(key -> domain.equals(key.domain())).map(Key::job).toList(); } + + public Map<Key, Value> getJobs() { + return Map.copyOf(jobs); + } } diff --git a/core/self-keymaster-starter/src/test/resources/core-debug.properties b/core/self-keymaster-starter/src/test/resources/core-debug.properties index 4faa0d847c..8af477ddff 100644 --- a/core/self-keymaster-starter/src/test/resources/core-debug.properties +++ b/core/self-keymaster-starter/src/test/resources/core-debug.properties @@ -20,7 +20,7 @@ service.discovery.address=http://localhost:9080/syncope/rest/ logging.config=file://${project.build.testOutputDirectory}/log4j2.xml -management.endpoints.web.exposure.include=health,info,beans,env,loggers,entityCache,metrics +management.endpoints.web.exposure.include=health,info,beans,env,loggers,entityCache,job,metrics keymaster.address=http://localhost:9080/syncope/rest/keymaster keymaster.username=${anonymousUser} diff --git a/core/starter/src/main/java/org/apache/syncope/core/starter/SyncopeCoreApplication.java b/core/starter/src/main/java/org/apache/syncope/core/starter/SyncopeCoreApplication.java index e5f3c5e588..45c89a2960 100644 --- a/core/starter/src/main/java/org/apache/syncope/core/starter/SyncopeCoreApplication.java +++ b/core/starter/src/main/java/org/apache/syncope/core/starter/SyncopeCoreApplication.java @@ -42,10 +42,12 @@ import org.apache.syncope.core.provisioning.api.ConnIdBundleManager; import org.apache.syncope.core.provisioning.api.ConnectorManager; import org.apache.syncope.core.provisioning.api.ImplementationLookup; import org.apache.syncope.core.provisioning.api.data.ConnInstanceDataBinder; +import org.apache.syncope.core.provisioning.java.job.SyncopeTaskScheduler; import org.apache.syncope.core.starter.actuate.DefaultSyncopeCoreInfoContributor; import org.apache.syncope.core.starter.actuate.DomainsHealthIndicator; import org.apache.syncope.core.starter.actuate.EntityCacheEndpoint; import org.apache.syncope.core.starter.actuate.ExternalResourcesHealthIndicator; +import org.apache.syncope.core.starter.actuate.JobEndpoint; import org.apache.syncope.core.starter.actuate.SyncopeCoreInfoContributor; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.Qualifier; @@ -197,6 +199,12 @@ public class SyncopeCoreApplication extends SpringBootServletInitializer { return new EntityCacheEndpoint(entityCacheDAO); } + @ConditionalOnMissingBean + @Bean + public JobEndpoint jobEndpoint(final SyncopeTaskScheduler syncopeTaskScheduler) { + return new JobEndpoint(syncopeTaskScheduler); + } + @Bean public SyncopeStarterEventListener syncopeCoreEventListener( @Qualifier("syncopeCoreInfoContributor") diff --git a/core/starter/src/main/java/org/apache/syncope/core/starter/actuate/JobEndpoint.java b/core/starter/src/main/java/org/apache/syncope/core/starter/actuate/JobEndpoint.java new file mode 100644 index 0000000000..ba9508a104 --- /dev/null +++ b/core/starter/src/main/java/org/apache/syncope/core/starter/actuate/JobEndpoint.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.core.starter.actuate; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.apache.syncope.common.lib.types.JobAction; +import org.apache.syncope.core.provisioning.java.job.SyncopeTaskScheduler; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; + +@Endpoint(id = "job") +public class JobEndpoint { + + protected final SyncopeTaskScheduler syncopeTaskScheduler; + + public JobEndpoint(final SyncopeTaskScheduler syncopeTaskScheduler) { + this.syncopeTaskScheduler = syncopeTaskScheduler; + } + + @ReadOperation + public Map<String, Object> status() { + Map<String, Object> status = new HashMap<>(); + + syncopeTaskScheduler.getJobs().forEach((k, v) -> { + @SuppressWarnings("unchecked") + Map<String, Object> jobs = (Map<String, Object>) status.computeIfAbsent(k.domain(), d -> new HashMap<>()); + + Map<String, Object> job = new HashMap<>(); + jobs.put(k.job(), job); + + job.put("executor", v.job().getContext().getExecutor()); + job.put("dryRun", v.job().getContext().isDryRun()); + job.put("context", v.job().getContext().getData()); + + v.instant().ifPresent(f -> job.put("delay (seconds)", f.getDelay(TimeUnit.SECONDS))); + + v.cron().ifPresent(f -> { + job.put("next schedule (seconds)", f.getDelay(TimeUnit.SECONDS)); + job.put("done", f.isDone()); + job.put("cancelled", f.isCancelled()); + }); + }); + + return status; + } + + @WriteOperation + public void action( + final @Selector String domain, + final @Selector String jobName, + final @Selector JobAction action) { + + switch (action) { + case START -> + syncopeTaskScheduler.start(domain, jobName); + + case STOP -> + syncopeTaskScheduler.stop(domain, jobName); + + case DELETE -> + syncopeTaskScheduler.delete(domain, jobName); + + default -> { + } + } + } +} diff --git a/core/starter/src/main/resources/core.properties b/core/starter/src/main/resources/core.properties index 950bf0c65c..23d8724c3c 100644 --- a/core/starter/src/main/resources/core.properties +++ b/core/starter/src/main/resources/core.properties @@ -27,7 +27,7 @@ server.servlet.encoding.force=true server.servlet.contextPath=/syncope cxf.path=/rest -management.endpoints.web.exposure.include=health,info,loggers,entityCache +management.endpoints.web.exposure.include=health,info,loggers,entityCache,job management.endpoint.health.show-details=ALWAYS management.endpoint.env.show-values=WHEN_AUTHORIZED diff --git a/fit/core-reference/src/main/resources/core-embedded.properties b/fit/core-reference/src/main/resources/core-embedded.properties index d45dc0dac3..42c2f8c1df 100644 --- a/fit/core-reference/src/main/resources/core-embedded.properties +++ b/fit/core-reference/src/main/resources/core-embedded.properties @@ -16,7 +16,7 @@ # under the License. embedded.databases=syncope,syncopetwo,syncopetest -management.endpoints.web.exposure.include=health,info,beans,env,loggers,entityCache,metrics +management.endpoints.web.exposure.include=health,info,beans,env,loggers,entityCache,job,metrics keymaster.address=http://localhost:9080/syncope/rest/keymaster keymaster.username=${anonymousUser} diff --git a/src/main/asciidoc/reference-guide/concepts/routes.adoc b/src/main/asciidoc/reference-guide/concepts/routes.adoc index 8c632f93fe..f78cf80ec1 100644 --- a/src/main/asciidoc/reference-guide/concepts/routes.adoc +++ b/src/main/asciidoc/reference-guide/concepts/routes.adoc @@ -50,7 +50,7 @@ The received response, after being post-processed by matching route's _filters_, ==== Predicates Inside Route definition, each predicate will be referring to some Spring Cloud Gateway's -https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/request-predicates-factories.html[Predicate factory^]: +https://docs.spring.io/spring-cloud-gateway/reference/4.2/spring-cloud-gateway/request-predicates-factories.html[Predicate factory^]: * `AFTER` matches requests that happen after the specified datetime; * `BEFORE` matches requests that happen before the specified datetime; @@ -74,7 +74,7 @@ endif::[] ==== Filters Inside Route definition, each filter will be referring to some Spring Cloud Gateway's -https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/gatewayfilter-factories.html[Filter factory^]: +https://docs.spring.io/spring-cloud-gateway/reference/4.2/spring-cloud-gateway/gatewayfilter-factories.html[Filter factory^]: * `ADD_REQUEST_HEADER` adds a header to the downstream request's headers; * `ADD_REQUEST_PARAMETER` adds a parameter too the downstream request's query string; diff --git a/src/main/asciidoc/reference-guide/usage/actuator.adoc b/src/main/asciidoc/reference-guide/usage/actuator.adoc index c89a271d60..4e548172fb 100644 --- a/src/main/asciidoc/reference-guide/usage/actuator.adoc +++ b/src/main/asciidoc/reference-guide/usage/actuator.adoc @@ -45,6 +45,11 @@ a| Allows to work with https://openjpa.apache.org/builds/4.0.1/apache-openjpa/do * `GET` - shows JPA cache statistics * `POST {ENABLE,DISABLE,RESET}` - performs the requested operation onto JPA cache * `DELETE` - clears JPA cache's current content +| `job` +a| Allows to work with the various jobs defined after <<tasks>> and <<reports>>. + +* `GET` - shows the existing job data +* `POST {START,STOP,DELETE}` - performs the requested action on a given Job |=== @@ -80,6 +85,6 @@ a| * `DELETE {id}` - removes the session with given `id` | `gateway` -| https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/actuator-api.html[More details^] +| https://docs.spring.io/spring-cloud-gateway/reference/4.2/spring-cloud-gateway/actuator-api.html[More details^] |===
