This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/master by this push:
     new 637e68d115 [SYNCOPE-1947] Job actuator endpoint
637e68d115 is described below

commit 637e68d1150a3633add2fc92d04a2147a86104ec
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Tue Feb 3 10:43:29 2026 +0100

    [SYNCOPE-1947] Job actuator endpoint
---
 .../client/console/SyncopeConsoleApplication.java  |  3 +-
 .../syncope/client/console/widgets/JobWidget.java  |  7 +-
 .../client/enduser/SyncopeWebApplication.java      |  3 +-
 .../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 +-
 11 files changed, 117 insertions(+), 18 deletions(-)

diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleApplication.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleApplication.java
index a92b9227f4..d58f35f5e8 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleApplication.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleApplication.java
@@ -89,8 +89,7 @@ public class SyncopeConsoleApplication extends 
SpringBootServletInitializer {
             final Cache<String, OffsetDateTime> loggedoutSessionIdCache,
             @Qualifier(SyncopeWebApplication.DESTROYED_SESSIONID_CACHE)
             final Cache<String, OffsetDateTime> destroyedSessionIdCache,
-            final DynamicMenuStringResourceLoader 
dynamicMenuStringResourceLoader
-    ) {
+            final DynamicMenuStringResourceLoader 
dynamicMenuStringResourceLoader) {
 
         return new SyncopeWebApplication(
                 props,
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/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeWebApplication.java
 
b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeWebApplication.java
index 97dcf13ccb..9318e46496 100644
--- 
a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeWebApplication.java
+++ 
b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeWebApplication.java
@@ -101,8 +101,7 @@ public class SyncopeWebApplication extends 
WicketBootSecuredWebApplication imple
             final ClassPathScanImplementationLookup lookup,
             final ServiceOps serviceOps,
             final List<IResource> resources,
-            final DynamicMenuStringResourceLoader 
dynamicMenuStringResourceLoader
-    ) {
+            final DynamicMenuStringResourceLoader 
dynamicMenuStringResourceLoader) {
 
         this.resourceLoader = resourceLoader;
         this.props = props;
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 761e5b100c..22859312ce 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.3/spring-cloud-gateway-server-webflux/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.3/spring-cloud-gateway-server-webflux/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 8c00796806..de2414042c 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.3/spring-cloud-gateway-server-webflux/actuator-api.html[More
 details^]
 
 |===

Reply via email to