From 36f9cf88555457c895d3b77633cda189e5b8d03b Mon Sep 17 00:00:00 2001 From: Kate Alpert Date: Sat, 2 Mar 2024 17:35:55 -0700 Subject: [PATCH 01/44] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46ed9487..61430807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,11 @@ ## 3.4.3 [Released](https://bitbucket.org/xnatdev/container-service/src/3.4.3/). +* **Improvement** [XNAT-7903][] Support shared data at project level * **Bugfix** [CS-945][] Fix race condition on overlapping container status events [CS-945]: https://radiologics.atlassian.net/browse/CS-945 +[XNAT-7903]: https://radiologics.atlassian.net/browse/XNAT-7903 ## 3.4.2 [Released](https://bitbucket.org/xnatdev/container-service/src/3.4.2/). From e40bd6c72f181852b6ecc9b7ceac85d77855214c Mon Sep 17 00:00:00 2001 From: Automated Build Date: Sat, 2 Mar 2024 18:51:17 -0600 Subject: [PATCH 02/44] Update to version 3.4.3 and parent version 1.8.10 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 8dbf15a5..445eeebd 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - vXnat = "1.8.10-INTERNAL" + vXnat = "1.8.10" pluginAppName = "XNAT Container Service" } } @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.4.3-INTERNAL" +version "3.4.3" sourceCompatibility = 1.8 targetCompatibility = 1.8 From 02e2654819e615fcfdb8e5ee25cb411a5bdf386b Mon Sep 17 00:00:00 2001 From: John Flavin Date: Fri, 31 May 2024 10:19:38 -0500 Subject: [PATCH 03/44] CS-968 Add more detail to changelog about docker library switch --- CHANGELOG.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1dddca2..6dc3f6a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,27 @@ Not yet released. * **Improvement** [CS-946][] Prevent setting mutually distinct k8s PVC mounting options * **Bugfix** [CS-968][] Switch the docker API library we use from [docker-client][] to [docker-java][]. This should restore CS functionality on docker engine v25 and higher. + +### A Note About Our Docker Library +Originally we used the [spotify/docker-client][] library to wrap the docker remote API in java method calls. They stopped updating that and put out their final release [v6.1.1][] in 2016. + +We switched the Container Service to use a fork of that client, [dmandalidis/docker-client][] in CS version 3.0.0. Given that this was a fork of the client we already used, it was a simple drop-in replacement with no changes needed. + +But that library maintainer did continue to make changes. In 2023 they released a major version upgrade, [v7.0.0][], which dropped support for Java 8. That is the version of Java we use in XNAT (at time of writing) so this change meant we weren't able to update our version of this library. That was fine for a while... +...Until version 25 of the docker engine, in which they made an API change which caused an error in the version we used of `docker-client`. The library (presumably) fixed their issue but we weren't able to use that fix because our version of the library was frozen by their decision to drop Java 8 support. + +This forced us to switch our library from `docker-client` to [docker-java][]. This was not a drop-in replacement, and did require a migration. All the same docker API endpoints were supported in a 1:1 replacement—which took a little effort but was straightforward—except for one. The `docker-java` library did not support requesting `GenericResources` on a swarm service, which is the mechanism by which we allow commands to specify that they need a GPU. We opened a ticket reporting that lack of support (https://github.com/docker-java/docker-java/issues/2320), but at time of writing there has been no response. I created a fork (https://github.com/johnflavin/docker-java) and fixed the issue myself (https://github.com/docker-java/docker-java/pull/2327), but at time of writing that also has no response. I built a custom version of `docker-java` `3.4.0.1` and pushed that to the XNAT artifactory ([ext-release-local/com/github/docker-java][]). + +Long story short, as of CS version `3.5.0` we depend on `docker-java` version `3.4.0.1` for our docker (and swarm) API support. [CS-946]: https://radiologics.atlassian.net/browse/CS-946 [CS-968]: https://radiologics.atlassian.net/browse/CS-968 -[docker-client]: https://github.com/dmandalidis/docker-client +[spotify/docker-client]: https://github.com/spotify/docker-client +[v6.1.1]: https://github.com/spotify/docker-client/releases/tag/v6.1.1 +[dmandalidis/docker-client]: https://github.com/dmandalidis/docker-client +[v7.0.0]: https://github.com/dmandalidis/docker-client/tree/v7.0.0 [docker-java]: https://github.com/docker-java/docker-java +[ext-release-local/com/github/docker-java]: https://nrgxnat.jfrog.io/ui/repos/tree/General/ext-release-local/com/github/docker-java ## 3.4.3 [Released](https://bitbucket.org/xnatdev/container-service/src/3.4.3/). From e22f1ec35b857c564c75b80115708c37659b2cb7 Mon Sep 17 00:00:00 2001 From: John Flavin Date: Fri, 31 May 2024 10:21:05 -0500 Subject: [PATCH 04/44] Update version to 3.5.0-RC-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0fc081c8..55fd79c7 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.5.0-SNAPSHOT" +version "3.5.0-RC-SNAPSHOT" sourceCompatibility = 1.8 targetCompatibility = 1.8 From a700f3b0e6374baa15582c1eb6c46768e20cb18a Mon Sep 17 00:00:00 2001 From: Will Horton Date: Mon, 19 Aug 2024 12:13:28 -0500 Subject: [PATCH 05/44] CS-966 Store k8s node id on updates --- .../api/KubernetesInformerImpl.java | 10 ++- .../model/KubernetesStatusChangeEvent.java | 86 +++---------------- .../services/impl/ContainerServiceImpl.java | 21 +++-- 3 files changed, 34 insertions(+), 83 deletions(-) diff --git a/src/main/java/org/nrg/containers/api/KubernetesInformerImpl.java b/src/main/java/org/nrg/containers/api/KubernetesInformerImpl.java index 5672a181..260a2473 100644 --- a/src/main/java/org/nrg/containers/api/KubernetesInformerImpl.java +++ b/src/main/java/org/nrg/containers/api/KubernetesInformerImpl.java @@ -18,6 +18,7 @@ import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.openapi.models.V1Pod; import io.kubernetes.client.openapi.models.V1PodList; +import io.kubernetes.client.openapi.models.V1PodSpec; import io.kubernetes.client.openapi.models.V1PodStatus; import io.kubernetes.client.util.CallGeneratorParams; import lombok.extern.slf4j.Slf4j; @@ -223,6 +224,10 @@ private static KubernetesStatusChangeEvent createEvent(V1Pod pod) { timestamp = null; } + // Node + final V1PodSpec podSpec = pod.getSpec(); + final String nodeName = podSpec == null ? null : podSpec.getNodeName(); + return new KubernetesStatusChangeEvent( jobNameFromPodLabels(pod), objName(pod), @@ -232,7 +237,8 @@ private static KubernetesStatusChangeEvent createEvent(V1Pod pod) { containerState, containerStateReason, exitCode, - timestamp + timestamp, + nodeName ); } @@ -276,7 +282,7 @@ public void onUpdate(V1Pod oldObj, V1Pod newObj) { return; } - log.debug("Pod {} updated", newEvent.podName()); + log.debug("Pod {} updated", newEvent.getPodName()); triggerEvent(newEvent); } diff --git a/src/main/java/org/nrg/containers/events/model/KubernetesStatusChangeEvent.java b/src/main/java/org/nrg/containers/events/model/KubernetesStatusChangeEvent.java index bbf31a4c..b60fe637 100644 --- a/src/main/java/org/nrg/containers/events/model/KubernetesStatusChangeEvent.java +++ b/src/main/java/org/nrg/containers/events/model/KubernetesStatusChangeEvent.java @@ -1,42 +1,24 @@ package org.nrg.containers.events.model; +import lombok.Value; import org.nrg.containers.model.kubernetes.KubernetesPodPhase; import java.time.OffsetDateTime; import java.util.Collections; import java.util.Map; -import java.util.Objects; +@Value public class KubernetesStatusChangeEvent implements ContainerEvent { - private final String jobName; - private final String podName; - private final String containerId; - private final KubernetesPodPhase podPhase; - private final String podPhaseReason; - private final KubernetesContainerState containerState; - private final String containerStateReason; - private final Integer exitCode; - private final OffsetDateTime timestamp; - - public KubernetesStatusChangeEvent(String jobName, - String podName, - String containerId, - KubernetesPodPhase podPhase, - String podPhaseReason, - KubernetesContainerState containerState, - String containerStateReason, - Integer exitCode, - OffsetDateTime timestamp) { - this.jobName = jobName; - this.podName = podName; - this.containerId = containerId; - this.podPhase = podPhase; - this.podPhaseReason = podPhaseReason; - this.containerState = containerState; - this.containerStateReason = containerStateReason; - this.exitCode = exitCode; - this.timestamp = timestamp; - } + String jobName; + String podName; + String containerId; + KubernetesPodPhase podPhase; + String podPhaseReason; + KubernetesContainerState containerState; + String containerStateReason; + Integer exitCode; + OffsetDateTime timestamp; + String nodeId; @Override public String backendId() { @@ -82,48 +64,4 @@ public String details() { } return details; } - - public String podName() { - return podName; - } - - public String containerId() { - return containerId; - } - - @Override - public String toString() { - return "KubernetesStatusChangeEvent{" + - "jobName=\"" + jobName + "\"" + - ", podName=\"" + podName + "\"" + - ", containerId=\"" + containerId + "\"" + - ", podPhase=\"" + podPhase + "\"" + - ", podPhaseReason=\"" + podPhaseReason + "\"" + - ", containerState=\"" + containerState + "\"" + - ", containerStateReason=\"" + containerStateReason + "\"" + - ", exitCode=\"" + exitCode + "\"" + - ", timestamp=\"" + timestamp + "\"" + - "}"; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - KubernetesStatusChangeEvent that = (KubernetesStatusChangeEvent) o; - return Objects.equals(jobName, that.jobName) && - Objects.equals(podName, that.podName) && - Objects.equals(containerId, that.containerId) && - podPhase == that.podPhase && - Objects.equals(podPhaseReason, that.podPhaseReason) && - containerState == that.containerState && - Objects.equals(containerStateReason, that.containerStateReason) && - Objects.equals(exitCode, that.exitCode) && - Objects.equals(timestamp, that.timestamp); - } - - @Override - public int hashCode() { - return Objects.hash(jobName, podName, containerId, podPhase, podPhaseReason, containerState, containerStateReason, exitCode, timestamp); - } } diff --git a/src/main/java/org/nrg/containers/services/impl/ContainerServiceImpl.java b/src/main/java/org/nrg/containers/services/impl/ContainerServiceImpl.java index f32ed66c..68c2c122 100644 --- a/src/main/java/org/nrg/containers/services/impl/ContainerServiceImpl.java +++ b/src/main/java/org/nrg/containers/services/impl/ContainerServiceImpl.java @@ -861,22 +861,29 @@ public void processEvent(final ContainerEvent event) { KubernetesStatusChangeEvent kEvent = ((KubernetesStatusChangeEvent) event); // Check if we need to set additional ids - boolean shouldUpdatePodName = containerWithAddedEvent.podName() == null && kEvent.podName() != null; - boolean shouldUpdateContainerId = containerWithAddedEvent.containerId() == null && kEvent.containerId() != null; - if (shouldUpdatePodName || shouldUpdateContainerId) { + boolean shouldUpdateNode = containerWithAddedEvent.nodeId() == null && kEvent.getNodeId() != null; + boolean shouldUpdatePodName = containerWithAddedEvent.podName() == null && kEvent.getPodName() != null; + boolean shouldUpdateContainerId = containerWithAddedEvent.containerId() == null && kEvent.getContainerId() != null; + if (shouldUpdateNode || shouldUpdatePodName || shouldUpdateContainerId) { Container.Builder builder = containerWithAddedEvent.toBuilder(); + if (shouldUpdateNode) { + log.debug("Container {} for job {}: setting nodeId to node {}", + container.databaseId(), container.jobName(), kEvent.getNodeId() + ); + builder.nodeId(kEvent.getNodeId()); + } if (shouldUpdatePodName) { log.debug("Container {} for job {}: setting taskId to pod name {}", - container.databaseId(), container.jobName(), kEvent.podName() + container.databaseId(), container.jobName(), kEvent.getPodName() ); - builder.taskId(kEvent.podName()); + builder.taskId(kEvent.getPodName()); } if (shouldUpdateContainerId) { log.debug("Container {} for job {}: setting containerId to container id {}", - container.databaseId(), container.jobName(), kEvent.containerId() + container.databaseId(), container.jobName(), kEvent.getContainerId() ); - builder.containerId(kEvent.containerId()); + builder.containerId(kEvent.getContainerId()); } containerEntityService.update(fromPojo(builder.build())); containerWithAddedEvent = retrieve(container.databaseId()); From fe0ecf368dee22ea1e4cc938481cc0ae8ec1e0bf Mon Sep 17 00:00:00 2001 From: Will Horton Date: Tue, 20 Aug 2024 16:26:22 -0500 Subject: [PATCH 06/44] Update changelog and build version for 3.5.0 --- CHANGELOG.md | 5 ++++- build.gradle | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc3f6a7..93932e6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # Changelog ## 3.5.0 -Not yet released. +[Released](https://bitbucket.org/xnatdev/container-service/src/3.5.0/). * **Improvement** [CS-946][] Prevent setting mutually distinct k8s PVC mounting options +* **Bugfix** [CS-966][] Ensure tracking of container IDs in workflow tables in a Kubernetes environment * **Bugfix** [CS-968][] Switch the docker API library we use from [docker-client][] to [docker-java][]. This should restore CS functionality on docker engine v25 and higher. @@ -20,7 +21,9 @@ This forced us to switch our library from `docker-client` to [docker-java][]. Th Long story short, as of CS version `3.5.0` we depend on `docker-java` version `3.4.0.1` for our docker (and swarm) API support. [CS-946]: https://radiologics.atlassian.net/browse/CS-946 +[CS-966]: https://radiologics.atlassian.net/browse/CS-966 [CS-968]: https://radiologics.atlassian.net/browse/CS-968 +[docker-client]: https://github.com/spotify/docker-client [spotify/docker-client]: https://github.com/spotify/docker-client [v6.1.1]: https://github.com/spotify/docker-client/releases/tag/v6.1.1 [dmandalidis/docker-client]: https://github.com/dmandalidis/docker-client diff --git a/build.gradle b/build.gradle index 55fd79c7..2e28ddb6 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.5.0-RC-SNAPSHOT" +version "3.5.0" sourceCompatibility = 1.8 targetCompatibility = 1.8 From 38155fdadb624475816b595ec97278d13c76a8ca Mon Sep 17 00:00:00 2001 From: Automated Build Date: Mon, 9 Sep 2024 13:41:12 -0500 Subject: [PATCH 07/44] Update to version 3.6.0-RC-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 228bf178..16d2a231 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.6.0-SNAPSHOT" +version "3.6.0-RC-SNAPSHOT" sourceCompatibility = 1.8 targetCompatibility = 1.8 From 027b0fbcaadb2e97344199031bdb9dae2473169f Mon Sep 17 00:00:00 2001 From: Automated Build Date: Wed, 25 Sep 2024 15:45:28 -0500 Subject: [PATCH 08/44] Update to version 3.6.0 and parent version 1.9.0 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 16d2a231..851a98f4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - vXnat = "1.9.0-SNAPSHOT" + vXnat = "1.9.0" pluginAppName = "XNAT Container Service" } } @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.6.0-RC-SNAPSHOT" +version "3.6.0" sourceCompatibility = 1.8 targetCompatibility = 1.8 From 55014c3f0f65618f2f16986b32c34deb4b7b4a76 Mon Sep 17 00:00:00 2001 From: Will Horton Date: Thu, 26 Sep 2024 15:51:11 +0000 Subject: [PATCH 09/44] Update changelog for CS 3.6.0 release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42c7da01..d1c5c84c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 3.6.0 +[Released](https://bitbucket.org/xnatdev/container-service/src/3.6.0/) + +* **Improvement** [XNAT-7990](https://radiologics.atlassian.net/browse/XNAT-7990) Update Hibernate and Spring dependencies for compatibility with XNAT 1.9.0 +* **Improvement** [XNAT-7991](https://radiologics.atlassian.net/browse/XNAT-7991) Replace calls to deprecated commons-httpclient dependency +* **Improvement** [CS-943](https://radiologics.atlassian.net/browse/CS-943) Add filtering to lists of installed commands in site-wide and project settings +* Also improved unit tests for automated validation + ## 3.5.0 [Released](https://bitbucket.org/xnatdev/container-service/src/3.5.0/). From f8fc2279b041c09199f56adb98fececcfb32636b Mon Sep 17 00:00:00 2001 From: Automated Build Date: Tue, 19 Nov 2024 15:20:13 -0600 Subject: [PATCH 10/44] Update to version 3.6.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ac5c4810..597b545c 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.6.1-RC-SNAPSHOT" +version "3.6.1" java { sourceCompatibility = JavaVersion.VERSION_1_8 From 9cd11dfca916a3cd57421a4a7c88d69ad3b8be6f Mon Sep 17 00:00:00 2001 From: Rick Herrick Date: Thu, 5 Dec 2024 10:40:10 -0600 Subject: [PATCH 11/44] Update to version 3.6.2-RC-SNAPSHOT --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index d26ed9c7..afb3519c 100644 --- a/build.gradle +++ b/build.gradle @@ -9,14 +9,14 @@ plugins { id "idea" id "java" id "maven-publish" - id "com.palantir.git-version" version "0.12.1" - id "io.spring.dependency-management" version "1.0.9.RELEASE" - id "io.franzbecker.gradle-lombok" version "4.0.0" + id "com.palantir.git-version" version "3.1.0" + id "io.spring.dependency-management" version "1.1.6" + id "io.franzbecker.gradle-lombok" version "5.0.0" id "net.linguica.maven-settings" version "0.5" } group "org.nrg.xnatx.plugins" -version "3.7.0-SNAPSHOT" +version "3.6.2-RC-SNAPSHOT" java { sourceCompatibility = JavaVersion.VERSION_1_8 From 7f83e57178ca50ca8709b9971e14049c9d597f86 Mon Sep 17 00:00:00 2001 From: John Flavin Date: Fri, 31 May 2024 10:19:38 -0500 Subject: [PATCH 12/44] CS-968 Add more detail to changelog about docker library switch --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ee5ab15..a9f5046e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,6 @@ Long story short, as of CS version `3.5.0` we depend on `docker-java` version `3 [CS-946]: https://radiologics.atlassian.net/browse/CS-946 [CS-966]: https://radiologics.atlassian.net/browse/CS-966 [CS-968]: https://radiologics.atlassian.net/browse/CS-968 -[docker-client]: https://github.com/spotify/docker-client [spotify/docker-client]: https://github.com/spotify/docker-client [v6.1.1]: https://github.com/spotify/docker-client/releases/tag/v6.1.1 [dmandalidis/docker-client]: https://github.com/dmandalidis/docker-client From ba4d26ead5f926eaaae3168e37ba3567305f6def Mon Sep 17 00:00:00 2001 From: Will Horton Date: Tue, 20 Aug 2024 16:26:22 -0500 Subject: [PATCH 13/44] Update changelog and build version for 3.5.0 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9f5046e..3ee5ab15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Long story short, as of CS version `3.5.0` we depend on `docker-java` version `3 [CS-946]: https://radiologics.atlassian.net/browse/CS-946 [CS-966]: https://radiologics.atlassian.net/browse/CS-966 [CS-968]: https://radiologics.atlassian.net/browse/CS-968 +[docker-client]: https://github.com/spotify/docker-client [spotify/docker-client]: https://github.com/spotify/docker-client [v6.1.1]: https://github.com/spotify/docker-client/releases/tag/v6.1.1 [dmandalidis/docker-client]: https://github.com/dmandalidis/docker-client From 20488212777182fbab59299ce6e368e927c8933d Mon Sep 17 00:00:00 2001 From: Rick Herrick Date: Fri, 6 Dec 2024 13:02:59 -0600 Subject: [PATCH 14/44] CS-975 Merged fixes/cs-975-misqueue from dev --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ee5ab15..f46f84de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.6.2 +[Released](https://bitbucket.org/xnatdev/container-service/src/3.6.2/) +* **Bugfix** [CS-975](https://radiologics.atlassian.net/browse/CS-975) Queued messages failed on ActiveMQ due to improper configuration of class serialization + ## 3.6.1 [Released](https://bitbucket.org/xnatdev/container-service/src/3.6.1/) * **Improvement** [CS-975](https://radiologics.atlassian.net/browse/CS-975)/[XNAT-8111](https://radiologics.atlassian.net/browse/XNAT-8111) Convert event handlers in CS to use JMS instead of executors From 05ce5b10e69093d7bacf08d8af876ffaf8088a69 Mon Sep 17 00:00:00 2001 From: Automated Build Date: Mon, 9 Dec 2024 16:31:56 -0600 Subject: [PATCH 15/44] Update to version 3.6.2 and parent version 1.9.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index afb3519c..1ccf4375 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.6.2-RC-SNAPSHOT" +version "3.6.2" java { sourceCompatibility = JavaVersion.VERSION_1_8 From d05c3cf873a19f7afb830a5930c172ebf618c363 Mon Sep 17 00:00:00 2001 From: Tim Olsen Date: Fri, 14 Mar 2025 19:51:12 -0500 Subject: [PATCH 16/44] Update version to 3.6.3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1ccf4375..6873c7b5 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.6.2" +version "3.6.3-SNAPSHOT" java { sourceCompatibility = JavaVersion.VERSION_1_8 From 915158ce553e20cbe2ffc77e8b87833453c0cf14 Mon Sep 17 00:00:00 2001 From: Kate Alpert Date: Mon, 3 Mar 2025 19:55:29 +0000 Subject: [PATCH 17/44] CS-989 properly track submitting finalization jobs to the active mq (pull request #135) --- .../services/impl/ContainerServiceImpl.java | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/nrg/containers/services/impl/ContainerServiceImpl.java b/src/main/java/org/nrg/containers/services/impl/ContainerServiceImpl.java index 03a88a00..bbbf033e 100644 --- a/src/main/java/org/nrg/containers/services/impl/ContainerServiceImpl.java +++ b/src/main/java/org/nrg/containers/services/impl/ContainerServiceImpl.java @@ -1129,19 +1129,26 @@ public void queueFinalize(final String exitCodeString, final boolean isSuccessfu private boolean canFinalize(UserI user, Container containerOrService, ContainerFinalizingRequest request) { Integer limit = containerControlApi.getFinalizingThrottle(); if (limit == null) { + markAsQueuedForFinalizing(containerOrService, request, user); return true; } - synchronized (this) { - List wfs = getContainerWorkflowsByStatuses(Arrays.asList(FINALIZING, _WAITING), user); - boolean canFinalize = wfs == null || wfs.size() < limit; - if (canFinalize) { - addContainerHistoryItem(containerOrService, ContainerHistory.fromSystem( - request.makeJMSQueuedStatus(containerOrService.status()), "Queued for finalizing"), - user); - } - return canFinalize; + // To be perfectly accurate, this should be synchronized, but that's a massive performance bottleneck. + // A slightly-overlapping check is probably good enough. + List wfs = getContainerWorkflowsByStatuses(Arrays.asList(FINALIZING, _WAITING), user); + boolean canFinalize = wfs == null || wfs.size() < limit; + if (canFinalize) { + markAsQueuedForFinalizing(containerOrService, request, user); } + return canFinalize; + } + + private void markAsQueuedForFinalizing(final Container containerOrService, + final ContainerRequest request, + final UserI user) { + addContainerHistoryItem(containerOrService, ContainerHistory.fromSystem( + request.makeJMSQueuedStatus(containerOrService.status()), "Queued for finalizing"), + user); } private void recoverFromQueueingFailureFinalizing(Exception e, From c1b736aa3456e2748414facfb1be3df36ab00c63 Mon Sep 17 00:00:00 2001 From: "Will Horton (XW)" Date: Mon, 24 Mar 2025 19:04:31 +0000 Subject: [PATCH 18/44] Merged in rc/3.6.3-edits (pull request #136) Amend Changelog for 3.6.3 release * Amend Changelog for 3.6.3 release Approved-by: Rick Herrick --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f46f84de..e0c0531a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.6.3 +[Released](https://bitbucket.org/xnatdev/container-service/src/3.6.3/) +* **Bugfix** [CS-989](https://radiologics.atlassian.net/browse/CS-989) Addresses container finalization errors on very large batch runs due to lack of status tracking in the finalization queue + ## 3.6.2 [Released](https://bitbucket.org/xnatdev/container-service/src/3.6.2/) * **Bugfix** [CS-975](https://radiologics.atlassian.net/browse/CS-975) Queued messages failed on ActiveMQ due to improper configuration of class serialization From cc3ae0348eb57d35977ca07f7cafe1fcb5297c4c Mon Sep 17 00:00:00 2001 From: Bin Zhang Date: Wed, 26 Mar 2025 14:25:08 -0500 Subject: [PATCH 19/44] fix version number --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6873c7b5..cf2d8d34 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.6.3-SNAPSHOT" +version "3.6.3-RC-SNAPSHOT" java { sourceCompatibility = JavaVersion.VERSION_1_8 From c9803b18bd97bc22c8447617c5b92afc71511342 Mon Sep 17 00:00:00 2001 From: Automated Build Date: Wed, 26 Mar 2025 14:31:07 -0500 Subject: [PATCH 20/44] Update to version 3.6.3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cf2d8d34..bc7f7875 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.6.3-RC-SNAPSHOT" +version "3.6.3" java { sourceCompatibility = JavaVersion.VERSION_1_8 From 00fb1bc05154354cebe8df1569f2d75ee914241d Mon Sep 17 00:00:00 2001 From: Automated Build Date: Wed, 11 Jun 2025 14:04:21 -0500 Subject: [PATCH 21/44] Update to version 3.7.0-RC-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2f80574a..f5aab181 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.7.0-SNAPSHOT" +version "3.7.0-RC-SNAPSHOT" java { sourceCompatibility = JavaVersion.VERSION_1_8 From 4e3c23433ffb4660ec9b7be4d62725c49565bd12 Mon Sep 17 00:00:00 2001 From: Automated Build Date: Fri, 13 Jun 2025 16:10:35 -0500 Subject: [PATCH 22/44] Update to version 3.7.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f5aab181..5921d2e9 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.7.0-RC-SNAPSHOT" +version "3.7.0" java { sourceCompatibility = JavaVersion.VERSION_1_8 From 32c2480e294b883f7ee6f9b95e4c1dcb74abba57 Mon Sep 17 00:00:00 2001 From: Rick Herrick Date: Thu, 26 Jun 2025 16:32:38 -0500 Subject: [PATCH 23/44] Updated to 1.9.2 release and version 3.7.1-RC-SNAPSHOT --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 5921d2e9..06bca7fb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - vXnat = "1.9.2-RC-SNAPSHOT" + vXnat = "1.9.2" pluginAppName = "XNAT Container Service" } } @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.7.0" +version "3.7.1-RC-SNAPSHOT" java { sourceCompatibility = JavaVersion.VERSION_1_8 From f1c06e23100952c1cf43e3dfd9bcf09f41555411 Mon Sep 17 00:00:00 2001 From: Rick Herrick Date: Fri, 27 Jun 2025 14:50:35 +0000 Subject: [PATCH 24/44] Merged in fixes/cs-1036 (pull request #159) CS-1036 Added label processing to convert invalid chars to '-', truncate strings longer than 63 chars * CS-1036 Added label processing to convert invalid chars to '-', truncate strings longer than 63 chars * CS-1036 Added logging of changed label values Approved-by: Ian Gauthier Approved-by: Bin Zhang --- .../containers/api/KubernetesClientImpl.java | 223 ++++++++++-------- 1 file changed, 127 insertions(+), 96 deletions(-) diff --git a/src/main/java/org/nrg/containers/api/KubernetesClientImpl.java b/src/main/java/org/nrg/containers/api/KubernetesClientImpl.java index 4a0f7fa9..8a3beef6 100644 --- a/src/main/java/org/nrg/containers/api/KubernetesClientImpl.java +++ b/src/main/java/org/nrg/containers/api/KubernetesClientImpl.java @@ -49,49 +49,55 @@ import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.ExecutorService; +import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import static io.kubernetes.client.util.Config.SERVICEACCOUNT_CA_PATH; + import org.springframework.http.HttpStatus; + import static org.nrg.containers.utils.ContainerUtils.SECONDS_PER_WEEK; @Slf4j public class KubernetesClientImpl implements KubernetesClient { // Format: (label)(comparator)(value) // comparator can be != or == - private static final String LABEL_CAPTURE_GROUP = "label"; - private static final String COMPARATOR_CAPTURE_GROUP = "comparator"; - private static final String VALUE_CAPTURE_GROUP = "value"; - public static final Pattern SWARM_CONSTRAINT_PATTERN = Pattern.compile("(node\\.labels\\.)?(?<" + LABEL_CAPTURE_GROUP +">.+?)(?<" + COMPARATOR_CAPTURE_GROUP + ">!=|==)(?<" + VALUE_CAPTURE_GROUP +">.+?)"); + private static final String LABEL_CAPTURE_GROUP = "label"; + private static final String COMPARATOR_CAPTURE_GROUP = "comparator"; + private static final String VALUE_CAPTURE_GROUP = "value"; + private static final Predicate NOT_K8S_LABEL_PATTERN = Pattern.compile("^$|^[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$").asPredicate().negate(); + private static final String K8S_REPLACE_PATTERN = "[^A-Za-z0-9\\-.]"; + + public static final Pattern SWARM_CONSTRAINT_PATTERN = Pattern.compile("(node\\.labels\\.)?(?<" + LABEL_CAPTURE_GROUP + ">.+?)(?<" + COMPARATOR_CAPTURE_GROUP + ">!=|==)(?<" + VALUE_CAPTURE_GROUP + ">.+?)"); - public static final int JOB_TIME_TO_LIVE_WEEKS = 1; // Clean up lingering jobs + pods + logs after one week - public static final int JOB_TIME_TO_LIVE_SECS = JOB_TIME_TO_LIVE_WEEKS * SECONDS_PER_WEEK; - public static final int JOB_BACKOFF_LIMIT = 0; // No retries - public static final String POD_RESTART_POLICY = "Never"; + public static final int JOB_TIME_TO_LIVE_WEEKS = 1; // Clean up lingering jobs + pods + logs after one week + public static final int JOB_TIME_TO_LIVE_SECS = JOB_TIME_TO_LIVE_WEEKS * SECONDS_PER_WEEK; + public static final int JOB_BACKOFF_LIMIT = 0; // No retries + public static final String POD_RESTART_POLICY = "Never"; public static final String CONTAINER_CREATING = "ContainerCreating"; - private final ExecutorService executorService; + private final ExecutorService executorService; private final NrgEventServiceI eventService; - private ApiClient apiClient; - private CoreV1Api coreApi; - private BatchV1Api batchApi; - private String namespace; + private ApiClient apiClient; + private CoreV1Api coreApi; + private BatchV1Api batchApi; + private String namespace; private KubernetesInformer kubernetesInformer = null; public KubernetesClientImpl( final ExecutorService executorService, final NrgEventServiceI eventService - ) throws IOException, NoContainerServerException { + ) throws IOException, NoContainerServerException { this.executorService = executorService; - this.eventService = eventService; + this.eventService = eventService; - ApiClient apiClient = null; - String namespace = null; + ApiClient apiClient = null; + String namespace = null; final KubeConfig kubeConfig = KubernetesConfiguration.loadStandard(true); if (kubeConfig != null) { apiClient = ClientBuilder.kubeconfig(kubeConfig).build(); @@ -120,10 +126,10 @@ public KubernetesClientImpl( loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); apiClient.setHttpClient( apiClient.getHttpClient() - .newBuilder() - .addInterceptor(loggingInterceptor) - .build() - ); + .newBuilder() + .addInterceptor(loggingInterceptor) + .build() + ); } setBackendClient(apiClient); @@ -139,8 +145,8 @@ public ApiClient getBackendClient() { @Override public void setBackendClient(ApiClient apiClient) { this.apiClient = apiClient; - coreApi = new CoreV1Api(apiClient); - batchApi = new BatchV1Api(apiClient); + coreApi = new CoreV1Api(apiClient); + batchApi = new BatchV1Api(apiClient); restart(); } @@ -260,8 +266,8 @@ public String createJob(final Container toCreate, final DockerControlApi.NumRepl // Labels final Map labels = toCreate.containerLabels() == null ? - new HashMap<>() : - new HashMap<>(toCreate.containerLabels()); + new HashMap<>() : + new HashMap<>(toCreate.containerLabels()); // TODO add helpful labels. Things like command and wrapper IDs, maybe workflow id if we have one, etc. // Or maybe those could be annotations? @@ -274,24 +280,24 @@ public String createJob(final Container toCreate, final DockerControlApi.NumRepl // Mounts final List ourMounts = toCreate.mounts(); - final List mounts = new ArrayList<>(); - final List volumes = new ArrayList<>(); + final List mounts = new ArrayList<>(); + final List volumes = new ArrayList<>(); if (ourMounts != null && !ourMounts.isEmpty()) { if (StringUtils.isNotBlank(ourMounts.get(0).mountPvcName())) { Set volumesSet = new HashSet<>(); for (final Container.ContainerMount mount : ourMounts) { volumesSet.add(new V1VolumeBuilder() - .withName(mount.mountPvcName()) - .withNewPersistentVolumeClaim() - .withClaimName(mount.mountPvcName()) - .endPersistentVolumeClaim() - .build()); + .withName(mount.mountPvcName()) + .withNewPersistentVolumeClaim() + .withClaimName(mount.mountPvcName()) + .endPersistentVolumeClaim() + .build()); mounts.add(new V1VolumeMountBuilder() - .withName(mount.mountPvcName()) - .withMountPath(mount.containerPath()) - .withSubPath(mount.containerHostPath()) - .withReadOnly(!mount.writable()) - .build()); + .withName(mount.mountPvcName()) + .withMountPath(mount.containerPath()) + .withSubPath(mount.containerHostPath()) + .withReadOnly(!mount.writable()) + .build()); } volumes.addAll(volumesSet); } else { @@ -301,16 +307,16 @@ public String createJob(final Container toCreate, final DockerControlApi.NumRepl // We assume that all mounts we make are already available on all worker nodes, // which implies we can mount the files we want at a host path. volumes.add(new V1VolumeBuilder() - .withName(mount.name()) - .withNewHostPath() - .withPath(mount.xnatHostPath()) - .endHostPath() - .build()); + .withName(mount.name()) + .withNewHostPath() + .withPath(mount.xnatHostPath()) + .endHostPath() + .build()); mounts.add(new V1VolumeMountBuilder() - .withName(mount.name()) - .withMountPath(mount.containerPath()) - .withReadOnly(!mount.writable()) - .build()); + .withName(mount.name()) + .withMountPath(mount.containerPath()) + .withReadOnly(!mount.writable()) + .build()); } } } @@ -321,18 +327,18 @@ public String createJob(final Container toCreate, final DockerControlApi.NumRepl if (shmSize != null && shmSize > 0) { final String shmVolName = "shm"; volumes.add(new V1VolumeBuilder() - .withName(shmVolName) - .withNewEmptyDir() - .withMedium("Memory") - .withNewSizeLimit(String.valueOf(shmSize)) - .endEmptyDir() - .build() - ); + .withName(shmVolName) + .withNewEmptyDir() + .withMedium("Memory") + .withNewSizeLimit(String.valueOf(shmSize)) + .endEmptyDir() + .build() + ); mounts.add(new V1VolumeMountBuilder() - .withMountPath("/dev/shm") - .withName(shmVolName) - .build() - ); + .withMountPath("/dev/shm") + .withName(shmVolName) + .build() + ); } // Command line and args @@ -341,10 +347,10 @@ public String createJob(final Container toCreate, final DockerControlApi.NumRepl if (toCreate.overrideEntrypointNonnull()) { // Run their command line as a script through a shell command = Arrays.asList("/bin/sh", "-c"); - args = Collections.singletonList(toCreate.commandLine()); + args = Collections.singletonList(toCreate.commandLine()); } else { // Run their command line as args (tokenized and split) through the image's entrypoint - args = ShellSplitter.shellSplit(toCreate.commandLine()); + args = ShellSplitter.shellSplit(toCreate.commandLine()); command = null; } @@ -365,11 +371,11 @@ public String createJob(final Container toCreate, final DockerControlApi.NumRepl if (toCreate.limitCpu() != null) { resourceRequirementsBuilder.addToLimits("cpu", new Quantity(String.valueOf(toCreate.limitCpu()))); } - if (toCreate.genericResources()!=null) { + if (toCreate.genericResources() != null) { final String gpuNumber = toCreate.genericResources().get("gpu"); if (StringUtils.isNotEmpty(gpuNumber)) { if (StringUtils.isNotEmpty(gpuVendor)) { - resourceRequirementsBuilder.addToLimits(gpuVendor.toLowerCase() + ".com/gpu", new Quantity(String.valueOf(gpuNumber))); + resourceRequirementsBuilder.addToLimits(gpuVendor.toLowerCase() + ".com/gpu", new Quantity(gpuNumber)); } else { log.error("When the value of the GPU vendor in the Kubernetes cluster is empty or null, the GPU resource cannot be requested."); throw new ContainerException("When the value of the GPU vendor in the Kubernetes cluster is empty or null, the GPU resource cannot be requested."); @@ -397,15 +403,36 @@ public String createJob(final Container toCreate, final DockerControlApi.NumRepl // Environment variables final List envVars = containerPropertiesWithSecretValues.environmentVariables() - .entrySet().stream() - .map(entry -> new V1EnvVarBuilder().withName(entry.getKey()).withValue(entry.getValue()).build()) - .collect(Collectors.toList()); - + .entrySet().stream() + .map(entry -> new V1EnvVarBuilder().withName(entry.getKey()).withValue(entry.getValue()).build()) + .collect(Collectors.toList()); + +final Map cleanedLabels = labels.entrySet().stream() + .peek(entry -> { + String value = entry.getValue(); + boolean cleaned = false; + // Kubernetes labels can only contain A-z, 0-9, -, and . characters, so if we find anything else... + if (NOT_K8S_LABEL_PATTERN.test(value)) { + // ...replace it with a dash. + value = value.replaceAll(K8S_REPLACE_PATTERN, "-"); + cleaned = true; + } + if (value.length() > 63) { + // ...and if the value is too long, truncate it. + value = value.substring(0, 60) + "..."; + cleaned = true; + } + if (cleaned) { + log.info("Label {} for container {} was cleaned to {}", entry.getKey(), name, value); + entry.setValue(value); + } + }) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); // Build job V1Job job = new V1JobBuilder() .withNewMetadata() .withGenerateName(name) // Container name used as job name prefix - .withLabels(labels) + .withLabels(cleanedLabels) .endMetadata() .withNewSpec() .withTtlSecondsAfterFinished(JOB_TIME_TO_LIVE_SECS) @@ -413,7 +440,7 @@ public String createJob(final Container toCreate, final DockerControlApi.NumRepl .withSuspend(suspend) .withNewTemplate() .withNewMetadata() - .withLabels(labels) + .withLabels(cleanedLabels) .endMetadata() .withNewSpec() .withAffinity(affinity) @@ -440,24 +467,24 @@ public String createJob(final Container toCreate, final DockerControlApi.NumRepl log.trace("Creating kubernetes job: {}", job); } else if (log.isDebugEnabled()) { log.debug("Creating kubernetes job:" + - "\n\timage {}" + - "\n\tcommand \"{}\"" + - "\n\tworking directory \"{}\"" + - "\n\tcontainerUser \"{}\"" + - "\n\tvolumes [{}]" + - "\n\texposed ports: {}", - toCreate.dockerImage(), - toCreate.commandLine(), - toCreate.workingDirectory(), - containerUserId, - StringUtils.join(toCreate.bindMountStrings(), ", "), - StringUtils.join(toCreate.portStrings(), ", ")); + "\n\timage {}" + + "\n\tcommand \"{}\"" + + "\n\tworking directory \"{}\"" + + "\n\tcontainerUser \"{}\"" + + "\n\tvolumes [{}]" + + "\n\texposed ports: {}", + toCreate.dockerImage(), + toCreate.commandLine(), + toCreate.workingDirectory(), + containerUserId, + StringUtils.join(toCreate.bindMountStrings(), ", "), + StringUtils.join(toCreate.portStrings(), ", ")); } try { job = batchApi.createNamespacedJob(namespace, job, null, null, null, null); - final V1ObjectMeta meta = job.getMetadata(); - final String jobName = meta == null ? null : meta.getName(); + final V1ObjectMeta meta = job.getMetadata(); + final String jobName = meta == null ? null : meta.getName(); log.debug("Created job {}", jobName); return jobName; } catch (ApiException e) { @@ -508,17 +535,17 @@ public static V1Affinity parseSwarmConstraints(List constraints) throws // Group the constraints by unique key, operator pairs and collect the values into a list. final Map> constraintMap = parsedConstraints.stream() - .collect(Collectors.groupingBy(ParsedConstraint::asKey, - Collectors.mapping(ParsedConstraint::value, Collectors.toList()))); + .collect(Collectors.groupingBy(ParsedConstraint::asKey, + Collectors.mapping(ParsedConstraint::value, Collectors.toList()))); // Create a match expression for each grouped constraint final List matchExpressions = constraintMap.entrySet().stream() - .map(entry -> new V1NodeSelectorRequirementBuilder() - .withKey(entry.getKey().label) - .withOperator(entry.getKey().operator) - .withValues(entry.getValue()) - .build() - ).collect(Collectors.toList()); + .map(entry -> new V1NodeSelectorRequirementBuilder() + .withKey(entry.getKey().label) + .withOperator(entry.getKey().operator) + .withValues(entry.getValue()) + .build() + ).collect(Collectors.toList()); // Build the rest of the affinity infrastructure around the match expressions return new V1AffinityBuilder() @@ -539,9 +566,9 @@ public static class ParsedConstraint { final String value; ParsedConstraint(String label, String operator, String value) { - this.label = label; + this.label = label; this.operator = operator; - this.value = value; + this.value = value; } static ParsedConstraint fromComparator(String label, String comparator, String value) { @@ -570,14 +597,18 @@ private static class ConstraintKey { final String operator; ConstraintKey(String label, String operator) { - this.label = label; + this.label = label; this.operator = operator; } @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } ConstraintKey that = (ConstraintKey) o; return Objects.equals(label, that.label) && Objects.equals(operator, that.operator); } @@ -617,7 +648,7 @@ public void unsuspendJob(final String jobName) () -> batchApi.patchNamespacedJobCall(jobName, namespace, patch, null, null, null, null, null, null), V1Patch.PATCH_FORMAT_STRATEGIC_MERGE_PATCH, batchApi.getApiClient() - ); + ); } catch (ApiException e) { log.error("Could not start job {}: message \"{}\" code {} body {}", jobName, e.getMessage(), e.getCode(), e.getResponseBody(), e); throw new ContainerBackendException("Could not start job " + jobName, e); @@ -646,6 +677,6 @@ public void removeJob(final String jobName) throws NotFoundException, ContainerB public enum Propagation { Orphan, Background, - Foreground; + Foreground } } From 505622310981022eef7250ed3ad77145dd8cdadf Mon Sep 17 00:00:00 2001 From: Automated Build Date: Mon, 30 Jun 2025 11:35:55 -0500 Subject: [PATCH 25/44] Update to version 3.7.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 06bca7fb..37542292 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.7.1-RC-SNAPSHOT" +version "3.7.1" java { sourceCompatibility = JavaVersion.VERSION_1_8 From 07214f16bb5cbc4e60803cb243a69561e9d896ad Mon Sep 17 00:00:00 2001 From: "Will Horton (XW)" Date: Mon, 30 Jun 2025 18:26:07 +0000 Subject: [PATCH 26/44] Update changelog for 3.7.1 release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0131b0f..cf6aaf71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,14 @@ # Changelog +## 3.7.1 +[Released](https://bitbucket.org/xnatdev/container-service/src/3.7.1/) + +* **Bugfix** [CS-1036](https://radiologics.atlassian.net/browse/CS-1036): Commands with spaces in their names could not be run in Kubernetes environment + + ## 3.7.0 [Released](https://bitbucket.org/xnatdev/container-service/src/3.7.0/) + * **New Feature** [CS-980](https://radiologics.atlassian.net/browse/CS-990): Add tracking labels to command json * **New Feature** [CS-990](https://radiologics.atlassian.net/browse/CS-990): Added Container Manager Role, Added Container Visibility * **Improvement** [CS-986](https://radiologics.atlassian.net/browse/CS-986): Allow halt to orchestrations on midstream command failures @@ -17,6 +24,7 @@ ## 3.6.1 [Released](https://bitbucket.org/xnatdev/container-service/src/3.6.1/) + * **Improvement** [CS-975](https://radiologics.atlassian.net/browse/CS-975)/[XNAT-8111](https://radiologics.atlassian.net/browse/XNAT-8111) Convert event handlers in CS to use JMS instead of executors * **Bugfix** [CS-985](https://radiologics.atlassian.net/browse/CS-985) Call to /xapi/docker/images failing in 3.6.0 in Kubernetes environments From 95aae10969de86a6430f6607a12766d699adc410 Mon Sep 17 00:00:00 2001 From: Bin Zhang Date: Wed, 23 Jul 2025 16:29:01 +0800 Subject: [PATCH 27/44] Update to version 3.7.1-java21-RC-SNAPSHOT --- build.gradle | 63 +++- gradle/wrapper/gradle-wrapper.jar | Bin 59821 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 6 + gradlew.bat | 14 +- .../command/entity/CommandVisibility.java | 1 - .../utils/KubernetesConfiguration.java | 2 +- .../CommandLaunchIntegrationTest.java | 192 ++++------ .../nrg/containers/CommandResolutionTest.java | 42 +-- .../ContainerCleanupIntegrationTest.java | 54 +-- .../org/nrg/containers/OrchestrationTest.java | 17 + .../org/nrg/containers/QueueConsumerTest.java | 69 ++-- .../SwarmConstraintsIntegrationTest.java | 117 +++--- .../SwarmRestartIntegrationTest.java | 90 +++-- .../api/DockerControlApiIntegrationTest.java | 2 +- .../containers/api/DockerControlApiTest.java | 105 ++++-- .../containers/config/CommandTestConfig.java | 6 + .../config/IntegrationTestConfig.java | 9 +- .../org/nrg/containers/config/JmsConfig.java | 31 +- .../config/LaunchRestApiTestConfig.java | 6 + .../nrg/containers/config/MockJmsConfig.java | 4 +- .../config/QueueConsumerTestConfig.java | 7 + .../QueueSettingsRestApiTestConfig.java | 37 +- .../containers/config/RestApiTestConfig.java | 2 +- .../jms/JmsConsumerExceptionTest.java | 87 +++-- .../jms/JmsExceptionIntegrationTest.java | 75 ++-- .../model/CommandConfigurationTest.java | 12 +- .../rest/CommandConfigurationRestApiTest.java | 33 +- .../containers/rest/CommandRestApiTest.java | 37 +- .../containers/rest/ContainerRestApiTest.java | 31 +- .../containers/rest/LaunchRestApiTest.java | 20 +- .../rest/QueueSettingsRestApiTest.java | 341 ++++++++++-------- .../nrg/containers/secrets/SecretsTest.java | 99 +++-- .../services/CommandsAvailableTest.java | 78 ++-- .../containers/services/ContainerLogTest.java | 39 +- .../services/ContainerStatusUpdateTest.java | 2 +- .../nrg/containers/utils/TestingUtils.java | 66 ++-- 37 files changed, 964 insertions(+), 834 deletions(-) diff --git a/build.gradle b/build.gradle index 37542292..d822a0c0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - vXnat = "1.9.2" + vXnat = "1.10.0-RC-SNAPSHOT" pluginAppName = "XNAT Container Service" } } @@ -13,17 +13,19 @@ plugins { id "io.spring.dependency-management" version "1.1.7" id "io.franzbecker.gradle-lombok" version "5.0.0" id "net.linguica.maven-settings" version "0.5" + id("org.openrewrite.rewrite") version("7.0.4") + } group "org.nrg.xnatx.plugins" -version "3.7.1" +version "3.7.1-java21-RC-SNAPSHOT" java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = 21 + targetCompatibility = 21 } -def vMockito = "1.10.19" +def vMockito = "5.7.0" def vJavassist = "3.21.0-GA" def vAwaitility = "2.0.0" @@ -38,7 +40,7 @@ dependencyManagement { imports { mavenBom("org.nrg:parent:${vXnat}") { bomProperties([ - "mockito.version": vMockito, + // "mockito.version": vMockito, "javassist.version": vJavassist ]) } @@ -46,9 +48,9 @@ dependencyManagement { } def vDockerJava = "3.4.0.1" -def vAutoValue = "1.3" +def vAutoValue = "1.5.4" def vKubernetesClient = "15.0.+" -def vPowerMock = "1.7.0" +def vPowerMock = "2.0.9" def vGson = dependencyManagement.importedProperties["gson.version"] def vJackson = dependencyManagement.importedProperties["jackson.version"] def vSpringSecurity = dependencyManagement.importedProperties["spring-security.version"] @@ -64,10 +66,24 @@ configurations { } } +tasks.withType(Test).configureEach { +// jvmArgs("--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED") + jvmArgs("--add-opens", "java.xml/jdk.xml.internal=ALL-UNNAMED") + jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED") + } + +lombok { + version = dependencyManagement.importedProperties["lombok.version"] as String + sha256 = dependencyManagement.importedProperties["lombok.checksum"] as String +} + dependencies { annotationProcessor "org.nrg:framework" annotationProcessor "com.google.auto.value:auto-value:${vAutoValue}" + rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.1.0") + + compileOnly "com.google.auto.value:auto-value:${vAutoValue}" implementAndInclude "com.github.docker-java:docker-java-core:${vDockerJava}" @@ -178,35 +194,52 @@ dependencies { runtimeOnly "com.google.code.findbugs:jsr305" runtimeOnly "org.yaml:snakeyaml:1.15+" - testImplementation "junit:junit" + testImplementation "junit:junit:4.13.2" + testImplementation(platform('org.junit:junit-bom:5.10.2')) + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation "org.hamcrest:hamcrest-library" testImplementation "org.springframework:spring-test" testImplementation "org.springframework.security:spring-security-test:${vSpringSecurity}" testImplementation "org.springframework.security:spring-security-config" - testImplementation "org.mockito:mockito-core" + testImplementation "org.mockito:mockito-core:${vMockito}" + testImplementation 'org.mockito:mockito-junit-jupiter:5.0.0' // JUnit 5 integration for Mockito + testImplementation "javax.servlet:javax.servlet-api" testImplementation "com.h2database:h2" testImplementation "org.springframework:spring-jdbc" testImplementation "org.springframework:spring-orm" testImplementation "org.apache.commons:commons-dbcp2" testImplementation "commons-net:commons-net" - testImplementation "org.powermock:powermock-module-junit4:${vPowerMock}" - testImplementation "org.powermock:powermock-api-mockito:${vPowerMock}" +// testImplementation "org.powermock:powermock-module-junit4:${vPowerMock}" +// testImplementation "org.powermock:powermock-api-mockito2:${vPowerMock}" testImplementation "org.awaitility:awaitility:${vAwaitility}" testImplementation "org.testcontainers:junit-jupiter:1.19.0" testImplementation "org.testcontainers:postgresql:1.19.0" } +rewrite { + // activeRecipe("org.openrewrite.java.testing.mockito.Mockito1to5Migration") + activeRecipe("org.openrewrite.java.testing.mockito.ReplacePowerMockito") + + setExportDatatables(true) +} + test { + useJUnitPlatform() systemProperty "integration", System.getProperty('integration') == "true" } tasks.register ( 'unitTest', Test ) { + useJUnitPlatform() + systemProperties['integration'] = "false" test } tasks.register ( 'integrationTest', Test ) { + useJUnitPlatform() systemProperties['integration'] = "true" test } @@ -252,7 +285,7 @@ ext.gitManifest = manifest { task fatJar(type: Jar) { zip64 true - classifier = "fat" + archiveClassifier.set("fat") from { configurations.implementAndInclude.collect { it.isDirectory() ? it : zipTree(it) } } { @@ -284,7 +317,7 @@ jar{ } task sourceJar(type: Jar, dependsOn: classes) { - classifier = "sources" + archiveClassifier.set("sources") manifest { from gitManifest } @@ -292,7 +325,7 @@ task sourceJar(type: Jar, dependsOn: classes) { } task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = "javadoc" + archiveClassifier.set("javadoc") manifest { from gitManifest } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 41d9927a4d4fb3f96a785543079b8df6723c946b..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 10197 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661e..a5952066 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c7873..a69d9cb6 100755 --- a/gradlew +++ b/gradlew @@ -205,6 +205,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f9..53a6b238 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/main/java/org/nrg/containers/model/command/entity/CommandVisibility.java b/src/main/java/org/nrg/containers/model/command/entity/CommandVisibility.java index cfd1ab8b..df926329 100644 --- a/src/main/java/org/nrg/containers/model/command/entity/CommandVisibility.java +++ b/src/main/java/org/nrg/containers/model/command/entity/CommandVisibility.java @@ -5,7 +5,6 @@ import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; -import org.omg.CORBA.PRIVATE_MEMBER; public enum CommandVisibility { diff --git a/src/main/java/org/nrg/containers/utils/KubernetesConfiguration.java b/src/main/java/org/nrg/containers/utils/KubernetesConfiguration.java index e8a6bede..35312cd8 100644 --- a/src/main/java/org/nrg/containers/utils/KubernetesConfiguration.java +++ b/src/main/java/org/nrg/containers/utils/KubernetesConfiguration.java @@ -136,7 +136,7 @@ public static Path find() { * * If a file cannot be found in those locations, returns null. * - * @return A KubeConfig if it can be found, or null + * @return A KubeConfig if it can be found, or null * @throws IOException if the configuration file or a file specified in a configuration file * cannot be read. */ diff --git a/src/test/java/org/nrg/containers/CommandLaunchIntegrationTest.java b/src/test/java/org/nrg/containers/CommandLaunchIntegrationTest.java index 543d6eda..5de542e2 100644 --- a/src/test/java/org/nrg/containers/CommandLaunchIntegrationTest.java +++ b/src/test/java/org/nrg/containers/CommandLaunchIntegrationTest.java @@ -12,11 +12,9 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.hamcrest.Matchers; -import org.junit.After; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.rules.TemporaryFolder; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; @@ -24,6 +22,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.mockito.ArgumentMatcher; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.nrg.containers.api.DockerControlApi; import org.nrg.containers.api.KubernetesClient; @@ -36,12 +35,7 @@ import org.nrg.containers.model.container.auto.Container.ContainerMount; import org.nrg.containers.model.server.docker.Backend; import org.nrg.containers.model.server.docker.DockerServerBase.DockerServer; -import org.nrg.containers.model.xnat.FakeWorkflow; -import org.nrg.containers.model.xnat.Project; -import org.nrg.containers.model.xnat.Resource; -import org.nrg.containers.model.xnat.Scan; -import org.nrg.containers.model.xnat.Session; -import org.nrg.containers.model.xnat.XnatModelObject; +import org.nrg.containers.model.xnat.*; import org.nrg.containers.services.CommandService; import org.nrg.containers.services.ContainerService; import org.nrg.containers.services.DockerServerService; @@ -62,7 +56,6 @@ import org.nrg.xft.ItemI; import org.nrg.xft.XFTItem; import org.nrg.xft.event.EventDetails; -import org.nrg.xft.event.EventMetaI; import org.nrg.xft.event.persist.PersistentWorkflowI; import org.nrg.xft.event.persist.PersistentWorkflowUtils; import org.nrg.xft.schema.XFTManager; @@ -76,11 +69,6 @@ import org.nrg.xnat.services.archive.CatalogService; import org.nrg.xnat.turbine.utils.ArchivableItem; import org.nrg.xnat.utils.WorkflowUtils; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.modules.junit4.PowerMockRunnerDelegate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; @@ -91,66 +79,46 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; import static org.awaitility.Awaitility.await; -import static org.hamcrest.Matchers.arrayContainingInAnyOrder; -import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.Matchers.hasItemInArray; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isEmptyOrNullString; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import static org.junit.Assume.assumeThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.argThat; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.nrg.containers.services.ContainerService.XNAT_EVENT_ID; -import static org.nrg.containers.services.ContainerService.XNAT_HOST; -import static org.nrg.containers.services.ContainerService.XNAT_PASS; -import static org.nrg.containers.services.ContainerService.XNAT_USER; -import static org.nrg.containers.services.ContainerService.XNAT_WORKFLOW_ID; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.nrg.containers.services.ContainerService.*; import static org.nrg.containers.utils.TestingUtils.BUSYBOX; -import static org.powermock.api.mockito.PowerMockito.doNothing; -import static org.powermock.api.mockito.PowerMockito.doReturn; -import static org.powermock.api.mockito.PowerMockito.mockStatic; @Slf4j -@RunWith(PowerMockRunner.class) -@PowerMockRunnerDelegate(Parameterized.class) -@PrepareForTest({UriParserUtils.class, XFTManager.class, Users.class, WorkflowUtils.class, - PersistentWorkflowUtils.class, XDATServlet.class, Session.class, ContainerServicePermissionUtils.class, - org.nrg.xnat.utils.FileUtils.class}) -@PowerMockIgnore({"org.apache.*", "java.*", "javax.*", "org.w3c.*", "com.sun.*"}) +@RunWith(Parameterized.class) @ContextConfiguration(classes = EventPullingIntegrationTestConfig.class) @Parameterized.UseParametersRunnerFactory(SpringJUnit4ClassRunnerFactory.class) @Transactional public class CommandLaunchIntegrationTest { + private MockedStatic mockedContainerServicePermissionUtils; + + private MockedStatic mockedSession; + + private MockedStatic mockedXDATServlet; + + private MockedStatic mockedWorkflowUtils; + + private MockedStatic mockedUsers; + + private MockedStatic mockedXFTManager; + + private MockedStatic mockedUriParserUtils; + @Parameterized.Parameters(name = "backend={0}") public static Collection backend() { return EnumSet.allOf(Backend.class); @@ -218,18 +186,12 @@ public void setup() throws Exception { // Mock the user management service when(mockUserManagementServiceI.getUser(FAKE_USER)).thenReturn(mockUser); - // Mock UriParserUtils using PowerMock. This allows us to mock out - // the responses to its static method parseURI(). - mockStatic(UriParserUtils.class); - // Mock the aliasTokenService final AliasToken mockAliasToken = new AliasToken(); mockAliasToken.setAlias(FAKE_ALIAS); mockAliasToken.setSecret(FAKE_SECRET); when(mockAliasTokenService.issueTokenForUser(mockUser)).thenReturn(mockAliasToken); - - mockStatic(Users.class); - when(Users.getUser(FAKE_USER)).thenReturn(mockUser); + mockedUsers.when(() -> Users.getUser(FAKE_USER)).thenReturn(mockUser); // Mock the site config preferences final String buildDir = folder.newFolder().getAbsolutePath(); @@ -238,35 +200,22 @@ public void setup() throws Exception { when(mockSiteConfigPreferences.getBuildPath()).thenReturn(buildDir); // transporter makes a directory under build when(mockSiteConfigPreferences.getArchivePath()).thenReturn(archiveDir); // container logs get stored under archive when(mockSiteConfigPreferences.getProperty("processingUrl", FAKE_HOST)).thenReturn(FAKE_HOST); - - // Use powermock to mock out the static method XFTManager.isInitialized() and XDATServlet.isDatabasePopulateOrUpdateCompleted() - mockStatic(XFTManager.class); - when(XFTManager.isInitialized()).thenReturn(true); - mockStatic(XDATServlet.class); - when(XDATServlet.isDatabasePopulateOrUpdateCompleted()).thenReturn(true); + mockedXFTManager.when(XFTManager::isInitialized).thenReturn(true); + mockedXDATServlet.when(XDATServlet::isDatabasePopulateOrUpdateCompleted).thenReturn(true); // Also mock out workflow operations to return our fake workflow object fakeWorkflow = new FakeWorkflow(); - mockStatic(WorkflowUtils.class); - when(WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) + mockedWorkflowUtils.when(() -> WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) .thenReturn(fakeWorkflow); - doNothing().when(WorkflowUtils.class, "save", any(PersistentWorkflowI.class), isNull(EventMetaI.class)); - PowerMockito.spy(PersistentWorkflowUtils.class); - doReturn(fakeWorkflow).when(PersistentWorkflowUtils.class, "getOrCreateWorkflowData", eq(FakeWorkflow.defaultEventId), - eq(mockUser), any(XFTItem.class), any(EventDetails.class)); + doNothing().when(WorkflowUtils.class); + Mockito.spy(PersistentWorkflowUtils.class); + doReturn(fakeWorkflow).when(PersistentWorkflowUtils.class); // mock external FS check when(mockCatalogService.hasRemoteFiles(eq(mockUser), any(String.class))).thenReturn(false); - - // We can't load the XFT item in the session, so don't try - // This is only used to check the permissions, and we mock that response anyway, so we don't need a real value - mockStatic(Session.class); - when(Session.loadXnatImageSessionData(any(String.class), eq(mockUser))) + mockedSession.when(() -> Session.loadXnatImageSessionData(any(String.class), eq(mockUser))) .thenReturn(null); - - // Permissions checks - mockStatic(ContainerServicePermissionUtils.class); - when(ContainerServicePermissionUtils.canCreateOutputObject( + mockedContainerServicePermissionUtils.when(() -> ContainerServicePermissionUtils.canCreateOutputObject( eq(mockUser), any(String.class), any(XnatModelObject.class), any(Command.CommandWrapperOutput.class) )).thenReturn(true); @@ -315,6 +264,28 @@ public void cleanup() throws Exception { executorService.shutdown(); } + @BeforeEach + void setUpStaticMocks() { + mockedContainerServicePermissionUtils = mockStatic(ContainerServicePermissionUtils.class); + mockedSession = mockStatic(Session.class); + mockedXDATServlet = mockStatic(XDATServlet.class); + mockedWorkflowUtils = mockStatic(WorkflowUtils.class); + mockedUsers = mockStatic(Users.class); + mockedXFTManager = mockStatic(XFTManager.class); + mockedUriParserUtils = mockStatic(UriParserUtils.class); + } + + @AfterEach + void tearDownStaticMocks() { + mockedUriParserUtils.closeOnDemand(); + mockedXFTManager.closeOnDemand(); + mockedUsers.closeOnDemand(); + mockedWorkflowUtils.closeOnDemand(); + mockedXDATServlet.closeOnDemand(); + mockedSession.closeOnDemand(); + mockedContainerServicePermissionUtils.closeOnDemand(); + } + @Test @DirtiesContext public void testFakeReconAll() throws Exception { @@ -345,7 +316,7 @@ public void testFakeReconAll() throws Exception { final String sessionJson = mapper.writeValueAsString(session); final ArchivableItem mockSesItem = mock(ArchivableItem.class); final ExptURI mockUriObject = mock(ExptURI.class); - when(UriParserUtils.parseURI("/archive" + session.getUri())).thenReturn(mockUriObject); + mockedUriParserUtils.when(() -> UriParserUtils.parseURI("/archive" + session.getUri())).thenReturn(mockUriObject); when(mockUriObject.getSecurityItem()).thenReturn(mockSesItem); final String t1Scantype = "T1_TEST_SCANTYPE"; @@ -467,11 +438,8 @@ public void testProjectMount() throws Exception { // Create the mock objects we will need in order to verify permissions final ArchivableItem mockProjectItem = mock(ArchivableItem.class); final ExptURI mockUriObject = mock(ExptURI.class); - when(UriParserUtils.parseURI("/archive" + project.getUri())).thenReturn(mockUriObject); + mockedUriParserUtils.when(() -> UriParserUtils.parseURI("/archive" + project.getUri())).thenReturn(mockUriObject); when(mockUriObject.getSecurityItem()).thenReturn(mockProjectItem); - - // Mock util method for shared file paths - PowerMockito.mockStatic(org.nrg.xnat.utils.FileUtils.class); when(org.nrg.xnat.utils.FileUtils.getAllSharedPaths(project.getId(), mockUser, false, false, true, false)) .thenReturn(Collections.emptyMap()); @@ -624,7 +592,7 @@ public void testLaunchCommandWithSetupCommand() throws Exception { final ArchivableItem mockItem = mock(ArchivableItem.class); final ResourcesExptURI mockUriObject = mock(ResourcesExptURI.class); String uri = "/archive" + resourceInput.getUri(); - when(UriParserUtils.parseURI(uri)).thenReturn(mockUriObject); + mockedUriParserUtils.when(() -> UriParserUtils.parseURI(uri)).thenReturn(mockUriObject); when(mockUriObject.getSecurityItem()).thenReturn(mockItem); fakeWorkflow.setId(uri); ResourceData mockRD = mock(ResourceData.class); @@ -642,12 +610,12 @@ public void testLaunchCommandWithSetupCommand() throws Exception { FakeWorkflow setupWrapupWorkflow = new FakeWorkflow(); setupWrapupWorkflow.setWfid(111); setupWrapupWorkflow.setEventId(2); - doReturn(setupWrapupWorkflow).when(PersistentWorkflowUtils.class, "getOrCreateWorkflowData", eq(2), - eq(mockUser), any(XFTItem.class), any(EventDetails.class)); - when(WorkflowUtils.buildOpenWorkflow(eq(mockUser), eq(xsiType), eq(id), eq(project), any(EventDetails.class))) + doReturn(setupWrapupWorkflow).when(PersistentWorkflowUtils.class + ); + mockedWorkflowUtils.when(() -> WorkflowUtils.buildOpenWorkflow(eq(mockUser), eq(xsiType), eq(id), eq(project), any(EventDetails.class))) .thenReturn(setupWrapupWorkflow); - when(WorkflowUtils.getUniqueWorkflow(mockUser, setupWrapupWorkflow.getWorkflowId().toString())) + mockedWorkflowUtils.when(() -> WorkflowUtils.getUniqueWorkflow(mockUser, setupWrapupWorkflow.getWorkflowId().toString())) .thenReturn(setupWrapupWorkflow); // Time to launch this thing @@ -767,7 +735,7 @@ public void testLaunchCommandWithWrapupCommand() throws Exception { final ArchivableItem mockSessionItem = mock(ArchivableItem.class); final ExptURI mockUriObject = mock(ExptURI.class); String uri = "/archive" + sessionInput.getUri(); - when(UriParserUtils.parseURI(uri)).thenReturn(mockUriObject); + mockedUriParserUtils.when(() -> UriParserUtils.parseURI(uri)).thenReturn(mockUriObject); when(mockUriObject.getSecurityItem()).thenReturn(mockSessionItem); String id = "id"; String xsiType = "type"; @@ -783,12 +751,12 @@ public void testLaunchCommandWithWrapupCommand() throws Exception { FakeWorkflow setupWrapupWorkflow = new FakeWorkflow(); setupWrapupWorkflow.setWfid(111); setupWrapupWorkflow.setEventId(2); - doReturn(setupWrapupWorkflow).when(PersistentWorkflowUtils.class, "getOrCreateWorkflowData", eq(2), - eq(mockUser), any(XFTItem.class), any(EventDetails.class)); - when(WorkflowUtils.buildOpenWorkflow(eq(mockUser), eq(xsiType), eq(id), eq(project), any(EventDetails.class))) + doReturn(setupWrapupWorkflow).when(PersistentWorkflowUtils.class + ); + mockedWorkflowUtils.when(() -> WorkflowUtils.buildOpenWorkflow(eq(mockUser), eq(xsiType), eq(id), eq(project), any(EventDetails.class))) .thenReturn(setupWrapupWorkflow); - when(WorkflowUtils.getUniqueWorkflow(mockUser, setupWrapupWorkflow.getWorkflowId().toString())) + mockedWorkflowUtils.when(() -> WorkflowUtils.getUniqueWorkflow(mockUser, setupWrapupWorkflow.getWorkflowId().toString())) .thenReturn(setupWrapupWorkflow); // Time to launch this thing @@ -1104,15 +1072,15 @@ public void testScanUpload() throws Exception { final ExptURI mockSesUri = mock(ExptURI.class); final ExptScanURI mockScanUri = mock(ExptScanURI.class); final String uri = "/archive" + session.getUri() + "/scans/scanNew"; - when(UriParserUtils.getArchiveUri(mockScanItem)).thenReturn(uri); - when(UriParserUtils.parseURI(uri)).thenReturn(mockScanUri); - when(UriParserUtils.parseURI("/archive" + session.getUri())).thenReturn(mockSesUri); + mockedUriParserUtils.when(() -> UriParserUtils.getArchiveUri(mockScanItem)).thenReturn(uri); + mockedUriParserUtils.when(() -> UriParserUtils.parseURI(uri)).thenReturn(mockScanUri); + mockedUriParserUtils.when(() -> UriParserUtils.parseURI("/archive" + session.getUri())).thenReturn(mockSesUri); when(mockScanUri.getSecurityItem()).thenReturn(mockSessionItem); when(mockSesUri.getSecurityItem()).thenReturn(mockSessionItem); ArgumentMatcher matchesXml = new ArgumentMatcher() { @Override - public boolean matches(Object arg) { + public boolean matches(File arg) { if (!(arg instanceof File)) { return false; } @@ -1126,7 +1094,7 @@ public boolean matches(Object arg) { }; ArgumentMatcher> matchesFileList = new ArgumentMatcher>() { @Override - public boolean matches(Object arg) { + public boolean matches(List arg) { if (!(arg instanceof List)) { return false; } @@ -1228,15 +1196,15 @@ public void testAssessorUpload() throws Exception { final ExptURI mockSesUri = mock(ExptURI.class); final ExptAssessorURI mockAssessorUri = mock(ExptAssessorURI.class); final String uri = "/archive" + session.getUri() + "/assessors/assessorNew"; - when(UriParserUtils.getArchiveUri(mockAssessorItem)).thenReturn(uri); - when(UriParserUtils.parseURI(uri)).thenReturn(mockAssessorUri); - when(UriParserUtils.parseURI("/archive" + session.getUri())).thenReturn(mockSesUri); + mockedUriParserUtils.when(() -> UriParserUtils.getArchiveUri(mockAssessorItem)).thenReturn(uri); + mockedUriParserUtils.when(() -> UriParserUtils.parseURI(uri)).thenReturn(mockAssessorUri); + mockedUriParserUtils.when(() -> UriParserUtils.parseURI("/archive" + session.getUri())).thenReturn(mockSesUri); when(mockAssessorUri.getSecurityItem()).thenReturn(mockSessionItem); when(mockSesUri.getSecurityItem()).thenReturn(mockSessionItem); ArgumentMatcher matchesXml = new ArgumentMatcher() { @Override - public boolean matches(Object arg) { + public boolean matches(File arg) { if (!(arg instanceof File)) { return false; } @@ -1250,7 +1218,7 @@ public boolean matches(Object arg) { }; ArgumentMatcher> matchesFileList = new ArgumentMatcher>() { @Override - public boolean matches(Object arg) { + public boolean matches(List arg) { if (!(arg instanceof List)) { return false; } @@ -1345,7 +1313,7 @@ public void testCommandWithGenericResourcesGpu() throws Exception { // Create the mock objects we will need in order to verify permissions final ArchivableItem mockProjectItem = mock(ArchivableItem.class); final ExptURI mockUriObject = mock(ExptURI.class); - when(UriParserUtils.parseURI("/archive" + project.getUri())).thenReturn(mockUriObject); + mockedUriParserUtils.when(() -> UriParserUtils.parseURI("/archive" + project.getUri())).thenReturn(mockUriObject); when(mockUriObject.getSecurityItem()).thenReturn(mockProjectItem); final Map runtimeValues = Collections.singletonMap("project", projectJson); diff --git a/src/test/java/org/nrg/containers/CommandResolutionTest.java b/src/test/java/org/nrg/containers/CommandResolutionTest.java index ac93126e..a4142e3c 100644 --- a/src/test/java/org/nrg/containers/CommandResolutionTest.java +++ b/src/test/java/org/nrg/containers/CommandResolutionTest.java @@ -7,6 +7,7 @@ import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.tuple.Pair; import org.hamcrest.Matchers; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -15,8 +16,7 @@ import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; -import org.junit.runner.RunWith; -import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.nrg.containers.config.ObjectMapperConfig; import org.nrg.containers.exceptions.CommandResolutionException; @@ -53,9 +53,6 @@ import org.nrg.xdat.services.cache.UserDataCache; import org.nrg.xft.security.UserI; import org.nrg.xnat.services.archive.CatalogService; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; import java.io.File; import java.nio.file.Path; @@ -80,15 +77,14 @@ import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; @Slf4j -@RunWith(PowerMockRunner.class) -@PrepareForTest(Users.class) public class CommandResolutionTest { + private MockedStatic mockedUsers; public static final String HELLO_1 = "hello1.txt"; public static final String HELLO_2 = "hello2.txt"; public static final String SUBDIR = "subdir"; @@ -108,13 +104,13 @@ public class CommandResolutionTest { private CommandResolutionService commandResolutionService; - @Mock private SiteConfigPreferences siteConfigPreferences; - @Mock private DockerService dockerService; - @Mock private DockerServerService dockerServerService; - @Mock private CatalogService catalogService; - @Mock private UserDataCache userDataCache; - @Mock private CommandService commandService; - @Mock private SystemPropertySecretSource.ValueObtainer systemPropertyObtainer; + private DockerService dockerService = Mockito.mock(DockerService.class); + private SiteConfigPreferences siteConfigPreferences = Mockito.mock(SiteConfigPreferences.class); + private DockerServerService dockerServerService = Mockito.mock(DockerServerService.class); + private CatalogService catalogService = Mockito.mock(CatalogService.class); + private UserDataCache userDataCache = Mockito.mock(UserDataCache.class); + private CommandService commandService = Mockito.mock(CommandService.class); + private SystemPropertySecretSource.ValueObtainer systemPropertyObtainer = Mockito.mock(SystemPropertySecretSource.ValueObtainer.class); private ObjectMapper mapper; @@ -136,14 +132,13 @@ protected void finished(Description description) { @Before public void setup() throws Exception { - + mockedUsers = Mockito.mockStatic(Users.class); mapper = (new ObjectMapperConfig()).objectMapper(); // Mock out a user for tests. Will return this as "admin" user. userI = Mockito.mock(UserI.class); when(userI.getLogin()).thenReturn("mockUser"); - PowerMockito.mockStatic(Users.class); - PowerMockito.when(Users.getAdminUser()).thenReturn(userI); + mockedUsers.when(Users::getAdminUser).thenReturn(userI); // Read test data files for command + wrappers resourceDir = Paths.get(ClassLoader.getSystemResource("commandResolutionTest").toURI()).toString().replace("%20", " "); @@ -197,6 +192,11 @@ private CommandWrapper wrapperByName(final Command command, final String wrapper return wrapper; } + @After + public void tearDownStaticMocks() { + mockedUsers.closeOnDemand(); + } + @Test public void testSessionScanResource() throws Exception { final String commandWrapperName = "session-scan-resource"; @@ -703,8 +703,8 @@ public void testRemoteFilesMount() throws Exception { // Just copy the archive dir over for now (tests for pullResourceCatalogsToDestination in xnat-web and filesystems_plugin) doAnswer(inv -> { - String src = inv.getArgumentAt(2, String.class); - String dest = inv.getArgumentAt(3, String.class); + String src = inv.getArgument(2, String.class); + String dest = inv.getArgument(3, String.class); FileUtils.copyDirectory(new File(src), new File(dest)); return null; }).when(catalogService).pullResourceCatalogsToDestination(eq(userI), diff --git a/src/test/java/org/nrg/containers/ContainerCleanupIntegrationTest.java b/src/test/java/org/nrg/containers/ContainerCleanupIntegrationTest.java index 9ebd4553..062ab328 100644 --- a/src/test/java/org/nrg/containers/ContainerCleanupIntegrationTest.java +++ b/src/test/java/org/nrg/containers/ContainerCleanupIntegrationTest.java @@ -6,11 +6,7 @@ import io.kubernetes.client.openapi.apis.BatchV1Api; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.SystemUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; import org.junit.rules.TemporaryFolder; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; @@ -57,27 +53,15 @@ import org.nrg.xnat.helpers.uri.UriParserUtils; import org.nrg.xnat.services.archive.CatalogService; import org.nrg.xnat.utils.WorkflowUtils; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.modules.junit4.PowerMockRunnerDelegate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Nullable; import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadLocalRandom; @@ -85,28 +69,24 @@ import java.util.function.Consumer; import static org.awaitility.Awaitility.await; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.startsWith; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import static org.junit.Assume.assumeThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNull; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; import static org.nrg.containers.utils.TestingUtils.BUSYBOX; -import static org.powermock.api.mockito.PowerMockito.doNothing; -import static org.powermock.api.mockito.PowerMockito.doReturn; -import static org.powermock.api.mockito.PowerMockito.mockStatic; +//import static org.powermock.api.mockito.PowerMockito.*; @Slf4j -@RunWith(PowerMockRunner.class) -@PowerMockRunnerDelegate(Parameterized.class) -@PrepareForTest({UriParserUtils.class, XFTManager.class, Users.class, WorkflowUtils.class, - PersistentWorkflowUtils.class, XDATServlet.class, Session.class, ContainerServicePermissionUtils.class}) -@PowerMockIgnore({"org.apache.*", "java.*", "javax.*", "org.w3c.*", "com.sun.*"}) +@RunWith(Parameterized.class) +//@PowerMockRunnerDelegate(Parameterized.class) +//@PrepareForTest({UriParserUtils.class, XFTManager.class, Users.class, WorkflowUtils.class, +// PersistentWorkflowUtils.class, XDATServlet.class, Session.class, ContainerServicePermissionUtils.class}) +//@PowerMockIgnore({"org.apache.*", "java.*", "javax.*", "org.w3c.*", "com.sun.*"}) @ContextConfiguration(classes = EventPullingIntegrationTestConfig.class) @Parameterized.UseParametersRunnerFactory(SpringJUnit4ClassRunnerFactory.class) @Transactional @@ -215,10 +195,10 @@ public void setup() throws Exception { mockStatic(WorkflowUtils.class); when(WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) .thenReturn(fakeWorkflow); - doNothing().when(WorkflowUtils.class, "save", any(PersistentWorkflowI.class), isNull(EventMetaI.class)); - PowerMockito.spy(PersistentWorkflowUtils.class); - doReturn(fakeWorkflow).when(PersistentWorkflowUtils.class, "getOrCreateWorkflowData", eq(FakeWorkflow.defaultEventId), - eq(mockUser), any(XFTItem.class), any(EventDetails.class)); + doNothing().when(WorkflowUtils.class); + Mockito.spy(PersistentWorkflowUtils.class); + doReturn(fakeWorkflow).when(PersistentWorkflowUtils.class + ); // mock external FS check when(mockCatalogService.hasRemoteFiles(eq(mockUser), any(String.class))).thenReturn(false); diff --git a/src/test/java/org/nrg/containers/OrchestrationTest.java b/src/test/java/org/nrg/containers/OrchestrationTest.java index fcc9f888..e273818a 100644 --- a/src/test/java/org/nrg/containers/OrchestrationTest.java +++ b/src/test/java/org/nrg/containers/OrchestrationTest.java @@ -390,20 +390,37 @@ public void testFindWhereWrapperIsFirstNotMatched() throws Exception { @Test @DirtiesContext + //CS-1031 public void testGetAvailableForProject() throws Exception { Orchestration o = orchestrationService.createOrUpdate(ORCHESTRATION); + //Test no orchestration is availble if individual wrappers are not enabled for the project OrchestrationProject op = orchestrationService.getAvailableForProject(PROJECT); assertNull(op.getSelectedOrchestrationId()); + assertThat(op.getAvailableOrchestrations(), hasSize(0)); + + //Enable the individual wrappers for the project + commandService.enableForProject(PROJECT, o.getWrapperIds().get(0), user.getUsername(), "test"); + commandService.enableForProject(PROJECT, o.getWrapperIds().get(1), user.getUsername(), "test"); + op = orchestrationService.getAvailableForProject(PROJECT); + assertNull(op.getSelectedOrchestrationId()); assertThat(op.getAvailableOrchestrations(), hasSize(1)); assertThat(op.getAvailableOrchestrations().get(0).isEnabled(), is(true)); + //Enable the Orchestration for the project + orchestrationService.setProjectOrchestration(PROJECT, o.getId(), user); + op = orchestrationService.getAvailableForProject(PROJECT); + assertNotNull(op.getSelectedOrchestrationId()); + assertThat(op.getAvailableOrchestrations().get(0).isEnabled(), is(true)); + + //Disable the Orchestration at the site, will disable the project orchestration orchestrationService.setEnabled(o.getId(), false, user); op = orchestrationService.getAvailableForProject(PROJECT); assertNull(op.getSelectedOrchestrationId()); assertThat(op.getAvailableOrchestrations(), hasSize(1)); assertThat(op.getAvailableOrchestrations().get(0).isEnabled(), is(false)); + //Enable the Orchestration at the site, Project level Orchestration has to be re-enabled orchestrationService.setEnabled(o.getId(), true, user); orchestrationService.setProjectOrchestration(PROJECT, o.getId(), user); op = orchestrationService.getAvailableForProject(PROJECT); diff --git a/src/test/java/org/nrg/containers/QueueConsumerTest.java b/src/test/java/org/nrg/containers/QueueConsumerTest.java index d70a8854..3517158b 100644 --- a/src/test/java/org/nrg/containers/QueueConsumerTest.java +++ b/src/test/java/org/nrg/containers/QueueConsumerTest.java @@ -9,7 +9,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; -import org.mockito.Mockito; +import org.mockito.MockedStatic; import org.nrg.containers.api.DockerControlApi; import org.nrg.containers.config.QueueConsumerTestConfig; import org.nrg.containers.exceptions.CommandResolutionException; @@ -22,7 +22,6 @@ import org.nrg.containers.model.xnat.FakeWorkflow; import org.nrg.containers.services.CommandResolutionService; import org.nrg.containers.services.CommandService; -import org.nrg.containers.services.ContainerEntityService; import org.nrg.containers.services.ContainerService; import org.nrg.containers.services.DockerServerService; import org.nrg.containers.utils.TestingUtils; @@ -40,10 +39,6 @@ import org.nrg.xft.security.UserI; import org.nrg.xnat.helpers.uri.UriParserUtils; import org.nrg.xnat.utils.WorkflowUtils; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.modules.junit4.PowerMockRunnerDelegate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; @@ -59,23 +54,24 @@ import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assume.assumeThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.argThat; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.doNothing; -import static org.powermock.api.mockito.PowerMockito.mockStatic; +//import static org.mockito.Mockito.*; @Slf4j -@RunWith(PowerMockRunner.class) -@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class) -@PrepareForTest({UriParserUtils.class, XFTManager.class, Users.class, WorkflowUtils.class}) -@PowerMockIgnore({"org.apache.*", "java.*", "javax.*", "org.w3c.*", "com.sun.*"}) +@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = QueueConsumerTestConfig.class) @Transactional public class QueueConsumerTest { + private MockedStatic mockedWorkflowUtils; + private MockedStatic mockedUsers; + private MockedStatic mockedXFTManager; + private MockedStatic mockedUriParserUtils; private UserI mockUser; private String buildDir; private String archiveDir; @@ -99,7 +95,6 @@ public class QueueConsumerTest { private final String REAL_IMAGE = "busybox:latest"; @Autowired private CommandService mockCommandService; - @Autowired private ContainerEntityService mockContainerEntityService; @Autowired private CommandResolutionService mockCommandResolutionService; @Autowired private ContainerService containerService; @Autowired private DockerControlApi mockDockerControlApi; @@ -120,6 +115,11 @@ public void setup() throws Exception { final String certPathEnv = System.getenv("DOCKER_CERT_PATH"); final String tlsVerify = System.getenv("DOCKER_TLS_VERIFY"); + mockedWorkflowUtils = mockStatic(WorkflowUtils.class); + mockedUsers = mockStatic(Users.class); + mockedXFTManager = mockStatic(XFTManager.class); + mockedUriParserUtils = mockStatic(UriParserUtils.class); + final boolean useTls = tlsVerify != null && tlsVerify.equals("1"); final String certPath; if (useTls) { @@ -155,6 +155,7 @@ public void setup() throws Exception { // Mock the userI mockUser = mock(UserI.class); when(mockUser.getLogin()).thenReturn(FAKE_USER); + when(mockUser.getEmail()).thenReturn("unittest@xnat.org"); // Permissions when(mockPermissionsServiceI.canEdit(any(UserI.class), any(ItemI.class))).thenReturn(Boolean.TRUE); @@ -162,18 +163,12 @@ public void setup() throws Exception { // Mock the user management service when(mockUserManagementServiceI.getUser(FAKE_USER)).thenReturn(mockUser); - // Mock UriParserUtils using PowerMock. This allows us to mock out - // the responses to its static method parseURI(). - mockStatic(UriParserUtils.class); - // Mock the aliasTokenService final AliasToken mockAliasToken = new AliasToken(); mockAliasToken.setAlias(FAKE_ALIAS); mockAliasToken.setSecret(FAKE_SECRET); when(mockAliasTokenService.issueTokenForUser(mockUser)).thenReturn(mockAliasToken); - - mockStatic(Users.class); - when(Users.getUser(FAKE_USER)).thenReturn(mockUser); + mockedUsers.when(() -> Users.getUser(FAKE_USER)).thenReturn(mockUser); // Mock the site config preferences buildDir = folder.newFolder().getAbsolutePath(); @@ -197,7 +192,7 @@ public void setup() throws Exception { .commandLine("echo hello world") .addRawInputValue(INPUT_NAME, INPUT_VALUE) .build(); - mockConfiguredCommand = Mockito.mock(ConfiguredCommand.class); + mockConfiguredCommand = mock(ConfiguredCommand.class); when(mockCommandService.getWrapper(WRAPPER_ID)).thenReturn(COMMAND_WRAPPER); when(mockCommandService.retrieveWrapper(WRAPPER_ID)).thenReturn(COMMAND_WRAPPER); when(mockCommandService.getAndConfigure(WRAPPER_ID)).thenReturn(mockConfiguredCommand); @@ -210,35 +205,36 @@ public void setup() throws Exception { mockUser, fakeWorkflow.getWorkflowId().toString() )).thenReturn(RESOLVED_COMMAND); - - // Use powermock to mock out the static method XFTManager.isInitialized() - mockStatic(XFTManager.class); - when(XFTManager.isInitialized()).thenReturn(true); - - // Also mock out workflow operations to return our fake workflow object - mockStatic(WorkflowUtils.class); - when(WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) + mockedXFTManager.when(XFTManager::isInitialized).thenReturn(true); + mockedWorkflowUtils.when(() -> WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) .thenReturn(fakeWorkflow); - doNothing().when(WorkflowUtils.class, "save", any(PersistentWorkflowI.class), isNull(EventMetaI.class)); + mockedWorkflowUtils.when(() -> WorkflowUtils.save(any(PersistentWorkflowI.class), isNull(EventMetaI.class))) + .thenAnswer(invocationOnMock -> null); } @After public void cleanup() throws Exception { fakeWorkflow = new FakeWorkflow(); + mockedUriParserUtils.closeOnDemand(); + mockedXFTManager.closeOnDemand(); + mockedUsers.closeOnDemand(); + mockedWorkflowUtils.closeOnDemand(); } @Test @DirtiesContext + //CS-1031 public void testCommandResolutionException() throws Exception { final Map input = new HashMap<>(); final String badInputValue = "a bad value"; input.put(INPUT_NAME, badInputValue); final String exceptionMessage = "uh oh - bad input!"; + final String exceptionMessageReturned = "Submitted data does not match command resolution requirements. " + exceptionMessage; when(mockCommandResolutionService.resolve( eq(mockConfiguredCommand), argThat(TestingUtils.isMapWithEntry(INPUT_NAME, badInputValue)), - isNull(String.class), + isNull(), eq(mockUser), eq(fakeWorkflow.getWorkflowId().toString()) )).thenThrow(new CommandResolutionException(exceptionMessage)); @@ -247,11 +243,12 @@ public void testCommandResolutionException() throws Exception { containerService.consumeResolveCommandAndLaunchContainer(null, WRAPPER_ID, 0L, null, input, mockUser, fakeWorkflow.getWorkflowId().toString()); assertThat(fakeWorkflow.getStatus(), is(expectedWorkflowStatus)); - assertThat(fakeWorkflow.getDetails(), is(exceptionMessage)); + assertThat(fakeWorkflow.getDetails(), is(exceptionMessageReturned)); } @Test @DirtiesContext + //CS-1031 and PR Commit 9fd443a public void testContainerLaunchException() throws Exception { assumeThat(SystemUtils.IS_OS_WINDOWS_7, is(false)); final String exceptionMessage = "uh oh - issue launching container!"; diff --git a/src/test/java/org/nrg/containers/SwarmConstraintsIntegrationTest.java b/src/test/java/org/nrg/containers/SwarmConstraintsIntegrationTest.java index d485fbae..8ba56db5 100644 --- a/src/test/java/org/nrg/containers/SwarmConstraintsIntegrationTest.java +++ b/src/test/java/org/nrg/containers/SwarmConstraintsIntegrationTest.java @@ -4,16 +4,15 @@ import com.github.dockerjava.api.model.SwarmNode; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.SystemUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.rules.TemporaryFolder; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.junit.runner.RunWith; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.nrg.containers.api.DockerControlApi; import org.nrg.containers.config.EventPullingIntegrationTestConfig; @@ -44,7 +43,6 @@ import org.nrg.xft.ItemI; import org.nrg.xft.XFTItem; import org.nrg.xft.event.EventDetails; -import org.nrg.xft.event.EventMetaI; import org.nrg.xft.event.persist.PersistentWorkflowI; import org.nrg.xft.event.persist.PersistentWorkflowUtils; import org.nrg.xft.schema.XFTManager; @@ -52,11 +50,6 @@ import org.nrg.xnat.helpers.uri.UriParserUtils; import org.nrg.xnat.services.archive.CatalogService; import org.nrg.xnat.utils.WorkflowUtils; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.modules.junit4.PowerMockRunnerDelegate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; @@ -64,47 +57,33 @@ import org.springframework.transaction.annotation.Transactional; import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static org.awaitility.Awaitility.await; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.either; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assume.assumeThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; import static org.nrg.containers.model.command.entity.CommandType.DOCKER_SETUP; import static org.nrg.containers.model.command.entity.CommandType.DOCKER_WRAPUP; -import static org.powermock.api.mockito.PowerMockito.doNothing; -import static org.powermock.api.mockito.PowerMockito.doReturn; -import static org.powermock.api.mockito.PowerMockito.mockStatic; @Slf4j -@RunWith(PowerMockRunner.class) -@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class) -@PrepareForTest({UriParserUtils.class, XFTManager.class, Users.class, WorkflowUtils.class, - PersistentWorkflowUtils.class, XDATServlet.class, Session.class, ContainerServicePermissionUtils.class}) -@PowerMockIgnore({"org.apache.*", "java.*", "javax.*", "org.w3c.*", "com.sun.*"}) +@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = EventPullingIntegrationTestConfig.class) @Transactional public class SwarmConstraintsIntegrationTest { + private MockedStatic mockedContainerServicePermissionUtils; + private MockedStatic mockedSession; + private MockedStatic mockedXDATServlet; + private MockedStatic mockedWorkflowUtils; + private MockedStatic mockedUsers; + private MockedStatic mockedXFTManager; + private MockedStatic mockedUriParserUtils; private UserI mockUser; private String buildDir; private String archiveDir; @@ -207,18 +186,12 @@ public void setup() throws Exception { // Mock the user management service when(mockUserManagementServiceI.getUser(FAKE_USER)).thenReturn(mockUser); - // Mock UriParserUtils using PowerMock. This allows us to mock out - // the responses to its static method parseURI(). - mockStatic(UriParserUtils.class); - // Mock the aliasTokenService final AliasToken mockAliasToken = new AliasToken(); mockAliasToken.setAlias(FAKE_ALIAS); mockAliasToken.setSecret(FAKE_SECRET); when(mockAliasTokenService.issueTokenForUser(mockUser)).thenReturn(mockAliasToken); - - mockStatic(Users.class); - when(Users.getUser(FAKE_USER)).thenReturn(mockUser); + mockedUsers.when(() -> Users.getUser(FAKE_USER)).thenReturn(mockUser); // Mock the site config preferences buildDir = folder.newFolder().getAbsolutePath(); @@ -227,34 +200,20 @@ public void setup() throws Exception { when(mockSiteConfigPreferences.getBuildPath()).thenReturn(buildDir); // transporter makes a directory under build when(mockSiteConfigPreferences.getArchivePath()).thenReturn(archiveDir); // container logs get stored under archive when(mockSiteConfigPreferences.getProperty("processingUrl", FAKE_HOST)).thenReturn(FAKE_HOST); - - // Use powermock to mock out the static method XFTManager.isInitialized() and XDATServlet.isDatabasePopulateOrUpdateCompleted() - mockStatic(XFTManager.class); - when(XFTManager.isInitialized()).thenReturn(true); - mockStatic(XDATServlet.class); - when(XDATServlet.isDatabasePopulateOrUpdateCompleted()).thenReturn(true); - - // Also mock out workflow operations to return our fake workflow object - mockStatic(WorkflowUtils.class); - when(WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) + mockedXFTManager.when(XFTManager::isInitialized).thenReturn(true); + mockedXDATServlet.when(XDATServlet::isDatabasePopulateOrUpdateCompleted).thenReturn(true); + mockedWorkflowUtils.when(() -> WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) .thenReturn(fakeWorkflow); - doNothing().when(WorkflowUtils.class, "save", Mockito.any(PersistentWorkflowI.class), isNull(EventMetaI.class)); - PowerMockito.spy(PersistentWorkflowUtils.class); - doReturn(fakeWorkflow).when(PersistentWorkflowUtils.class, "getOrCreateWorkflowData", eq(FakeWorkflow.defaultEventId), - eq(mockUser), Mockito.any(XFTItem.class), Mockito.any(EventDetails.class)); + doNothing().when(WorkflowUtils.class); + Mockito.spy(PersistentWorkflowUtils.class); + doReturn(fakeWorkflow).when(PersistentWorkflowUtils.class + ); // mock external FS check when(mockCatalogService.hasRemoteFiles(eq(mockUser), any(String.class))).thenReturn(false); - - // We can't load the XFT item in the session, so don't try - // This is only used to check the permissions, and we mock that response anyway, so we don't need a real value - mockStatic(Session.class); - when(Session.loadXnatImageSessionData(any(String.class), eq(mockUser))) + mockedSession.when(() -> Session.loadXnatImageSessionData(any(String.class), eq(mockUser))) .thenReturn(null); - - // Permissions checks - mockStatic(ContainerServicePermissionUtils.class); - when(ContainerServicePermissionUtils.canCreateOutputObject( + mockedContainerServicePermissionUtils.when(() -> ContainerServicePermissionUtils.canCreateOutputObject( eq(mockUser), any(String.class), any(XnatModelObject.class), any(Command.CommandWrapperOutput.class) )).thenReturn(true); @@ -375,6 +334,28 @@ private List setUpServerWithConstr return constraints; } + @BeforeEach + void setUpStaticMocks() { + mockedContainerServicePermissionUtils = mockStatic(ContainerServicePermissionUtils.class); + mockedSession = mockStatic(Session.class); + mockedXDATServlet = mockStatic(XDATServlet.class); + mockedWorkflowUtils = mockStatic(WorkflowUtils.class); + mockedUsers = mockStatic(Users.class); + mockedXFTManager = mockStatic(XFTManager.class); + mockedUriParserUtils = mockStatic(UriParserUtils.class); + } + + @AfterEach + void tearDownStaticMocks() { + mockedUriParserUtils.closeOnDemand(); + mockedXFTManager.closeOnDemand(); + mockedUsers.closeOnDemand(); + mockedWorkflowUtils.closeOnDemand(); + mockedXDATServlet.closeOnDemand(); + mockedSession.closeOnDemand(); + mockedContainerServicePermissionUtils.closeOnDemand(); + } + @Test @DirtiesContext public void testAlwaysRunIfNoConstraints() throws Exception { diff --git a/src/test/java/org/nrg/containers/SwarmRestartIntegrationTest.java b/src/test/java/org/nrg/containers/SwarmRestartIntegrationTest.java index 76a135d8..a3e7632b 100644 --- a/src/test/java/org/nrg/containers/SwarmRestartIntegrationTest.java +++ b/src/test/java/org/nrg/containers/SwarmRestartIntegrationTest.java @@ -5,17 +5,16 @@ import com.github.dockerjava.api.model.SwarmNodeManagerStatus; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.SystemUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.rules.TemporaryFolder; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.nrg.containers.api.DockerControlApi; import org.nrg.containers.config.EventPullingIntegrationTestConfig; import org.nrg.containers.model.command.auto.Command; @@ -42,7 +41,6 @@ import org.nrg.xft.ItemI; import org.nrg.xft.XFTItem; import org.nrg.xft.event.EventDetails; -import org.nrg.xft.event.EventMetaI; import org.nrg.xft.event.persist.PersistentWorkflowI; import org.nrg.xft.event.persist.PersistentWorkflowUtils; import org.nrg.xft.schema.XFTManager; @@ -50,11 +48,6 @@ import org.nrg.xnat.helpers.uri.UriParserUtils; import org.nrg.xnat.services.XnatAppInfo; import org.nrg.xnat.utils.WorkflowUtils; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.modules.junit4.PowerMockRunnerDelegate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; @@ -70,30 +63,24 @@ import java.util.concurrent.TimeUnit; import static org.awaitility.Awaitility.await; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; import static org.junit.Assume.assumeThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; import static org.nrg.containers.utils.TestingUtils.BUSYBOX; -import static org.powermock.api.mockito.PowerMockito.doNothing; -import static org.powermock.api.mockito.PowerMockito.doReturn; -import static org.powermock.api.mockito.PowerMockito.mockStatic; @Slf4j -@RunWith(PowerMockRunner.class) -@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class) -@PrepareForTest({UriParserUtils.class, XFTManager.class, Users.class, WorkflowUtils.class, - PersistentWorkflowUtils.class, XDATServlet.class}) -@PowerMockIgnore({"org.apache.*", "java.*", "javax.*", "org.w3c.*", "com.sun.*"}) +@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = EventPullingIntegrationTestConfig.class) @Transactional public class SwarmRestartIntegrationTest { + private MockedStatic mockedXDATServlet; + private MockedStatic mockedWorkflowUtils; + private MockedStatic mockedUsers; + private MockedStatic mockedXFTManager; + private MockedStatic mockedUriParserUtils; private Backend backend = Backend.SWARM; private boolean swarmMode = true; @@ -154,18 +141,12 @@ public void setup() throws Exception { // Mock the user management service when(mockUserManagementServiceI.getUser(FAKE_USER)).thenReturn(mockUser); - // Mock UriParserUtils using PowerMock. This allows us to mock out - // the responses to its static method parseURI(). - mockStatic(UriParserUtils.class); - // Mock the aliasTokenService final AliasToken mockAliasToken = new AliasToken(); mockAliasToken.setAlias(FAKE_ALIAS); mockAliasToken.setSecret(FAKE_SECRET); when(mockAliasTokenService.issueTokenForUser(mockUser)).thenReturn(mockAliasToken); - - mockStatic(Users.class); - when(Users.getUser(FAKE_USER)).thenReturn(mockUser); + mockedUsers.when(() -> Users.getUser(FAKE_USER)).thenReturn(mockUser); // Mock the site config preferences buildDir = folder.newFolder().getAbsolutePath(); @@ -174,21 +155,14 @@ public void setup() throws Exception { when(mockSiteConfigPreferences.getBuildPath()).thenReturn(buildDir); // transporter makes a directory under build when(mockSiteConfigPreferences.getArchivePath()).thenReturn(archiveDir); // container logs get stored under archive when(mockSiteConfigPreferences.getProperty("processingUrl", FAKE_HOST)).thenReturn(FAKE_HOST); - - // Use powermock to mock out the static method XFTManager.isInitialized() and XDATServlet.isDatabasePopulateOrUpdateCompleted() - mockStatic(XFTManager.class); - when(XFTManager.isInitialized()).thenReturn(true); - mockStatic(XDATServlet.class); - when(XDATServlet.isDatabasePopulateOrUpdateCompleted()).thenReturn(true); - - // Also mock out workflow operations to return our fake workflow object - mockStatic(WorkflowUtils.class); - when(WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) + mockedXFTManager.when(XFTManager::isInitialized).thenReturn(true); + mockedXDATServlet.when(XDATServlet::isDatabasePopulateOrUpdateCompleted).thenReturn(true); + mockedWorkflowUtils.when(() -> WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) .thenReturn(fakeWorkflow); - doNothing().when(WorkflowUtils.class, "save", any(PersistentWorkflowI.class), isNull(EventMetaI.class)); - PowerMockito.spy(PersistentWorkflowUtils.class); - doReturn(fakeWorkflow).when(PersistentWorkflowUtils.class, "getOrCreateWorkflowData", eq(FakeWorkflow.defaultEventId), - eq(mockUser), any(XFTItem.class), any(EventDetails.class)); + doNothing().when(WorkflowUtils.class); + Mockito.spy(PersistentWorkflowUtils.class); + doReturn(fakeWorkflow).when(PersistentWorkflowUtils.class + ); // Setup docker server final BackendConfig backendConfig = TestingUtils.getBackendConfig(); @@ -229,6 +203,24 @@ public void cleanup() throws Exception { TestingUtils.cleanDockerImages(controlApi.getDockerClient(), imagesToCleanUp); } + @BeforeEach + void setUpStaticMocks() { + mockedXDATServlet = mockStatic(XDATServlet.class); + mockedWorkflowUtils = mockStatic(WorkflowUtils.class); + mockedUsers = mockStatic(Users.class); + mockedXFTManager = mockStatic(XFTManager.class); + mockedUriParserUtils = mockStatic(UriParserUtils.class); + } + + @AfterEach + void tearDownStaticMocks() { + mockedUriParserUtils.closeOnDemand(); + mockedXFTManager.closeOnDemand(); + mockedUsers.closeOnDemand(); + mockedWorkflowUtils.closeOnDemand(); + mockedXDATServlet.closeOnDemand(); + } + @Test @DirtiesContext public void testRestartShutdown() throws Exception { diff --git a/src/test/java/org/nrg/containers/api/DockerControlApiIntegrationTest.java b/src/test/java/org/nrg/containers/api/DockerControlApiIntegrationTest.java index 7dbb3001..7fa339eb 100644 --- a/src/test/java/org/nrg/containers/api/DockerControlApiIntegrationTest.java +++ b/src/test/java/org/nrg/containers/api/DockerControlApiIntegrationTest.java @@ -12,7 +12,7 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.nrg.containers.events.model.DockerContainerEvent; import org.nrg.containers.exceptions.DockerServerException; import org.nrg.containers.exceptions.NoDockerServerException; diff --git a/src/test/java/org/nrg/containers/api/DockerControlApiTest.java b/src/test/java/org/nrg/containers/api/DockerControlApiTest.java index 994371cb..29e2ce73 100644 --- a/src/test/java/org/nrg/containers/api/DockerControlApiTest.java +++ b/src/test/java/org/nrg/containers/api/DockerControlApiTest.java @@ -14,7 +14,6 @@ import com.github.dockerjava.api.model.ServiceModeConfig; import com.github.dockerjava.api.model.ServiceSpec; import com.google.common.collect.ImmutableList; -import io.kubernetes.client.util.PatchUtils; import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Rule; @@ -26,7 +25,6 @@ import org.junit.runners.Parameterized; import org.mockito.Answers; import org.mockito.ArgumentCaptor; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -38,12 +36,9 @@ import org.nrg.containers.services.DockerHubService; import org.nrg.containers.services.DockerServerService; import org.nrg.xft.security.UserI; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.modules.junit4.PowerMockRunnerDelegate; -import org.powermock.reflect.Whitebox; +import org.springframework.test.util.ReflectionTestUtils; +import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; @@ -56,18 +51,18 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assume.assumeThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.powermock.api.support.membermodification.MemberMatcher.method; +//import static org.powermock.api.support.membermodification.MemberMatcher.method; @Slf4j -@RunWith(PowerMockRunner.class) -@PowerMockRunnerDelegate(Parameterized.class) -@PrepareForTest({DockerControlApi.class, PatchUtils.class}) +@RunWith(Parameterized.class) public class DockerControlApiTest { final String BACKEND_ID = UUID.randomUUID().toString(); final String USER_LOGIN = UUID.randomUUID().toString(); @@ -97,17 +92,18 @@ protected void finished(Description description) { } }; - @Mock private DockerHubService dockerHubService; - @Mock private DockerServerService dockerServerService; - @Mock private KubernetesClientFactory kubernetesClientFactory; - @Mock private KubernetesClient kubernetesClient; + private DockerHubService dockerHubService = Mockito.mock(DockerHubService.class); + private DockerServerService dockerServerService = Mockito.mock(DockerServerService.class); - @Mock(answer = Answers.RETURNS_MOCKS) private com.github.dockerjava.api.DockerClient mockDockerJavaClient; - @Mock private DockerImage mockDockerImage; + private KubernetesClientFactory kubernetesClientFactory = Mockito.mock(KubernetesClientFactory.class); + private KubernetesClient kubernetesClient = Mockito.mock(KubernetesClient.class); - @Mock private DockerServer dockerServer; - @Mock private Container container; - @Mock private UserI user; + private com.github.dockerjava.api.DockerClient mockDockerJavaClient = Mockito.mock(com.github.dockerjava.api.DockerClient.class, Answers.RETURNS_MOCKS); + private DockerImage mockDockerImage = Mockito.mock(DockerImage.class); + + private DockerServer dockerServer = Mockito.mock(DockerServer.class); + private Container container = Mockito.mock(Container.class); + private UserI user = Mockito.mock(UserI.class); private DockerControlApi dockerControlApi; @@ -118,15 +114,28 @@ public void setup() throws Exception { // to use PowerMock to mock it out. // The fact that we have to do this is a code smell! // Should probably inject this client instance into DockerControlApi as a bean. - dockerControlApi = PowerMockito.spy(new DockerControlApi( + dockerControlApi = Mockito.spy(new DockerControlApi( dockerServerService, dockerHubService, kubernetesClientFactory )); - PowerMockito.doReturn(mockDockerImage) - .when(dockerControlApi, method(DockerControlApi.class, "pullImage", String.class)) - .withArguments(anyString()); - PowerMockito.doReturn(mockDockerJavaClient) - .when(dockerControlApi, method(DockerControlApi.class, "getDockerClient", DockerServer.class)) - .withArguments(dockerServer); +// Mockito.doReturn(mockDockerImage) +// .when(dockerControlApi, method(DockerControlApi.class, "pullImage", String.class)) +// .withArguments(anyString()); + + Method method = DockerControlApi.class.getDeclaredMethod("pullImage", String.class); + method.setAccessible(true); + + Mockito.doReturn(mockDockerImage) + .when(dockerControlApi) + .getClass() + .getDeclaredMethod("pullImage", String.class) + .invoke(dockerControlApi, anyString()); + +// PowerMockito.doReturn(mockDockerJavaClient) +// .when(dockerControlApi, method(DockerControlApi.class, "getDockerClient", DockerServer.class)) +// .withArguments(dockerServer); + Mockito.doReturn(mockDockerJavaClient) + .when(dockerControlApi) + .getDockerClient(any(DockerServer.class)); // Mock simple return values when(dockerServer.backend()).thenReturn(backend); @@ -160,7 +169,7 @@ public void testPing() throws Exception { case DOCKER: final PingCmd pingCmd = Mockito.mock(PingCmd.class); when(mockDockerJavaClient.pingCmd()).thenReturn(pingCmd); - when(pingCmd.exec()).thenReturn(null); + doNothing().when(pingCmd).exec(); break; case SWARM: final InspectSwarmCmd inspectSwarmCmd = Mockito.mock(InspectSwarmCmd.class); @@ -299,9 +308,18 @@ public void testCreate_container() throws Exception { // We also try to pull the image Mockito.when(mockDockerImage.tags()).thenReturn(ImmutableList.of(dockerImage)); - PowerMockito.doReturn(Collections.singletonList(mockDockerImage)) - .when(dockerControlApi, method(DockerControlApi.class, "getAllImages", DockerServer.class)) - .withArguments(dockerServer); +// Mockito.doReturn(Collections.singletonList(mockDockerImage)) +// .when(dockerControlApi, method(DockerControlApi.class, "getAllImages", DockerServer.class)) +// .withArguments(dockerServer); + + Method method = DockerControlApi.class.getDeclaredMethod("getAllImages", DockerServer.class); + method.setAccessible(true); + + Mockito.doReturn(Collections.singletonList(mockDockerImage)) + .when(dockerControlApi) + .getClass() + .getDeclaredMethod("getAllImages", DockerServer.class); + dockerControlApi.getAllImages(); } final Container expected = toLaunchAndExpectedContainerBuilder.build(); @@ -360,9 +378,14 @@ public void testCreate_resolvedCommand() throws Exception { // We also try to pull the image Mockito.when(mockDockerImage.tags()).thenReturn(ImmutableList.of(dockerImage)); - PowerMockito.doReturn(Collections.singletonList(mockDockerImage)) - .when(dockerControlApi, method(DockerControlApi.class, "getAllImages", DockerServer.class)) - .withArguments(dockerServer); + Method method = DockerControlApi.class.getDeclaredMethod("getAllImages", DockerServer.class); + method.setAccessible(true); +// doReturn(mockDockerImage).when(dockerControlApi).getAllImages(any(DockerServer.class)); +// List result = (List) method.invoke(dockerControlApi, dockerServer); + +// Mockito.doReturn(Collections.singletonList(mockDockerImage)) +// .when(dockerControlApi, method(DockerControlApi.class, "getAllImages", DockerServer.class)) +// .withArguments(dockerServer); expectedCreatedBuilder.containerId(BACKEND_ID); } @@ -411,14 +434,18 @@ public void invokeCreateDockerSwarmServicePrivateMethod(final DockerControlApi.N Mockito.when(mockDockerJavaClient.createServiceCmd(Mockito.any(ServiceSpec.class))).thenReturn(cmd); // Run the method - Whitebox.invokeMethod(dockerControlApi, "createDockerSwarmService", toCreate, dockerServer, numReplicas); +// Whitebox.invokeMethod(dockerControlApi, "createDockerSwarmService", toCreate, dockerServer, numReplicas); + Method method = DockerControlApi.class.getDeclaredMethod("createDockerSwarmService", Container.class, DockerServer.class, DockerControlApi.NumReplicas.class); + // 设置该方法可访问 + method.setAccessible(true); + method.invoke(dockerControlApi, toCreate, dockerServer, numReplicas); // Assert on results final ArgumentCaptor serviceSpecCaptor = ArgumentCaptor.forClass(ServiceSpec.class); Mockito.verify(mockDockerJavaClient).createServiceCmd(serviceSpecCaptor.capture()); final ServiceSpec serviceSpec = serviceSpecCaptor.getValue(); - assertThat(serviceSpec.getMode().getReplicated().getReplicas(), equalTo(new Integer(numReplicas.value).longValue())); + assertThat(serviceSpec.getMode().getReplicated().getReplicas(), equalTo((long) numReplicas.value)); } @Test diff --git a/src/test/java/org/nrg/containers/config/CommandTestConfig.java b/src/test/java/org/nrg/containers/config/CommandTestConfig.java index 84199840..a147c910 100644 --- a/src/test/java/org/nrg/containers/config/CommandTestConfig.java +++ b/src/test/java/org/nrg/containers/config/CommandTestConfig.java @@ -4,6 +4,7 @@ import org.nrg.containers.api.ContainerControlApi; import org.nrg.containers.services.ContainerEntityService; import org.nrg.framework.services.SerializerService; +import org.nrg.prefs.services.NrgPreferenceService; import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xdat.security.services.PermissionsServiceI; import org.nrg.xdat.services.AliasTokenService; @@ -30,6 +31,11 @@ public SiteConfigPreferences siteConfigPreferences() { return Mockito.mock(SiteConfigPreferences.class); } + @Bean + public NrgPreferenceService nrgPreferenceService() { + return Mockito.mock(NrgPreferenceService.class); + } + @Bean public SerializerService serializerService() { return Mockito.mock(SerializerService.class); diff --git a/src/test/java/org/nrg/containers/config/IntegrationTestConfig.java b/src/test/java/org/nrg/containers/config/IntegrationTestConfig.java index 856c594e..481d1b16 100644 --- a/src/test/java/org/nrg/containers/config/IntegrationTestConfig.java +++ b/src/test/java/org/nrg/containers/config/IntegrationTestConfig.java @@ -61,6 +61,7 @@ import org.nrg.framework.services.NrgEventService; import org.nrg.framework.services.NrgEventServiceI; import org.nrg.mail.services.MailService; +import org.nrg.prefs.services.NrgPreferenceService; import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xdat.security.services.PermissionsServiceI; import org.nrg.xdat.security.user.XnatUserProvider; @@ -91,7 +92,7 @@ @Configuration @EnableTransactionManagement -@Import({CommandConfig.class, HibernateConfig.class, RestApiTestConfig.class}) +@Import({CommandConfig.class, HibernateConfig.class, RestApiTestConfig.class, MockJmsConfig.class}) public class IntegrationTestConfig { /* Control API and dependencies + Events @@ -340,4 +341,10 @@ public LocalSessionFactoryBean sessionFactory(final DataSource dataSource, @Qual public ResourceTransactionManager transactionManager(final SessionFactory sessionFactory) throws Exception { return new HibernateTransactionManager(sessionFactory); } + + @Bean + public NrgPreferenceService nrgPreferenceService() { + return Mockito.mock(NrgPreferenceService.class); + } + } diff --git a/src/test/java/org/nrg/containers/config/JmsConfig.java b/src/test/java/org/nrg/containers/config/JmsConfig.java index b82c93ba..20f07328 100644 --- a/src/test/java/org/nrg/containers/config/JmsConfig.java +++ b/src/test/java/org/nrg/containers/config/JmsConfig.java @@ -2,16 +2,21 @@ import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.command.ActiveMQQueue; +import org.mockito.Mockito; import org.nrg.containers.jms.errors.ContainerJmsErrorHandler; import org.nrg.containers.jms.listeners.ContainerFinalizingRequestListener; import org.nrg.containers.jms.listeners.ContainerStagingRequestListener; import org.nrg.containers.jms.requests.ContainerFinalizingRequest; import org.nrg.containers.jms.requests.ContainerStagingRequest; import org.nrg.containers.services.ContainerService; +import org.nrg.framework.configuration.ConfigPaths; +import org.nrg.framework.utilities.OrderedProperties; import org.nrg.mail.services.MailService; +import org.nrg.prefs.services.NrgPreferenceService; import org.nrg.xdat.preferences.NotificationsPreferences; import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xdat.security.services.UserManagementServiceI; +import org.nrg.xdat.services.DataTypeAwareEventService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jms.annotation.EnableJms; @@ -25,6 +30,12 @@ @Configuration @EnableJms public class JmsConfig { + + @Bean + public NotificationsPreferences notificationsPreferences() { + return Mockito.mock(NotificationsPreferences.class); + } + @Bean public ContainerStagingRequestListener containerStagingRequestListener(ContainerService containerService, UserManagementServiceI mockUserManagementServiceI) { @@ -55,22 +66,25 @@ public Destination eventHandlingRequest() { @Bean(name = "finalizingQueueListenerFactory") public DefaultJmsListenerContainerFactory finalizingQueueListenerFactory(final SiteConfigPreferences siteConfigPreferences, final MailService mockMailService, - final ConnectionFactory connectionFactory) { - return defaultFactory(connectionFactory, siteConfigPreferences, mockMailService); + final ConnectionFactory connectionFactory, + final NotificationsPreferences notificationsPreferences) { + return defaultFactory(connectionFactory, siteConfigPreferences, notificationsPreferences, mockMailService); } @Bean(name = "stagingQueueListenerFactory") public DefaultJmsListenerContainerFactory stagingQueueListenerFactory(final SiteConfigPreferences siteConfigPreferences, final MailService mockMailService, - final ConnectionFactory connectionFactory) { - return defaultFactory(connectionFactory, siteConfigPreferences, mockMailService); + final ConnectionFactory connectionFactory, + final NotificationsPreferences notificationsPreferences) { + return defaultFactory(connectionFactory, siteConfigPreferences, notificationsPreferences, mockMailService); } @Bean(name = "eventHandlingQueueListenerFactory") public DefaultJmsListenerContainerFactory eventHandlingQueueListenerFactory(final SiteConfigPreferences siteConfigPreferences, final MailService mailService, - final ConnectionFactory connectionFactory) { - return defaultFactory(connectionFactory, siteConfigPreferences, mailService); + final ConnectionFactory connectionFactory, + final NotificationsPreferences notificationsPreferences) { + return defaultFactory(connectionFactory, siteConfigPreferences, notificationsPreferences, mailService); } @Bean @@ -85,6 +99,11 @@ public ConnectionFactory connectionFactory() { return new CachingConnectionFactory(mq); } + @Bean + public NrgPreferenceService nrgPreferenceService() { + return Mockito.mock(NrgPreferenceService.class); + } + private DefaultJmsListenerContainerFactory defaultFactory(ConnectionFactory connectionFactory, final SiteConfigPreferences siteConfigPreferences, final NotificationsPreferences notificationsPreferences, diff --git a/src/test/java/org/nrg/containers/config/LaunchRestApiTestConfig.java b/src/test/java/org/nrg/containers/config/LaunchRestApiTestConfig.java index 5955d77f..4933de73 100644 --- a/src/test/java/org/nrg/containers/config/LaunchRestApiTestConfig.java +++ b/src/test/java/org/nrg/containers/config/LaunchRestApiTestConfig.java @@ -11,6 +11,7 @@ import org.nrg.framework.services.ContextService; import org.nrg.framework.services.NrgEventServiceI; import org.nrg.framework.services.SerializerService; +import org.nrg.prefs.services.NrgPreferenceService; import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xdat.security.services.PermissionsServiceI; import org.nrg.xdat.security.services.RoleHolder; @@ -83,6 +84,11 @@ public SiteConfigPreferences siteConfigPreferences() { return Mockito.mock(SiteConfigPreferences.class); } + @Bean + public NrgPreferenceService nrgPreferenceService() { + return Mockito.mock(NrgPreferenceService.class); + } + @Bean public SerializerService serializerService() { return Mockito.mock(SerializerService.class); diff --git a/src/test/java/org/nrg/containers/config/MockJmsConfig.java b/src/test/java/org/nrg/containers/config/MockJmsConfig.java index 20c528b9..2525f2d1 100644 --- a/src/test/java/org/nrg/containers/config/MockJmsConfig.java +++ b/src/test/java/org/nrg/containers/config/MockJmsConfig.java @@ -17,8 +17,8 @@ import javax.jms.Destination; import java.util.concurrent.ExecutorService; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; diff --git a/src/test/java/org/nrg/containers/config/QueueConsumerTestConfig.java b/src/test/java/org/nrg/containers/config/QueueConsumerTestConfig.java index 75511dbb..102fe2f8 100644 --- a/src/test/java/org/nrg/containers/config/QueueConsumerTestConfig.java +++ b/src/test/java/org/nrg/containers/config/QueueConsumerTestConfig.java @@ -33,9 +33,11 @@ import org.nrg.framework.services.NrgEventServiceI; import org.nrg.mail.services.MailService; import org.nrg.mail.services.impl.SpringBasedMailServiceImpl; +import org.nrg.prefs.services.NrgPreferenceService; import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xdat.security.services.PermissionsServiceI; import org.nrg.xdat.services.AliasTokenService; +import org.nrg.xdat.services.cache.UserDataCache; import org.nrg.xnat.services.XnatAppInfo; import org.nrg.xnat.services.archive.CatalogService; import org.springframework.beans.factory.annotation.Qualifier; @@ -194,6 +196,11 @@ public SiteConfigPreferences siteConfigPreferences() { return Mockito.mock(SiteConfigPreferences.class); } + @Bean + public NrgPreferenceService fakePrefsService() { + return Mockito.mock(NrgPreferenceService.class); + } + @Bean public ConfigService configService() { return Mockito.mock(ConfigService.class); diff --git a/src/test/java/org/nrg/containers/config/QueueSettingsRestApiTestConfig.java b/src/test/java/org/nrg/containers/config/QueueSettingsRestApiTestConfig.java index 19c4f0b4..ba97007b 100644 --- a/src/test/java/org/nrg/containers/config/QueueSettingsRestApiTestConfig.java +++ b/src/test/java/org/nrg/containers/config/QueueSettingsRestApiTestConfig.java @@ -2,8 +2,8 @@ import org.mockito.Mockito; import org.nrg.containers.jms.preferences.QueuePrefsBean; -import org.nrg.containers.model.xnat.FakePrefsService; import org.nrg.containers.jms.rest.QueueSettingsRestApi; +import org.nrg.containers.model.xnat.FakePrefsService; import org.nrg.containers.rest.QueueSettingsRestApiTest; import org.nrg.framework.services.ContextService; import org.nrg.framework.services.SerializerService; @@ -12,12 +12,10 @@ import org.nrg.xdat.security.services.RoleHolder; import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xdat.services.AliasTokenService; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.jms.config.DefaultJmsListenerContainerFactory; import org.springframework.jms.config.JmsListenerEndpointRegistry; import org.springframework.security.authentication.TestingAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; @@ -25,13 +23,18 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import javax.jms.ConnectionFactory; - @Configuration @EnableWebMvc @EnableWebSecurity @Import({RestApiTestConfig.class, ObjectMapperConfig.class}) public class QueueSettingsRestApiTestConfig extends WebSecurityConfigurerAdapter { + + @Bean + public JmsListenerEndpointRegistry jmsListenerEndpointRegistry() { + return Mockito.mock(JmsListenerEndpointRegistry.class); + } + + @Bean public QueueSettingsRestApi queueSettingsRestApi(QueuePrefsBean queuePrefsBean, final UserManagementServiceI mockUserManagementServiceI, @@ -52,28 +55,8 @@ public SerializerService serializerService() { @Bean public NrgPreferenceService fakePrefsService() { - return new FakePrefsService(QueueSettingsRestApiTest.TOOL_ID, - QueueSettingsRestApiTest.PREF_MAP); - } - - private DefaultJmsListenerContainerFactory defaultMockFactory() { - DefaultJmsListenerContainerFactory factory = Mockito.spy(new DefaultJmsListenerContainerFactory()); - ConnectionFactory connectionFactory = Mockito.mock(ConnectionFactory.class); - factory.setConnectionFactory(connectionFactory); - factory.setConcurrency(ContainersConfig.QUEUE_MIN_CONCURRENCY_DFLT + "-" + ContainersConfig.QUEUE_MAX_CONCURRENCY_DFLT); - return factory; - } - - @Bean - @Qualifier("finalizingQueueListenerFactory") - public DefaultJmsListenerContainerFactory finalizingQueueListenerFactory() { - return defaultMockFactory(); - } - - @Bean - @Qualifier("stagingQueueListenerFactory") - public DefaultJmsListenerContainerFactory stagingQueueListenerFactory() { - return defaultMockFactory(); + return new FakePrefsService(QueueSettingsRestApiTest.QUEUE_PREFS_BEAN_TOOL_ID, + QueueSettingsRestApiTest.DEFAULT_CONCURRENCY_PREFERENCES); } @Bean diff --git a/src/test/java/org/nrg/containers/config/RestApiTestConfig.java b/src/test/java/org/nrg/containers/config/RestApiTestConfig.java index e3794147..8caf439c 100644 --- a/src/test/java/org/nrg/containers/config/RestApiTestConfig.java +++ b/src/test/java/org/nrg/containers/config/RestApiTestConfig.java @@ -23,7 +23,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import static org.powermock.api.mockito.PowerMockito.when; +import static org.mockito.Mockito.when; @Configuration @Import({ObjectMapperConfig.class}) diff --git a/src/test/java/org/nrg/containers/jms/JmsConsumerExceptionTest.java b/src/test/java/org/nrg/containers/jms/JmsConsumerExceptionTest.java index a86194fd..d955add6 100644 --- a/src/test/java/org/nrg/containers/jms/JmsConsumerExceptionTest.java +++ b/src/test/java/org/nrg/containers/jms/JmsConsumerExceptionTest.java @@ -1,9 +1,11 @@ package org.nrg.containers.jms; import lombok.extern.slf4j.Slf4j; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.nrg.containers.config.IntegrationTestConfig; import org.nrg.containers.config.JmsConfig; @@ -22,7 +24,6 @@ import org.nrg.xft.ItemI; import org.nrg.xft.XFTItem; import org.nrg.xft.event.EventDetails; -import org.nrg.xft.event.EventMetaI; import org.nrg.xft.event.persist.PersistentWorkflowI; import org.nrg.xft.event.persist.PersistentWorkflowUtils; import org.nrg.xft.schema.XFTManager; @@ -30,11 +31,6 @@ import org.nrg.xnat.helpers.uri.UriParserUtils; import org.nrg.xnat.services.archive.CatalogService; import org.nrg.xnat.utils.WorkflowUtils; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.modules.junit4.PowerMockRunnerDelegate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; @@ -43,24 +39,25 @@ import java.util.Collections; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.contains; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.mockStatic; @Slf4j -@RunWith(PowerMockRunner.class) -@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class) -@PrepareForTest({UriParserUtils.class, XFTManager.class, Users.class, WorkflowUtils.class, - PersistentWorkflowUtils.class}) -@PowerMockIgnore({"org.apache.*", "java.*", "javax.*", "org.w3c.*", "com.sun.*"}) +@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {JmsConfig.class, IntegrationTestConfig.class}) @Transactional public class JmsConsumerExceptionTest { + private MockedStatic mockedWorkflowUtils; + private MockedStatic mockedUsers; + private MockedStatic mockedXFTManager; + private MockedStatic mockedUriParserUtils; + private MockedStatic mockedPersistentWorkflowUtils; + @Autowired private SiteConfigPreferences mockSiteConfigPreferences; @Autowired private UserManagementServiceI mockUserManagementServiceI; @Autowired private PermissionsServiceI mockPermissionsServiceI; @@ -83,11 +80,19 @@ public class JmsConsumerExceptionTest { public void setup() throws Exception { fakeWorkflow = new FakeWorkflow(); + mockedWorkflowUtils = Mockito.mockStatic(WorkflowUtils.class); + mockedUsers = mockStatic(Users.class); + mockedXFTManager = Mockito.mockStatic(XFTManager.class); + mockedUriParserUtils = Mockito.mockStatic(UriParserUtils.class); + mockedPersistentWorkflowUtils = Mockito.mockStatic(PersistentWorkflowUtils.class); + mockUser = mock(UserI.class); when(mockUser.getLogin()).thenReturn(FAKE_USER); when(mockUser.getEmail()).thenReturn(FAKE_EMAIL); - mockStatic(Users.class); - when(Users.getUser(FAKE_USER)).thenReturn(mockUser); + +// mockStatic(Users.class); +// when(Users.getUser(FAKE_USER)).thenReturn(mockUser); + mockedUsers.when(() -> Users.getUser(FAKE_USER)).thenReturn(mockUser); // Mock the site config preferences when(mockSiteConfigPreferences.getProperty("processingUrl", FAKE_HOST)).thenReturn(FAKE_HOST); @@ -107,22 +112,29 @@ public void setup() throws Exception { mockAliasToken.setSecret("secret"); when(mockAliasTokenService.issueTokenForUser(mockUser)).thenReturn(mockAliasToken); - // Mock UriParserUtils using PowerMock. This allows us to mock out - // the responses to its static method parseURI(). - PowerMockito.mockStatic(UriParserUtils.class); - // Use powermock to mock out the static method XFTManager.isInitialized() - PowerMockito.mockStatic(XFTManager.class); - when(XFTManager.isInitialized()).thenReturn(true); +// PowerMockito.mockStatic(XFTManager.class); +// when(XFTManager.isInitialized()).thenReturn(true); + mockedXFTManager.when(XFTManager::isInitialized).thenReturn(true); // Also mock out workflow operations to return our fake workflow object - PowerMockito.mockStatic(WorkflowUtils.class); - when(WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) +// PowerMockito.mockStatic(WorkflowUtils.class); +// when(WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) + mockedWorkflowUtils.when(() -> WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) + .thenReturn(fakeWorkflow); + +// PowerMockito.doNothing().when(WorkflowUtils.class, "save", any(PersistentWorkflowI.class), isNull(EventMetaI.class)); + mockedWorkflowUtils.when(() -> WorkflowUtils.save(Mockito.any(), Mockito.isNull())).thenAnswer(invocation -> null); + +// PowerMockito.spy(PersistentWorkflowUtils.class); +// PowerMockito.doReturn(fakeWorkflow).when(PersistentWorkflowUtils.class, "getOrCreateWorkflowData", eq(FakeWorkflow.defaultEventId), +// eq(mockUser), any(XFTItem.class), any(EventDetails.class)); + mockedPersistentWorkflowUtils.when(() -> PersistentWorkflowUtils.getOrCreateWorkflowData( + eq(FakeWorkflow.defaultEventId), eq(mockUser), any(XFTItem.class), any(EventDetails.class))) .thenReturn(fakeWorkflow); - PowerMockito.doNothing().when(WorkflowUtils.class, "save", any(PersistentWorkflowI.class), isNull(EventMetaI.class)); - PowerMockito.spy(PersistentWorkflowUtils.class); - PowerMockito.doReturn(fakeWorkflow).when(PersistentWorkflowUtils.class, "getOrCreateWorkflowData", eq(FakeWorkflow.defaultEventId), - eq(mockUser), any(XFTItem.class), any(EventDetails.class)); + + PersistentWorkflowUtils.getOrCreateWorkflowData( + FakeWorkflow.defaultEventId, mockUser, Mockito.mock(XFTItem.class), Mockito.mock(EventDetails.class)); // mock external FS check when(mockCatalogService.hasRemoteFiles(eq(mockUser), any(String.class))).thenReturn(false); @@ -137,20 +149,31 @@ public void setup() throws Exception { .name("placeholder") .build()) .build()); - wrapper = command.xnatCommandWrappers().get(0); + wrapper = command.xnatCommandWrappers().getFirst(); TestingUtils.commitTransaction(); fakeWorkflow.setId(FAKE_ID); fakeWorkflow.setPipelineName(wrapper.name()); } + @After + public void tearDownStaticMocks() { + mockedUriParserUtils.closeOnDemand(); + mockedXFTManager.closeOnDemand(); + mockedUsers.closeOnDemand(); + mockedWorkflowUtils.closeOnDemand(); + } + @Test @DirtiesContext + //CS-1031 public void testStagingConsumeFailure() throws Exception { // setup jmsTemplate to throw exception String exceptionMsg = "my tricky exception message"; - PowerMockito.when(WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) +// PowerMockito.when(WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) +// .thenThrow(new RuntimeException(exceptionMsg)); + mockedWorkflowUtils.when(() -> WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) .thenThrow(new RuntimeException(exceptionMsg)); containerService.queueResolveCommandAndLaunchContainer(null, wrapper.id(), 0L, wrapper.name(), Collections.emptyMap(), mockUser, fakeWorkflow diff --git a/src/test/java/org/nrg/containers/jms/JmsExceptionIntegrationTest.java b/src/test/java/org/nrg/containers/jms/JmsExceptionIntegrationTest.java index 57ba7e69..4c2d0fc3 100644 --- a/src/test/java/org/nrg/containers/jms/JmsExceptionIntegrationTest.java +++ b/src/test/java/org/nrg/containers/jms/JmsExceptionIntegrationTest.java @@ -5,9 +5,12 @@ import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; -import org.mockito.Matchers; +import org.mockito.ArgumentMatchers; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.nrg.containers.api.DockerControlApi; import org.nrg.containers.config.EventPullingIntegrationTestConfig; @@ -28,7 +31,6 @@ import org.nrg.xft.ItemI; import org.nrg.xft.XFTItem; import org.nrg.xft.event.EventDetails; -import org.nrg.xft.event.EventMetaI; import org.nrg.xft.event.persist.PersistentWorkflowI; import org.nrg.xft.event.persist.PersistentWorkflowUtils; import org.nrg.xft.schema.XFTManager; @@ -36,11 +38,6 @@ import org.nrg.xnat.helpers.uri.UriParserUtils; import org.nrg.xnat.services.archive.CatalogService; import org.nrg.xnat.utils.WorkflowUtils; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.modules.junit4.PowerMockRunnerDelegate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jms.core.JmsTemplate; import org.springframework.jms.core.MessagePostProcessor; @@ -59,24 +56,18 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.mockito.AdditionalMatchers.aryEq; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyMapOf; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; @Slf4j -@RunWith(PowerMockRunner.class) -@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class) -@PrepareForTest({UriParserUtils.class, XFTManager.class, Users.class, WorkflowUtils.class, - PersistentWorkflowUtils.class}) -@PowerMockIgnore({"org.apache.*", "java.*", "javax.*", "org.w3c.*", "com.sun.*"}) +@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = EventPullingIntegrationTestConfig.class) @Transactional public class JmsExceptionIntegrationTest { + private MockedStatic mockedWorkflowUtils; + private MockedStatic mockedUsers; + private MockedStatic mockedXFTManager; + private MockedStatic mockedUriParserUtils; @Autowired private JmsTemplate mockJmsTemplate; @Autowired private SiteConfigPreferences mockSiteConfigPreferences; @Autowired private UserManagementServiceI mockUserManagementServiceI; @@ -119,8 +110,7 @@ public void setup() throws Exception { mockUser = mock(UserI.class); when(mockUser.getLogin()).thenReturn(FAKE_USER); when(mockUser.getEmail()).thenReturn(FAKE_EMAIL); - mockStatic(Users.class); - when(Users.getUser(FAKE_USER)).thenReturn(mockUser); + mockedUsers.when(() -> Users.getUser(FAKE_USER)).thenReturn(mockUser); // Mock the site config preferences String buildDir = folder.newFolder().getAbsolutePath(); @@ -143,23 +133,12 @@ public void setup() throws Exception { mockAliasToken.setAlias("alias"); mockAliasToken.setSecret("secret"); when(mockAliasTokenService.issueTokenForUser(mockUser)).thenReturn(mockAliasToken); - - // Mock UriParserUtils using PowerMock. This allows us to mock out - // the responses to its static method parseURI(). - PowerMockito.mockStatic(UriParserUtils.class); - - // Use powermock to mock out the static method XFTManager.isInitialized() - PowerMockito.mockStatic(XFTManager.class); - when(XFTManager.isInitialized()).thenReturn(true); - - // Also mock out workflow operations to return our fake workflow object - PowerMockito.mockStatic(WorkflowUtils.class); - when(WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) + mockedXFTManager.when(XFTManager::isInitialized).thenReturn(true); + mockedWorkflowUtils.when(() -> WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) .thenReturn(fakeWorkflow); - PowerMockito.doNothing().when(WorkflowUtils.class, "save", any(PersistentWorkflowI.class), isNull(EventMetaI.class)); - PowerMockito.spy(PersistentWorkflowUtils.class); - PowerMockito.doReturn(fakeWorkflow).when(PersistentWorkflowUtils.class, "getOrCreateWorkflowData", eq(FakeWorkflow.defaultEventId), - eq(mockUser), any(XFTItem.class), any(EventDetails.class)); + Mockito.doNothing().when(WorkflowUtils.class); + Mockito.spy(PersistentWorkflowUtils.class); + Mockito.doReturn(fakeWorkflow).when(PersistentWorkflowUtils.class); // mock external FS check when(mockCatalogService.hasRemoteFiles(eq(mockUser), any(String.class))).thenReturn(false); @@ -181,6 +160,22 @@ public void setup() throws Exception { fakeWorkflow.setPipelineName(wrapper.name()); } + @BeforeEach + void setUpStaticMocks() { + mockedWorkflowUtils = Mockito.mockStatic(WorkflowUtils.class); + mockedUsers = mockStatic(Users.class); + mockedXFTManager = Mockito.mockStatic(XFTManager.class); + mockedUriParserUtils = Mockito.mockStatic(UriParserUtils.class); + } + + @AfterEach + void tearDownStaticMocks() { + mockedUriParserUtils.closeOnDemand(); + mockedXFTManager.closeOnDemand(); + mockedUsers.closeOnDemand(); + mockedWorkflowUtils.closeOnDemand(); + } + @Test @DirtiesContext public void testStagingQueueFailure() throws Exception { @@ -196,10 +191,10 @@ public void testStagingQueueFailure() throws Exception { assertThat(fakeWorkflow.getDetails(), is(exceptionMsg)); Mockito.verify(mockMailService, times(1)).sendHtmlMessage(eq(FAKE_EMAIL), - aryEq(new String[]{FAKE_EMAIL}), aryEq(new String[]{FAKE_EMAIL}), Matchers.eq(null), + aryEq(new String[]{FAKE_EMAIL}), aryEq(new String[]{FAKE_EMAIL}), ArgumentMatchers.eq(null), Mockito.matches(".*" + wrapper.name() + ".*Failed.*"), Mockito.matches(".*" + wrapper.name() + ".*" + FAKE_ID + ".*failed.*"), Mockito.matches(".*" + wrapper.name() + ".*" + FAKE_ID + ".*failed.*"), - anyMapOf(String.class, File.class)); + anyMap()); } } diff --git a/src/test/java/org/nrg/containers/model/CommandConfigurationTest.java b/src/test/java/org/nrg/containers/model/CommandConfigurationTest.java index ffaf078c..8ff5fca1 100644 --- a/src/test/java/org/nrg/containers/model/CommandConfigurationTest.java +++ b/src/test/java/org/nrg/containers/model/CommandConfigurationTest.java @@ -23,10 +23,10 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNotNull; -import static org.mockito.Matchers.isNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNotNull; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.when; import static org.nrg.containers.services.ContainerConfigService.TOOL_ID; @@ -56,7 +56,7 @@ public void testConfigureCommandForSite() throws Exception { final Configuration mockSiteConfiguration = Mockito.mock(Configuration.class); when(mockSiteConfiguration.getContents()).thenReturn(siteJson); - when(mockConfigService.getConfig(eq(TOOL_ID), anyString(), eq(Scope.Site), isNull(String.class))).thenReturn(mockSiteConfiguration); + when(mockConfigService.getConfig(eq(TOOL_ID), anyString(), eq(Scope.Site), isNull())).thenReturn(mockSiteConfiguration); final CommandConfigurationInternal retrieved = containerConfigService.getSiteConfiguration(WRAPPER_ID); assertThat(retrieved, is(site)); @@ -132,7 +132,7 @@ public void testConfigureCommandForProject() throws Exception { final Configuration mockSiteConfiguration = Mockito.mock(Configuration.class); when(mockSiteConfiguration.getContents()).thenReturn(siteJson); - when(mockConfigService.getConfig(eq(TOOL_ID), anyString(), eq(Scope.Site), isNull(String.class))).thenReturn(mockSiteConfiguration); + when(mockConfigService.getConfig(eq(TOOL_ID), anyString(), eq(Scope.Site), isNull())).thenReturn(mockSiteConfiguration); final Configuration mockProjectConfiguration = Mockito.mock(Configuration.class); when(mockProjectConfiguration.getContents()).thenReturn(projectJson); when(mockConfigService.getConfig(eq(TOOL_ID), anyString(), eq(Scope.Project), isNotNull(String.class))).thenReturn(mockProjectConfiguration); diff --git a/src/test/java/org/nrg/containers/rest/CommandConfigurationRestApiTest.java b/src/test/java/org/nrg/containers/rest/CommandConfigurationRestApiTest.java index 4d702eff..98ae3952 100644 --- a/src/test/java/org/nrg/containers/rest/CommandConfigurationRestApiTest.java +++ b/src/test/java/org/nrg/containers/rest/CommandConfigurationRestApiTest.java @@ -39,9 +39,9 @@ import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -54,14 +54,13 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(classes = CommandConfigurationRestApiTestConfig.class) public class CommandConfigurationRestApiTest { - private UserI mockAdmin; + private UserI mockContainerManager; private Authentication authentication; private MockMvc mockMvc; private CommandConfiguration commandConfiguration; @@ -94,19 +93,21 @@ public class CommandConfigurationRestApiTest { @Rule public TemporaryFolder folder = new TemporaryFolder(new File("/tmp")); @Before + //CS-1031 public void setup() throws Exception { mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build(); // Mock the userI - mockAdmin = Mockito.mock(UserI.class); - when(mockAdmin.getLogin()).thenReturn(FAKE_USERNAME); - when(mockAdmin.getPassword()).thenReturn(FAKE_PASSWORD); - when(mockRoleService.isSiteAdmin(mockAdmin)).thenReturn(true); + mockContainerManager = Mockito.mock(UserI.class); + when(mockContainerManager.getLogin()).thenReturn(FAKE_USERNAME); + when(mockContainerManager.getPassword()).thenReturn(FAKE_PASSWORD); + when(mockRoleService.checkRole(mockContainerManager, "ContainerManager")).thenReturn(true); + when(mockRoleService.checkRole(mockContainerManager, "Privileged")).thenReturn(true); - authentication = new TestingAuthenticationToken(mockAdmin, FAKE_PASSWORD); + authentication = new TestingAuthenticationToken(mockContainerManager, FAKE_PASSWORD); // Mock the user management service - when(mockUserManagementServiceI.getUser(FAKE_USERNAME)).thenReturn(mockAdmin); + when(mockUserManagementServiceI.getUser(FAKE_USERNAME)).thenReturn(mockContainerManager); // Command and wrapper final long commandId = 0L; @@ -189,7 +190,7 @@ public void testCreateSiteConfig() throws Exception { eq(configPath), eq(commandConfigurationInternalJson), eq(Scope.Site), - isNull(String.class))) + isNull())) .thenReturn(null); final MockHttpServletRequestBuilder request = @@ -211,7 +212,7 @@ public void testGetSiteConfig() throws Exception { eq(ContainerConfigService.TOOL_ID), eq(configPath), eq(Scope.Site), - isNull(String.class) + isNull() )) .thenReturn(mockConfig); @@ -239,7 +240,7 @@ public void testDeleteSiteConfig() throws Exception { eq(ContainerConfigService.TOOL_ID), eq(configPath), eq(Scope.Site), - isNull(String.class) + isNull() )) .thenReturn(mockConfig); doNothing().when(mockConfigService).delete(mockConfig); @@ -336,7 +337,7 @@ public void testEnableForSite() throws Exception { eq(ContainerConfigService.TOOL_ID), eq(configPath), eq(Scope.Site), - isNull(String.class) + isNull() )) .thenReturn(mockConfig); @@ -366,7 +367,7 @@ public void testDisableForSite() throws Exception { eq(ContainerConfigService.TOOL_ID), eq(configPath), eq(Scope.Site), - isNull(String.class) + isNull() )) .thenReturn(mockConfig); diff --git a/src/test/java/org/nrg/containers/rest/CommandRestApiTest.java b/src/test/java/org/nrg/containers/rest/CommandRestApiTest.java index caa20cf2..a200617f 100644 --- a/src/test/java/org/nrg/containers/rest/CommandRestApiTest.java +++ b/src/test/java/org/nrg/containers/rest/CommandRestApiTest.java @@ -12,6 +12,7 @@ import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.nrg.config.services.ConfigService; import org.nrg.containers.config.CommandRestApiTestConfig; import org.nrg.containers.model.command.auto.Command; @@ -27,7 +28,6 @@ import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xdat.services.AliasTokenService; import org.nrg.xft.security.UserI; -import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.security.authentication.TestingAuthenticationToken; @@ -70,12 +70,12 @@ @Slf4j @RunWith(SpringJUnit4ClassRunner.class) -@PowerMockIgnore({"org.apache.*", "java.*", "javax.*", "org.w3c.*", "com.sun.*"}) @WebAppConfiguration @Transactional @ContextConfiguration(classes = CommandRestApiTestConfig.class) public class CommandRestApiTest { private Authentication ADMIN_AUTH; + private Authentication CONTAINER_MANAGER_AUTH; private Authentication NONADMIN_AUTH; private MockMvc mockMvc; @@ -112,6 +112,7 @@ protected void finished(Description description) { }; @Before + //CS-1031 public void setup() throws Exception { mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build(); @@ -131,6 +132,16 @@ public void setup() throws Exception { when(mockUserManagementServiceI.getUser(adminUsername)).thenReturn(admin); ADMIN_AUTH = new TestingAuthenticationToken(admin, adminPassword); + final UserI containerManager = Mockito.mock(UserI.class); + final String cManagerUsername = "containerManager"; + final String cManagerPassword = "cmanager-pass"; + when(containerManager.getLogin()).thenReturn(cManagerUsername); + when(containerManager.getPassword()).thenReturn(cManagerPassword); + when(mockRoleService.checkRole(containerManager, "ContainerManager")).thenReturn(true); + when(mockRoleService.checkRole(containerManager, "Privileged")).thenReturn(true); + CONTAINER_MANAGER_AUTH = new TestingAuthenticationToken(containerManager, cManagerPassword); + + final UserI nonAdmin = mock(UserI.class); final String nonAdminUsername = "non-admin"; final String nonAdminPassword = "non-admin-pass"; @@ -260,7 +271,7 @@ public void testCreate() throws Exception { final MockHttpServletRequestBuilder request = post(path).content(commandJson) .contentType(JSON) - .with(authentication(ADMIN_AUTH)) + .with(authentication(CONTAINER_MANAGER_AUTH)) .with(csrf()) .with(testSecurityContext()); @@ -289,7 +300,7 @@ public void testCreate() throws Exception { // No 'Content-type' header final MockHttpServletRequestBuilder noContentType = post(path).content(commandJson) - .with(authentication(ADMIN_AUTH)) + .with(authentication(CONTAINER_MANAGER_AUTH)) .with(csrf()) .with(testSecurityContext()); mockMvc.perform(noContentType) @@ -300,7 +311,7 @@ public void testCreate() throws Exception { post(path).content(commandJson) .contentType(JSON) .accept(XML) - .with(authentication(ADMIN_AUTH)) + .with(authentication(CONTAINER_MANAGER_AUTH)) .with(csrf()) .with(testSecurityContext()); mockMvc.perform(badAccept) @@ -310,7 +321,7 @@ public void testCreate() throws Exception { final String blankCommand = "{\"type\": \"docker\"}"; final MockHttpServletRequestBuilder blankCommandRequest = post(path).content(blankCommand).contentType(JSON) - .with(authentication(ADMIN_AUTH)) + .with(authentication(CONTAINER_MANAGER_AUTH)) .with(csrf()) .with(testSecurityContext()); final String blankCommandResponse = @@ -339,7 +350,7 @@ public void testDelete() throws Exception { final String path = String.format(pathTemplate, command.id()); final MockHttpServletRequestBuilder request = delete(path) - .with(authentication(ADMIN_AUTH)) + .with(authentication(CONTAINER_MANAGER_AUTH)) .with(csrf()) .with(testSecurityContext()); @@ -370,7 +381,7 @@ public void testAddWrapper() throws Exception { final MockHttpServletRequestBuilder request = post(path).content(commandWrapperJson) .contentType(JSON) - .with(authentication(ADMIN_AUTH)) + .with(authentication(CONTAINER_MANAGER_AUTH)) .with(csrf()) .with(testSecurityContext()); @@ -407,7 +418,7 @@ public void testAddWrapper() throws Exception { final String blankWrapper = "{}"; final MockHttpServletRequestBuilder blankCommandRequest = post(path).content(blankWrapper).contentType(JSON) - .with(authentication(ADMIN_AUTH)) + .with(authentication(CONTAINER_MANAGER_AUTH)) .with(csrf()) .with(testSecurityContext()); final String blankCommandResponse = @@ -448,7 +459,7 @@ public void testUpdateWrapper() throws Exception { final MockHttpServletRequestBuilder request = post(path).content(mapper.writeValueAsString(updates)) .contentType(JSON) - .with(authentication(ADMIN_AUTH)) + .with(authentication(CONTAINER_MANAGER_AUTH)) .with(csrf()) .with(testSecurityContext()); @@ -484,7 +495,7 @@ public void testDeleteWrapper() throws Exception { final MockHttpServletRequestBuilder request = delete(path) - .with(authentication(ADMIN_AUTH)) + .with(authentication(CONTAINER_MANAGER_AUTH)) .with(csrf()) .with(testSecurityContext()); @@ -512,7 +523,7 @@ public void testCreateEcatHeaderDump() throws Exception { final MockHttpServletRequestBuilder request = post(path).content(commandJson) .contentType(JSON) - .with(authentication(ADMIN_AUTH)) + .with(authentication(CONTAINER_MANAGER_AUTH)) .with(csrf()) .with(testSecurityContext()); @@ -543,7 +554,7 @@ public void testSpringUnhelpfulError() throws Exception { final MockHttpServletRequestBuilder badInputTypeCommandRequest = post(path).content(badInputTypeJson).contentType(JSON) - .with(authentication(ADMIN_AUTH)) + .with(authentication(CONTAINER_MANAGER_AUTH)) .with(csrf()) .with(testSecurityContext()); diff --git a/src/test/java/org/nrg/containers/rest/ContainerRestApiTest.java b/src/test/java/org/nrg/containers/rest/ContainerRestApiTest.java index 7e47b206..01016fec 100644 --- a/src/test/java/org/nrg/containers/rest/ContainerRestApiTest.java +++ b/src/test/java/org/nrg/containers/rest/ContainerRestApiTest.java @@ -6,6 +6,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.nrg.containers.config.ContainerRestApiTestConfig; @@ -14,14 +16,9 @@ import org.nrg.containers.model.server.docker.Backend; import org.nrg.containers.services.ContainerEntityService; import org.nrg.containers.services.ContainerService; -import org.nrg.xdat.security.helpers.Permissions; import org.nrg.xdat.security.services.RoleServiceI; import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xft.security.UserI; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.modules.junit4.PowerMockRunnerDelegate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.security.authentication.TestingAuthenticationToken; @@ -41,26 +38,16 @@ import java.util.Set; import java.util.stream.Collectors; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.testSecurityContext; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@RunWith(PowerMockRunner.class) -@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class) -@PrepareForTest({Permissions.class}) -@PowerMockIgnore({"org.apache.*", "java.*", "javax.*", "org.w3c.*", "com.sun.*"}) +@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @Transactional @ContextConfiguration(classes = ContainerRestApiTestConfig.class) @@ -125,6 +112,14 @@ public void setup() throws Exception { } } + @BeforeEach + void setUpStaticMocks() { + } + + @AfterEach + void tearDownStaticMocks() { + } + @Test public void testSpringConfiguration() { assertThat(containerService, not(nullValue())); diff --git a/src/test/java/org/nrg/containers/rest/LaunchRestApiTest.java b/src/test/java/org/nrg/containers/rest/LaunchRestApiTest.java index 167a7c2e..7f79509e 100644 --- a/src/test/java/org/nrg/containers/rest/LaunchRestApiTest.java +++ b/src/test/java/org/nrg/containers/rest/LaunchRestApiTest.java @@ -51,11 +51,11 @@ import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyMapOf; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; import static org.nrg.containers.model.server.docker.DockerServerBase.DockerServer; @@ -97,7 +97,7 @@ public class LaunchRestApiTest { private CommandWrapper COMMAND_WRAPPER; private ResolvedCommand RESOLVED_COMMAND; - private final MediaType JSON = MediaType.APPLICATION_JSON_UTF8; + private final MediaType JSON = MediaType.APPLICATION_JSON; private final MediaType XML = MediaType.APPLICATION_XML; @Autowired private WebApplicationContext wac; @@ -205,7 +205,7 @@ public void testLaunchWithQueryParams() throws Exception { when(mockContainerService.launchContainer( anyString(), any(Long.class), anyString(), eq(WRAPPER_ID), eq(FAKE_ROOT), - anyMapOf(String.class, String.class), any(UserI.class)) + any(Map.class), any(UserI.class)) ).thenReturn(report); final String path = String.format(pathTemplate, WRAPPER_ID, FAKE_ROOT); @@ -232,7 +232,7 @@ public void testLaunchWithParamsInBody() throws Exception { when(mockContainerService.launchContainer( any(String.class), any(Long.class), any(String.class), eq(WRAPPER_ID), eq(FAKE_ROOT), - anyMapOf(String.class, String.class), any(UserI.class)) + any(Map.class), any(UserI.class)) ).thenReturn(report); final String path = String.format(pathTemplate, WRAPPER_ID, FAKE_ROOT); @@ -276,7 +276,7 @@ public void testBulkLaunch() throws Exception { when(mockContainerService.bulkLaunch( any(String.class), any(Long.class), any(String.class), eq(WRAPPER_ID), eq(FAKE_ROOT), - anyMapOf(String.class, String.class), any(UserI.class) + anyMap(), any(UserI.class) )).thenReturn(report); //final String exceptionMessage = "Unable to queue container launch"; @@ -326,7 +326,7 @@ public void testGetLaunchUI() throws Exception { .commandName(COMMAND_NAME) .image(IMAGE) .build(); - when(mockCommandResolutionService.preResolve(eq(project), any(long.class), any(String.class), eq(WRAPPER_ID), anyMapOf(String.class, String.class), eq(mockAdmin))) + when(mockCommandResolutionService.preResolve(eq(project), any(long.class), any(String.class), eq(WRAPPER_ID), anyMap(), eq(mockAdmin))) .thenReturn(partiallyResolvedCommand); final LaunchUi expectedLaunchUi = LaunchUi.create(partiallyResolvedCommand, diff --git a/src/test/java/org/nrg/containers/rest/QueueSettingsRestApiTest.java b/src/test/java/org/nrg/containers/rest/QueueSettingsRestApiTest.java index 96d5dea3..d6879ad0 100644 --- a/src/test/java/org/nrg/containers/rest/QueueSettingsRestApiTest.java +++ b/src/test/java/org/nrg/containers/rest/QueueSettingsRestApiTest.java @@ -18,10 +18,10 @@ import org.nrg.xdat.security.services.UserManagementServiceI; import org.nrg.xdat.services.AliasTokenService; import org.nrg.xft.security.UserI; -import org.powermock.reflect.Whitebox; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; -import org.springframework.jms.config.DefaultJmsListenerContainerFactory; +import org.springframework.jms.config.JmsListenerEndpointRegistry; +import org.springframework.jms.listener.DefaultMessageListenerContainer; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.test.annotation.DirtiesContext; @@ -34,12 +34,17 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.util.NestedServletException; +import javax.jms.ConnectionFactory; +import javax.jms.Destination; import java.io.File; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; +import static org.awaitility.Awaitility.await; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; @@ -48,236 +53,276 @@ import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(classes = QueueSettingsRestApiTestConfig.class) public class QueueSettingsRestApiTest { - private UserI mockAdmin; private Authentication authentication; private MockMvc mockMvc; - public static final String TOOL_ID = "jms-queue"; //Match QueuePrefsBean toolId + public static final String QUEUE_PREFS_BEAN_TOOL_ID = "jms-queue"; //Match QueuePrefsBean toolId + + private static final String JMS_QUEUE_PREFERENCE_API_PATH = "/jms_queues"; + private static final String TMP_DIRECTORY = "/tmp"; + + private static final String FAKE_USERNAME = "fakeuser"; + private static final String FAKE_PASSWORD = "fakepass"; + private static final String FAKE_ALIAS = "fakealias"; + private static final String FAKE_SECRET = "fakesecret"; + + private static final String CONTAINER_MANAGER_ROLE_NAME = "ContainerManager"; + private static final String PRIVILEGED_USER_ROLE_NAME = "Privileged"; + + private static final String VALID_MIN_STRING = "2"; + private static final int VALID_MIN_INT = Integer.parseInt(VALID_MIN_STRING); + + private static final String VALID_MAX_STRING = "300"; + private static final int VALID_MAX_INT = Integer.parseInt(VALID_MAX_STRING); + + private static final String VALID_MIN_ALT_STRING = "1"; + private static final int VALID_MIN_ALT_INT = Integer.parseInt(VALID_MIN_ALT_STRING); + + private static final String VALID_MAX_ALT_STRING = "1"; + private static final int VALID_MAX_ALT_INT = Integer.parseInt(VALID_MAX_ALT_STRING); + + private static final String INVALID_MIN_STRING = "50"; + private static final String INVALID_MAX_STRING = "20"; + private static final String MIN_FINALIZING = "concurrencyMinFinalizingQueue"; private static final String MAX_FINALIZING = "concurrencyMaxFinalizingQueue"; private static final String MIN_STAGING = "concurrencyMinStagingQueue"; private static final String MAX_STAGING = "concurrencyMaxStagingQueue"; - public static final Map PREF_MAP = new HashMap() {{ + + public static final Map DEFAULT_CONCURRENCY_PREFERENCES = new HashMap() {{ put(MIN_FINALIZING, Integer.parseInt(ContainersConfig.QUEUE_MIN_CONCURRENCY_DFLT)); put(MAX_FINALIZING, Integer.parseInt(ContainersConfig.QUEUE_MAX_CONCURRENCY_DFLT)); put(MIN_STAGING, Integer.parseInt(ContainersConfig.QUEUE_MIN_CONCURRENCY_DFLT)); put(MAX_STAGING, Integer.parseInt(ContainersConfig.QUEUE_MAX_CONCURRENCY_DFLT)); }}; - private final String PATH = "/jms_queues"; - - private final MediaType JSON = MediaType.APPLICATION_JSON; - - private String VALID_MIN = "2"; - private String VALID_MAX = "300"; - private String VALID_MIN_ALT = "1"; - private String VALID_MAX_ALT = "1"; - - private String INVALID_MIN = "50"; - private String INVALID_MAX = "20"; - - @Autowired private WebApplicationContext wac; - @Autowired private RoleServiceI mockRoleService; - @Autowired private AliasTokenService mockAliasTokenService; - @Autowired private UserManagementServiceI mockUserManagementServiceI; - @Autowired private QueuePrefsBean queuePrefsBean; - @Autowired private NrgPreferenceService fakePrefsService; - @Autowired private DefaultJmsListenerContainerFactory finalizingQueueListenerFactory; - @Autowired private DefaultJmsListenerContainerFactory stagingQueueListenerFactory; - @Autowired private ObjectMapper mapper; - - @Rule public TemporaryFolder folder = new TemporaryFolder(new File("/tmp")); + @Autowired + private WebApplicationContext wac; + @Autowired + private RoleServiceI mockRoleService; + @Autowired + private AliasTokenService mockAliasTokenService; + @Autowired + private UserManagementServiceI mockUserManagementServiceI; + @Autowired + private QueuePrefsBean queuePrefsBean; + @Autowired + private NrgPreferenceService fakePrefsService; + @Autowired + private ObjectMapper mapper; + + @Autowired + private JmsListenerEndpointRegistry jmsListenerEndpointRegistry; + + @Rule + public TemporaryFolder folder = new TemporaryFolder(new File(TMP_DIRECTORY)); @Before public void setup() throws Exception { mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build(); // Mock the userI - final String username = "fakeuser"; - final String password = "fakepass"; - mockAdmin = Mockito.mock(UserI.class); - when(mockAdmin.getLogin()).thenReturn(username); - when(mockAdmin.getPassword()).thenReturn(password); - when(mockRoleService.isSiteAdmin(mockAdmin)).thenReturn(true); + final String username = FAKE_USERNAME; + final String password = FAKE_PASSWORD; + UserI mockContainerManager = Mockito.mock(UserI.class); + when(mockContainerManager.getLogin()).thenReturn(username); + when(mockContainerManager.getPassword()).thenReturn(password); + when(mockRoleService.checkRole(mockContainerManager, CONTAINER_MANAGER_ROLE_NAME)).thenReturn(true); + when(mockRoleService.checkRole(mockContainerManager, PRIVILEGED_USER_ROLE_NAME)).thenReturn(true); - authentication = new TestingAuthenticationToken(mockAdmin, password); + authentication = new TestingAuthenticationToken(mockContainerManager, password); // Mock the user management service - when(mockUserManagementServiceI.getUser(username)).thenReturn(mockAdmin); + when(mockUserManagementServiceI.getUser(username)).thenReturn(mockContainerManager); // Mock the aliasTokenService final AliasToken mockAliasToken = new AliasToken(); + mockAliasToken.setAlias(FAKE_ALIAS); + mockAliasToken.setSecret(FAKE_SECRET); - final String alias = "fakealias"; - final String secret = "fakesecret"; - mockAliasToken.setAlias(alias); - mockAliasToken.setSecret(secret); - when(mockAliasTokenService.issueTokenForUser(mockAdmin)).thenReturn(mockAliasToken); - } + when(mockAliasTokenService.issueTokenForUser(mockContainerManager)).thenReturn(mockAliasToken); - private void testValidSet(DefaultJmsListenerContainerFactory factory, String minParam, String maxParam) throws Exception { - Map params = new HashMap<>(); - params.put(minParam, VALID_MIN); - params.put(maxParam, VALID_MAX); - String requestJson = mapper.writeValueAsString(params); + final DefaultMessageListenerContainer finalizingContainer = Mockito.spy(newDefaultMessageListenerContainer()); + Mockito.doNothing().when(finalizingContainer).start(); - MockHttpServletRequestBuilder request = - post(PATH).contentType(JSON) - .content(requestJson) - .with(authentication(authentication)) - .with(csrf()) - .with(testSecurityContext()); + final DefaultMessageListenerContainer stagingContainer = Mockito.spy(newDefaultMessageListenerContainer()); + Mockito.doNothing().when(stagingContainer).start(); - mockMvc.perform(request).andExpect(status().isOk()); + when(jmsListenerEndpointRegistry.getListenerContainer(ContainersConfig.FINALIZING_QUEUE_CONTAINER_ID)) + .thenReturn(finalizingContainer); - assertThat(queuePrefsBean.getIntegerValue(minParam), is(Integer.parseInt(VALID_MIN))); - assertThat(queuePrefsBean.getIntegerValue(maxParam), is(Integer.parseInt(VALID_MAX))); - assertThat((String) Whitebox.getInternalState(factory, "concurrency"), - is(VALID_MIN + "-" + VALID_MAX)); - - params = new HashMap<>(); - params.put(minParam, VALID_MIN_ALT); - params.put(maxParam, VALID_MAX_ALT); - requestJson = mapper.writeValueAsString(params); - request = post(PATH).contentType(JSON) - .content(requestJson) - .with(authentication(authentication)) - .with(csrf()) - .with(testSecurityContext()); - - mockMvc.perform(request).andExpect(status().isOk()); - - assertThat(queuePrefsBean.getIntegerValue(minParam), is(Integer.parseInt(VALID_MIN_ALT))); - assertThat(queuePrefsBean.getIntegerValue(maxParam), is(Integer.parseInt(VALID_MAX_ALT))); - assertThat((String) Whitebox.getInternalState(factory, "concurrency"), - is(VALID_MIN_ALT + "-" + VALID_MAX_ALT)); + when(jmsListenerEndpointRegistry.getListenerContainer(ContainersConfig.STAGING_QUEUE_CONTAINER_ID)) + .thenReturn(stagingContainer); } - public void testInvalidSet(DefaultJmsListenerContainerFactory factory, String minParam, String maxParam) throws Exception { - String prevState = Whitebox.getInternalState(factory, "concurrency"); - Map params = new HashMap<>(); - params.put(minParam, INVALID_MIN); - params.put(maxParam, INVALID_MAX); - String requestJson = mapper.writeValueAsString(params); - final MockHttpServletRequestBuilder request = - post(PATH).contentType(JSON) - .content(requestJson) - .with(authentication(authentication)) - .with(csrf()) - .with(testSecurityContext()); - - try { - mockMvc.perform(request).andExpect(status().isBadRequest()); - } catch (NestedServletException e) { - if (!(e.getCause() instanceof ClientException)) { - // we expect ClientException so ignore that, rethrow anything else - throw e; - } - } - - assertThat(queuePrefsBean.getIntegerValue(minParam), is(Integer.parseInt(ContainersConfig.QUEUE_MIN_CONCURRENCY_DFLT))); - assertThat(queuePrefsBean.getIntegerValue(maxParam), is(Integer.parseInt(ContainersConfig.QUEUE_MAX_CONCURRENCY_DFLT))); - assertThat((String) Whitebox.getInternalState(factory, "concurrency"), - is(prevState)); - } - - public void checkBeanValue(Map expectedPrefMap) throws Exception { - checkBeanValue(expectedPrefMap, mapper.writeValueAsString(queuePrefsBean)); - } - public void checkBeanValue(Map expectedPrefMap, String beanAsString) throws Exception { - // Get the QueuePrefsBean bean as it is - without hitting the fake prefs service - final Map prefs = mapper.readValue(beanAsString, - new TypeReference>() {}); - for (String key : expectedPrefMap.keySet()) { - assertThat(prefs, hasKey(key)); - assertThat(prefs.get(key), is(expectedPrefMap.get(key))); - } + private DefaultMessageListenerContainer newDefaultMessageListenerContainer() { + final DefaultMessageListenerContainer container = new DefaultMessageListenerContainer(); + container.setConcurrency(ContainersConfig.QUEUE_MIN_CONCURRENCY_DFLT + "-" + ContainersConfig.QUEUE_MAX_CONCURRENCY_DFLT); + container.setConnectionFactory(Mockito.mock(ConnectionFactory.class)); + container.setDestination(Mockito.mock(Destination.class)); + container.afterPropertiesSet(); + return container; } @Test @DirtiesContext public void testValidSetFinalizing() throws Exception { - testValidSet(finalizingQueueListenerFactory, MIN_FINALIZING, MAX_FINALIZING); + testValidSet(ContainersConfig.FINALIZING_QUEUE_CONTAINER_ID, MIN_FINALIZING, MAX_FINALIZING); } @Test @DirtiesContext public void testInvalidSetFinalizing() throws Exception { - testInvalidSet(finalizingQueueListenerFactory, MIN_FINALIZING, MAX_FINALIZING); + testInvalidSet(ContainersConfig.FINALIZING_QUEUE_CONTAINER_ID, MIN_FINALIZING, MAX_FINALIZING); } @Test @DirtiesContext public void testValidSetStaging() throws Exception { - testValidSet(stagingQueueListenerFactory, MIN_STAGING, MAX_STAGING); + testValidSet(ContainersConfig.STAGING_QUEUE_CONTAINER_ID, MIN_STAGING, MAX_STAGING); } @Test @DirtiesContext public void testInvalidSetStaging() throws Exception { - testInvalidSet(stagingQueueListenerFactory, MIN_STAGING, MAX_STAGING); + testInvalidSet(ContainersConfig.STAGING_QUEUE_CONTAINER_ID, MIN_STAGING, MAX_STAGING); } @Test public void testGet() throws Exception { // beanAsMap will be equal to PREF_MAP, but let's just be specific - we want the GET to return the bean - Map beanAsMap = mapper.readValue(mapper.writeValueAsString(queuePrefsBean), - new TypeReference>() {}); + final Map beanAsMap = mapper.readValue(mapper.writeValueAsString(queuePrefsBean), + new TypeReference>() { + }); - final MockHttpServletRequestBuilder request = get(PATH) + final MockHttpServletRequestBuilder request = get(JMS_QUEUE_PREFERENCE_API_PATH) .with(authentication(authentication)) .with(csrf()) .with(testSecurityContext()); - final String response = - mockMvc.perform(request) - .andExpect(status().isOk()) - .andReturn() - .getResponse() - .getContentAsString(); - + final String response = mockMvc.perform(request) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); checkBeanValue(beanAsMap, response); } @Test @DirtiesContext - public void testRefresh() throws Exception { + public void testRefresh() throws Exception { // Make sure that we're in the state we expect - checkBeanValue(PREF_MAP); + checkBeanValue(DEFAULT_CONCURRENCY_PREFERENCES); // Fake changing the preferences in the db, as if changed via API on another node - final Map altMap = new HashMap() {{ - put(MIN_FINALIZING, Integer.parseInt(VALID_MIN)); - put(MAX_FINALIZING, Integer.parseInt(VALID_MAX)); - put(MIN_STAGING, Integer.parseInt(VALID_MAX_ALT)); - put(MAX_STAGING, Integer.parseInt(VALID_MIN_ALT)); + final Map preferencesMap = new HashMap() {{ + put(MIN_FINALIZING, Integer.parseInt(VALID_MIN_STRING)); + put(MAX_FINALIZING, Integer.parseInt(VALID_MAX_STRING)); + put(MIN_STAGING, Integer.parseInt(VALID_MIN_ALT_STRING)); + put(MAX_STAGING, Integer.parseInt(VALID_MAX_ALT_STRING)); }}; - for (String key : altMap.keySet()) { - fakePrefsService.setPreferenceValue(TOOL_ID, key, ((Integer) altMap.get(key)).toString()); + + for (String key : preferencesMap.keySet()) { + fakePrefsService.setPreferenceValue(QUEUE_PREFS_BEAN_TOOL_ID, key, ((Integer) preferencesMap.get(key)).toString()); } // Confirm that we didn't just accidentally update our bean... - checkBeanValue(PREF_MAP); + checkBeanValue(DEFAULT_CONCURRENCY_PREFERENCES); + + verifyListenerContainerConcurrency(ContainersConfig.FINALIZING_QUEUE_CONTAINER_ID, + Integer.parseInt(ContainersConfig.QUEUE_MIN_CONCURRENCY_DFLT), Integer.parseInt(ContainersConfig.QUEUE_MAX_CONCURRENCY_DFLT)); + verifyListenerContainerConcurrency(ContainersConfig.STAGING_QUEUE_CONTAINER_ID, + Integer.parseInt(ContainersConfig.QUEUE_MIN_CONCURRENCY_DFLT), Integer.parseInt(ContainersConfig.QUEUE_MAX_CONCURRENCY_DFLT)); // Now update the bean - final Runnable updatePrefsFromDb = queuePrefsBean.getRefresher(false); - updatePrefsFromDb.run(); + queuePrefsBean.getRefresher(false).run(); + + // Get the QueuePrefsBean bean as it is, without hitting the prefs service, to + // make sure bean is actually updated + checkBeanValue(preferencesMap); + + // Make sure listener container concurrency settings are updated + verifyListenerContainerConcurrency(ContainersConfig.FINALIZING_QUEUE_CONTAINER_ID, VALID_MIN_INT, VALID_MAX_INT); + verifyListenerContainerConcurrency(ContainersConfig.STAGING_QUEUE_CONTAINER_ID, VALID_MIN_ALT_INT, VALID_MAX_ALT_INT); + } + + private void verifyListenerContainerConcurrency(final String listenerId, final int expectedMin, final int expectedMax) { + final DefaultMessageListenerContainer container + = (DefaultMessageListenerContainer) jmsListenerEndpointRegistry.getListenerContainer(listenerId); + assertNotNull(container); + + // Give the container a few seconds to re-initialize + await().atMost(5, TimeUnit.SECONDS).until( + () -> container.getConcurrentConsumers() == expectedMin && container.getMaxConcurrentConsumers() == expectedMax); + } + + private void postJmsPreferences(final Map preferences) throws Exception { + final MockHttpServletRequestBuilder request = + post(JMS_QUEUE_PREFERENCE_API_PATH).contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(preferences)) + .with(authentication(authentication)) + .with(csrf()) + .with(testSecurityContext()); + mockMvc.perform(request).andExpect(status().isOk()); + } + + private void testValidSet(final String listenerId, final String minPreferenceName, final String maxPreferenceName) throws Exception { + final Map initalPreferences = new HashMap<>(); + initalPreferences.put(minPreferenceName, VALID_MIN_STRING); + initalPreferences.put(maxPreferenceName, VALID_MAX_STRING); + postJmsPreferences(initalPreferences); - // Make sure bean is actually updated - checkBeanValue(altMap); // Get the QueuePrefsBean bean as it is - without hitting the prefs service + assertThat(queuePrefsBean.getIntegerValue(minPreferenceName), is(VALID_MIN_INT)); + assertThat(queuePrefsBean.getIntegerValue(maxPreferenceName), is(VALID_MAX_INT)); + verifyListenerContainerConcurrency(listenerId, VALID_MIN_INT, VALID_MAX_INT); - // Make sure concurrencies are also updated - assertThat((String) Whitebox.getInternalState(finalizingQueueListenerFactory, "concurrency"), - is(VALID_MIN + "-" + VALID_MAX)); - assertThat((String) Whitebox.getInternalState(stagingQueueListenerFactory, "concurrency"), - is(VALID_MIN_ALT + "-" + VALID_MAX_ALT)); + final Map altPreferences = new HashMap<>(); + altPreferences.put(minPreferenceName, VALID_MIN_ALT_STRING); + altPreferences.put(maxPreferenceName, VALID_MAX_ALT_STRING); + postJmsPreferences(altPreferences); + + assertThat(queuePrefsBean.getIntegerValue(minPreferenceName), is(VALID_MIN_ALT_INT)); + assertThat(queuePrefsBean.getIntegerValue(maxPreferenceName), is(VALID_MAX_ALT_INT)); + verifyListenerContainerConcurrency(listenerId, VALID_MIN_ALT_INT, VALID_MAX_ALT_INT); + } + + private void testInvalidSet(final String listenerId, String minParam, String maxParam) throws Exception { + Map initialPreferences = new HashMap<>(); + initialPreferences.put(minParam, INVALID_MIN_STRING); + initialPreferences.put(maxParam, INVALID_MAX_STRING); + try { + postJmsPreferences(initialPreferences); + } catch (NestedServletException e) { + if (!(e.getCause() instanceof ClientException)) { + throw e; // we expect ClientException so ignore that, rethrow anything else + } + } + + assertThat(queuePrefsBean.getValue(minParam), is(ContainersConfig.QUEUE_MIN_CONCURRENCY_DFLT)); + assertThat(queuePrefsBean.getValue(maxParam), is(ContainersConfig.QUEUE_MAX_CONCURRENCY_DFLT)); + verifyListenerContainerConcurrency(listenerId, + Integer.parseInt(ContainersConfig.QUEUE_MIN_CONCURRENCY_DFLT), Integer.parseInt(ContainersConfig.QUEUE_MAX_CONCURRENCY_DFLT)); + } + + public void checkBeanValue(Map expectedPrefMap) throws Exception { + checkBeanValue(expectedPrefMap, mapper.writeValueAsString(queuePrefsBean)); + } + + private void checkBeanValue(Map expectedPrefMap, String beanAsString) throws Exception { + // Get the QueuePrefsBean bean as it is - without hitting the fake prefs service + final Map prefs = mapper.readValue(beanAsString, + new TypeReference>() { + }); + for (String key : expectedPrefMap.keySet()) { + assertThat(prefs, hasKey(key)); + assertThat(prefs.get(key), is(expectedPrefMap.get(key))); + } } } diff --git a/src/test/java/org/nrg/containers/secrets/SecretsTest.java b/src/test/java/org/nrg/containers/secrets/SecretsTest.java index 8f31c678..9ceeec1e 100644 --- a/src/test/java/org/nrg/containers/secrets/SecretsTest.java +++ b/src/test/java/org/nrg/containers/secrets/SecretsTest.java @@ -25,11 +25,9 @@ import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; -import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.Mock; +import org.mockito.InjectMocks; import org.mockito.Mockito; -import org.mockito.internal.util.reflection.Whitebox; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.nrg.containers.api.DockerControlApi; @@ -51,11 +49,10 @@ import org.nrg.xdat.services.cache.UserDataCache; import org.nrg.xft.security.UserI; import org.nrg.xnat.services.archive.CatalogService; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest; -import org.powermock.modules.junit4.PowerMockRunner; import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.AbstractMap; import java.util.Collections; import java.util.List; @@ -75,24 +72,25 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; @Slf4j -@RunWith(PowerMockRunner.class) -@PrepareOnlyThisForTest(DockerControlApi.class) +//@PrepareOnlyThisForTest(DockerControlApi.class) public class SecretsTest { private final static Answer RETURN_SELF = InvocationOnMock::getMock; - @Mock private SystemPropertySecretSource.ValueObtainer valueObtainer; - @Mock private CommandService commandService; - @Mock private DockerServerService dockerServerService; - @Mock private SiteConfigPreferences siteConfigPreferences; - @Mock private DockerService dockerService; - @Mock private CatalogService catalogService; - @Mock private UserDataCache userDataCache; - @Mock private KubernetesClientImpl kubernetesClient; - @Mock private DockerServerBase.DockerServer dockerServer; - - @Mock private UserI user; + private SystemPropertySecretSource.ValueObtainer valueObtainer = Mockito.mock(SystemPropertySecretSource.ValueObtainer.class); + private CommandService commandService = Mockito.mock(CommandService.class); + private DockerServerService dockerServerService = Mockito.mock(DockerServerService.class); + private SiteConfigPreferences siteConfigPreferences = Mockito.mock(SiteConfigPreferences.class); + private DockerService dockerService = Mockito.mock(DockerService.class); + private CatalogService catalogService =Mockito.mock(CatalogService.class); + private UserDataCache userDataCache = Mockito.mock(UserDataCache.class); + private KubernetesClientImpl kubernetesClient = Mockito.mock(KubernetesClientImpl.class); + private DockerServerBase.DockerServer dockerServer = Mockito.mock(DockerServerBase.DockerServer.class); + + private UserI user = Mockito.mock(UserI.class); private Backend backend = null; private String userId; @@ -224,10 +222,17 @@ public void testLaunchKubernetesJobWithEnvSecret() throws Exception { backend = Backend.KUBERNETES; final String namespace = RandomStringUtils.randomAlphabetic(5); - Whitebox.setInternalState(kubernetesClient, "namespace", namespace); + // Whitebox.setInternalState(kubernetesClient, "namespace", namespace); + Field namespaceField = kubernetesClient.getClass().getDeclaredField("namespace"); + namespaceField.setAccessible(true); // Allow access to private fields + namespaceField.set(kubernetesClient, namespace); final BatchV1Api batchApi = Mockito.mock(BatchV1Api.class); - Whitebox.setInternalState(kubernetesClient, "batchApi", batchApi); + // Whitebox.setInternalState(kubernetesClient, "batchApi", batchApi); + Field batchApiField = kubernetesClient.getClass().getDeclaredField("batchApi"); + batchApiField.setAccessible(true); // Allow access to private fields + batchApiField.set(kubernetesClient, batchApi); + // Create secret value and objects final String secretValue = RandomStringUtils.randomAlphanumeric(32); @@ -255,8 +260,8 @@ public void testLaunchKubernetesJobWithEnvSecret() throws Exception { // In real life the backend would return the job spec we sent in + a bunch of info about what got created. // But we don't need any of that extra stuff, we only want to see the spec we made. // We return exactly the same job spec we sent in, as if the backend responded after having done nothing at all. - Mockito.when(batchApi.createNamespacedJob(Mockito.eq(namespace), Mockito.any(V1Job.class), Mockito.isNull(String.class), Mockito.isNull(String.class), Mockito.isNull(String.class), Mockito.isNull(String.class))) - .thenAnswer((Answer) invocation -> invocation.getArgumentAt(1, V1Job.class)); + Mockito.when(batchApi.createNamespacedJob(Mockito.eq(namespace), Mockito.any(V1Job.class), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull())) + .thenAnswer((Answer) invocation -> invocation.getArgument(1, V1Job.class)); // Call method under test Mockito.when(kubernetesClient.createJob(toCreate, DockerControlApi.NumReplicas.ZERO, null, null)) @@ -265,7 +270,7 @@ public void testLaunchKubernetesJobWithEnvSecret() throws Exception { // Capture call to backend api mock final ArgumentCaptor jobArgumentCaptor = ArgumentCaptor.forClass(V1Job.class); - Mockito.verify(batchApi).createNamespacedJob(Mockito.eq(namespace), jobArgumentCaptor.capture(), Mockito.isNull(String.class), Mockito.isNull(String.class), Mockito.isNull(String.class), Mockito.isNull(String.class)); + Mockito.verify(batchApi).createNamespacedJob(Mockito.eq(namespace), jobArgumentCaptor.capture(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull()); final V1Job job = jobArgumentCaptor.getValue(); // Extract container spec @@ -315,13 +320,17 @@ public void testLaunchSwarmJobWithEnvSecret() throws Exception { .build(); // Class under test - final DockerControlApi dockerControlApi = PowerMockito.mock(DockerControlApi.class); + final DockerControlApi dockerControlApi = Mockito.spy(DockerControlApi.class); // Mock out DockerControlApi#getDockerClient(dockerServer) final DockerClient dockerClient = Mockito.mock(DockerClient.class); - PowerMockito.doReturn(dockerClient).when(dockerControlApi, "getDockerClient", dockerServer); +// PowerMockito.doReturn(dockerClient).when(dockerControlApi, "getDockerClient", dockerServer); +// Mockito.doReturn(dockerClient).when(dockerControlApi) +// .getDockerClient(any(DockerServerBase.DockerServer.class)); + when(dockerControlApi.getDockerClient()).thenReturn(dockerClient); - PowerMockito.doReturn(dockerServer).when(dockerControlApi, "getServer"); + // PowerMockito.doReturn(dockerServer).when(dockerControlApi, "getServer"); + when(dockerServerService.getServer()).thenReturn(dockerServer); // Mock out backend call to create service final String containerId = RandomStringUtils.randomAlphanumeric(5); @@ -333,8 +342,19 @@ public void testLaunchSwarmJobWithEnvSecret() throws Exception { Mockito.when(dockerClient.createServiceCmd(Mockito.any(ServiceSpec.class))).thenReturn(cmd); // Call method under test - PowerMockito.doCallRealMethod().when(dockerControlApi, "createDockerSwarmService", toCreate, dockerServer, DockerControlApi.NumReplicas.ZERO); - PowerMockito.doCallRealMethod().when(dockerControlApi).create(toCreate, user); +// PowerMockito.doCallRealMethod().when(dockerControlApi, "createDockerSwarmService", toCreate, dockerServer, DockerControlApi.NumReplicas.ZERO); + Method method = DockerControlApi.class.getDeclaredMethod("createDockerSwarmService", Container.class, DockerServerBase.DockerServer.class, DockerControlApi.NumReplicas.class ); + method.setAccessible(true); + + Mockito.doCallRealMethod().when(dockerControlApi); + method.invoke(dockerControlApi, toCreate, dockerServer, DockerControlApi.NumReplicas.ZERO); +// Mockito.doCallRealMethod().when(dockerControlApi) +// .getClass() +// .getDeclaredMethod("createDockerSwarmService", Container.class, DockerServerBase.DockerServer.class, DockerControlApi.NumReplicas.class) +// .invoke(dockerControlApi, toCreate, dockerServer, DockerControlApi.NumReplicas.ZERO); + +// PowerMockito.doCallRealMethod().when(dockerControlApi).create(toCreate, user); + Mockito.doCallRealMethod().when(dockerControlApi).create(toCreate, user); dockerControlApi.create(toCreate, user); // Capture call to backend api mock @@ -376,13 +396,18 @@ public void testLaunchDockerContainerWithEnvSecret() throws Exception { .build(); // Class under test - final DockerControlApi dockerControlApi = PowerMockito.mock(DockerControlApi.class); + final DockerControlApi dockerControlApi = Mockito.mock(DockerControlApi.class); // Mock out DockerControlApi#getDockerClient(dockerServer) final DockerClient dockerClient = Mockito.mock(DockerClient.class); - PowerMockito.doReturn(dockerClient).when(dockerControlApi, "getDockerClient", dockerServer); - PowerMockito.doReturn(dockerServer).when(dockerControlApi, "getServer"); +// PowerMockito.doReturn(dockerClient).when(dockerControlApi, "getDockerClient", dockerServer); + Mockito.doReturn(dockerClient).when(dockerControlApi) + .getDockerClient(any(DockerServerBase.DockerServer.class)); + +// PowerMockito.doReturn(dockerServer).when(dockerControlApi, "getServer"); + when(dockerServerService.getServer()).thenReturn(dockerServer); + // Mock out backend call to create service final String containerId = RandomStringUtils.randomAlphanumeric(5); @@ -398,8 +423,14 @@ public void testLaunchDockerContainerWithEnvSecret() throws Exception { Mockito.when(dockerClient.createContainerCmd(dockerImage)).thenReturn(cmd); // Call method under test - PowerMockito.doCallRealMethod().when(dockerControlApi, "createDockerContainer", toCreate, dockerServer); - PowerMockito.doCallRealMethod().when(dockerControlApi).create(toCreate, user); +// PowerMockito.doCallRealMethod().when(dockerControlApi, "crxeateDockerContainer", toCreate, dockerServer); + Method method = DockerControlApi.class.getDeclaredMethod("createDockerContainer", CreateContainerCmd.class, DockerServerBase.DockerServer.class); + method.setAccessible(true); + Mockito.doCallRealMethod().when(dockerControlApi); + method.invoke(dockerControlApi, toCreate, dockerServer); + +// PowerMockito.doCallRealMethod().when(dockerControlApi).create(toCreate, user); + Mockito.doCallRealMethod().when(dockerControlApi).create(toCreate, user); dockerControlApi.create(toCreate, user); // Capture call to backend api mock diff --git a/src/test/java/org/nrg/containers/services/CommandsAvailableTest.java b/src/test/java/org/nrg/containers/services/CommandsAvailableTest.java index cfa7f001..91a0bef4 100644 --- a/src/test/java/org/nrg/containers/services/CommandsAvailableTest.java +++ b/src/test/java/org/nrg/containers/services/CommandsAvailableTest.java @@ -1,14 +1,16 @@ package org.nrg.containers.services; import lombok.extern.slf4j.Slf4j; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; -import org.junit.runner.RunWith; -import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.nrg.containers.model.command.auto.Command; import org.nrg.containers.model.command.auto.CommandSummaryForContext; @@ -18,9 +20,6 @@ import org.nrg.xdat.security.helpers.Permissions; import org.nrg.xft.schema.Wrappers.GenericWrapper.GenericWrapperElement; import org.nrg.xft.security.UserI; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; import java.util.Collections; import java.util.List; @@ -29,19 +28,21 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; import static org.nrg.containers.utils.ContainerServicePermissionUtils.PROJECT_EDIT_XML_PATH; import static org.nrg.containers.utils.ContainerServicePermissionUtils.PROJECT_READ_XML_PATH; import static org.nrg.xdat.security.SecurityManager.EDIT; import static org.nrg.xdat.security.SecurityManager.READ; -import static org.powermock.api.mockito.PowerMockito.mockStatic; @Slf4j -@RunWith(PowerMockRunner.class) -@PrepareForTest({Permissions.class, GenericWrapperElement.class}) public class CommandsAvailableTest { + private MockedStatic mockedGenericWrapperElement; + + private MockedStatic mockedPermissions; + @Rule public TestRule watcher = new TestWatcher() { protected void starting(Description description) { @@ -53,11 +54,11 @@ protected void finished(Description description) { } }; - @Mock private CommandEntityService commandEntityService; - @Mock private ContainerConfigService containerConfigService; + private CommandEntityService commandEntityService = Mockito.mock(CommandEntityService.class); + private ContainerConfigService containerConfigService = Mockito.mock(ContainerConfigService.class); - @Mock private UserI admin; - @Mock private UserI collaborator; + private UserI admin = Mockito.mock(UserI.class); + private UserI collaborator = Mockito.mock(UserI.class); private static final String project = "project"; private CommandService commandService; @@ -68,10 +69,17 @@ public void setup() { Mockito.when(admin.getUsername()).thenReturn("admin"); Mockito.when(collaborator.getLogin()).thenReturn("collab"); Mockito.when(collaborator.getUsername()).thenReturn("collab"); - + mockedGenericWrapperElement = mockStatic(GenericWrapperElement.class); + mockedPermissions = mockStatic(Permissions.class); commandService = new CommandServiceImpl(commandEntityService, containerConfigService); } + @After + public void tearDownStaticMocks() { + mockedPermissions.closeOnDemand(); + mockedGenericWrapperElement.closeOnDemand(); + } + @Test public void testNonProjectInput() throws Exception { // This UUID will stand in for the "context" and a data type @@ -117,29 +125,23 @@ public void testNonProjectInput() throws Exception { // Mock the call to get the enabled/disabled status Mockito.when(containerConfigService.isEnabled(eq(project), any(Long.class))).thenReturn(true); - - // Mock resolving xsi type - mockStatic(GenericWrapperElement.class); - PowerMockito.when(GenericWrapperElement.GetElement(any(String.class))) + mockedGenericWrapperElement.when(() -> GenericWrapperElement.GetElement(any(String.class))) .thenAnswer(invocation -> { GenericWrapperElement gwe = Mockito.mock(GenericWrapperElement.class); // Just return whatever xsi type was passed in - Mockito.when(gwe.getXSIType()).thenReturn(invocation.getArgumentAt(0, String.class)); + Mockito.when(gwe.getXSIType()).thenReturn(invocation.getArgument(0, String.class)); return gwe; }); - // Mock permissions checks - mockStatic(Permissions.class); - // Admin can read and edit - PowerMockito.when(Permissions.can(admin, PROJECT_READ_XML_PATH, project, READ)).thenReturn(true); - PowerMockito.when(Permissions.can(admin, xsiType + "/project", project, READ)).thenReturn(true); - PowerMockito.when(Permissions.can(admin, xsiType + "/project", project, EDIT)).thenReturn(true); + mockedPermissions.when(() -> Permissions.can(admin, PROJECT_READ_XML_PATH, project, READ)).thenReturn(true); + mockedPermissions.when(() -> Permissions.can(admin, xsiType + "/project", project, READ)).thenReturn(true); + mockedPermissions.when(() -> Permissions.can(admin, xsiType + "/project", project, EDIT)).thenReturn(true); // Collaborator can only read - PowerMockito.when(Permissions.can(collaborator, PROJECT_READ_XML_PATH, project, READ)).thenReturn(true); - PowerMockito.when(Permissions.can(collaborator, xsiType + "/project", project, READ)).thenReturn(true); - PowerMockito.when(Permissions.can(collaborator, xsiType + "/project", project, EDIT)).thenReturn(false); + mockedPermissions.when(() -> Permissions.can(collaborator, PROJECT_READ_XML_PATH, project, READ)).thenReturn(true); + mockedPermissions.when(() -> Permissions.can(collaborator, xsiType + "/project", project, READ)).thenReturn(true); + mockedPermissions.when(() -> Permissions.can(collaborator, xsiType + "/project", project, EDIT)).thenReturn(false); final CommandSummaryForContext readWrapperSummary = CommandSummaryForContext.create(command, wrapperRequiresRead, true, externalInputName); final CommandSummaryForContext editWrapperSummary = CommandSummaryForContext.create(command, wrapperRequiresEdit, true, externalInputName); @@ -199,27 +201,21 @@ public void testProjectInput() throws Exception { // Mock the call to get the enabled/disabled status Mockito.when(containerConfigService.isEnabled(eq(project), any(Long.class))).thenReturn(true); - - // Mock resolving xsi type - mockStatic(GenericWrapperElement.class); - PowerMockito.when(GenericWrapperElement.GetElement(any(String.class))) + mockedGenericWrapperElement.when(() -> GenericWrapperElement.GetElement(any(String.class))) .thenAnswer(invocation -> { GenericWrapperElement gwe = Mockito.mock(GenericWrapperElement.class); // Just return whatever xsi type was passed in - Mockito.when(gwe.getXSIType()).thenReturn(invocation.getArgumentAt(0, String.class)); + Mockito.when(gwe.getXSIType()).thenReturn(invocation.getArgument(0, String.class)); return gwe; }); - // Mock permissions checks - mockStatic(Permissions.class); - // Admin can read and edit - PowerMockito.when(Permissions.can(admin, PROJECT_READ_XML_PATH, project, READ)).thenReturn(true); - PowerMockito.when(Permissions.can(admin, PROJECT_EDIT_XML_PATH, project, EDIT)).thenReturn(true); + mockedPermissions.when(() -> Permissions.can(admin, PROJECT_READ_XML_PATH, project, READ)).thenReturn(true); + mockedPermissions.when(() -> Permissions.can(admin, PROJECT_EDIT_XML_PATH, project, EDIT)).thenReturn(true); // Collaborator can only read - PowerMockito.when(Permissions.can(collaborator, PROJECT_READ_XML_PATH, project, READ)).thenReturn(true); - PowerMockito.when(Permissions.can(collaborator, PROJECT_EDIT_XML_PATH, project, EDIT)).thenReturn(false); + mockedPermissions.when(() -> Permissions.can(collaborator, PROJECT_READ_XML_PATH, project, READ)).thenReturn(true); + mockedPermissions.when(() -> Permissions.can(collaborator, PROJECT_EDIT_XML_PATH, project, EDIT)).thenReturn(false); final CommandSummaryForContext readWrapperSummary = CommandSummaryForContext.create(command, wrapperRequiresRead, true, externalInputName); final CommandSummaryForContext editWrapperSummary = CommandSummaryForContext.create(command, wrapperRequiresEdit, true, externalInputName); diff --git a/src/test/java/org/nrg/containers/services/ContainerLogTest.java b/src/test/java/org/nrg/containers/services/ContainerLogTest.java index 06fb0417..bc6e6fdf 100644 --- a/src/test/java/org/nrg/containers/services/ContainerLogTest.java +++ b/src/test/java/org/nrg/containers/services/ContainerLogTest.java @@ -10,6 +10,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.rules.TemporaryFolder; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; @@ -20,8 +21,10 @@ import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import org.nrg.containers.api.ContainerControlApi; import org.nrg.containers.api.LogType; import org.nrg.containers.model.container.auto.Container; @@ -47,14 +50,16 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @Slf4j @RunWith(Enclosed.class) public class ContainerLogTest { + @ExtendWith(MockitoExtension.class) @Ignore + @MockitoSettings(strictness = Strictness.WARN) @RunWith(JUnit4.class) public static class Base { @@ -69,22 +74,20 @@ protected void finished(Description description) { } }; - @Rule public MockitoRule rule = MockitoJUnit.rule(); - @Rule public TemporaryFolder temp = new TemporaryFolder(new File(System.getProperty("java.io.tmpdir"))); - @Mock public ContainerControlApi containerControlApi; - @Mock public ContainerEntityService containerEntityService; - @Mock public CommandResolutionService commandResolutionService; - @Mock public CommandService commandService; - @Mock public AliasTokenService aliasTokenService; - @Mock public SiteConfigPreferences siteConfigPreferences; - @Mock public ContainerFinalizeService containerFinalizeService; - @Mock public XnatAppInfo xnatAppInfo; - @Mock public CatalogService catalogService; - @Mock public OrchestrationService orchestrationService; - @Mock public NrgEventServiceI eventService; - @Mock public ThreadPoolExecutorFactoryBean executorFactoryBean; + public ContainerControlApi containerControlApi = Mockito.mock(ContainerControlApi.class); + public ContainerEntityService containerEntityService = Mockito.mock(ContainerEntityService.class); + public CommandResolutionService commandResolutionService = Mockito.mock(CommandResolutionService.class); + public CommandService commandService = Mockito.mock(CommandService.class); + public AliasTokenService aliasTokenService = Mockito.mock(AliasTokenService.class); + public SiteConfigPreferences siteConfigPreferences = Mockito.mock(SiteConfigPreferences.class); + public ContainerFinalizeService containerFinalizeService = Mockito.mock(ContainerFinalizeService.class); + public XnatAppInfo xnatAppInfo = Mockito.mock(XnatAppInfo.class); + public CatalogService catalogService = Mockito.mock(CatalogService.class); + public OrchestrationService orchestrationService = Mockito.mock(OrchestrationService.class); + public NrgEventServiceI eventService = Mockito.mock(NrgEventServiceI.class); + public ThreadPoolExecutorFactoryBean executorFactoryBean = new ThreadPoolExecutorFactoryBean(); public final ObjectMapper mapper = new ObjectMapper(); diff --git a/src/test/java/org/nrg/containers/services/ContainerStatusUpdateTest.java b/src/test/java/org/nrg/containers/services/ContainerStatusUpdateTest.java index f564cf77..73c27240 100644 --- a/src/test/java/org/nrg/containers/services/ContainerStatusUpdateTest.java +++ b/src/test/java/org/nrg/containers/services/ContainerStatusUpdateTest.java @@ -6,7 +6,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.nrg.containers.daos.ContainerEntityRepository; import org.nrg.containers.model.container.auto.Container; import org.nrg.containers.model.container.entity.ContainerEntity; diff --git a/src/test/java/org/nrg/containers/utils/TestingUtils.java b/src/test/java/org/nrg/containers/utils/TestingUtils.java index 9dd1473c..69507b5c 100644 --- a/src/test/java/org/nrg/containers/utils/TestingUtils.java +++ b/src/test/java/org/nrg/containers/utils/TestingUtils.java @@ -21,6 +21,8 @@ import org.hamcrest.Matchers; import org.junit.rules.TemporaryFolder; import org.mockito.ArgumentMatcher; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.nrg.containers.api.KubernetesClient; import org.nrg.containers.api.KubernetesClientImpl; import org.nrg.containers.model.container.auto.Container; @@ -41,7 +43,6 @@ import org.nrg.xnat.services.archive.CatalogService; import org.nrg.xnat.turbine.utils.ArchivableItem; import org.nrg.xnat.utils.WorkflowUtils; -import org.powermock.api.mockito.PowerMockito; import org.springframework.test.context.transaction.TestTransaction; import java.io.File; @@ -63,10 +64,9 @@ import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertThat; import static org.junit.Assume.assumeTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; @Slf4j public class TestingUtils { @@ -354,19 +354,11 @@ public static void cleanSwarmServices(final DockerClient client, Collection> isMapWithEntry(final String key, final String value) { return new ArgumentMatcher>() { @Override - public boolean matches(final Object argument) { - if (argument == null || !Map.class.isAssignableFrom(argument.getClass())) { + public boolean matches(Map argument) { + if (argument == null) { return false; } - - final Map argumentMap = Maps.newHashMap(); - try { - argumentMap.putAll((Map)argument); - } catch (ClassCastException e) { - return false; - } - - for (final Map.Entry entry : argumentMap.entrySet()) { + for (final Map.Entry entry : argument.entrySet()) { if (entry.getKey().equals(key) && entry.getValue().equals(value)) { return true; } @@ -538,23 +530,31 @@ public static void setupMocksForSetupWrapupWorkflow(String uri, FakeWorkflow fak when(mockItem.getXSIType()).thenReturn(xsiType); when(mockItem.getProject()).thenReturn(project); final ExptURI mockUriObject = mock(ExptURI.class); - when(UriParserUtils.parseURI(uri)).thenReturn(mockUriObject); - when(mockUriObject.getSecurityItem()).thenReturn(mockItem); - fakeWorkflow.setId(uri); - ResourceData mockRD = mock(ResourceData.class); - when(mockRD.getItem()).thenReturn(mockItem); - when(mockCatalogService.getResourceDataFromUri(uri)).thenReturn(mockRD); - - FakeWorkflow setupWrapupWorkflow = new FakeWorkflow(); - setupWrapupWorkflow.setWfid(111); - setupWrapupWorkflow.setEventId(2); - PowerMockito.doReturn(setupWrapupWorkflow).when(PersistentWorkflowUtils.class, "getOrCreateWorkflowData", eq(2), - eq(mockUser), any(XFTItem.class), any(EventDetails.class)); - when(WorkflowUtils.buildOpenWorkflow(eq(mockUser), eq(xsiType), eq(id), eq(project), any(EventDetails.class))) - .thenReturn(setupWrapupWorkflow); - - when(WorkflowUtils.getUniqueWorkflow(mockUser, setupWrapupWorkflow.getWorkflowId().toString())) - .thenReturn(setupWrapupWorkflow); + try (MockedStatic mockUriParserUtils = mockStatic(UriParserUtils.class)) { + mockUriParserUtils.when(() -> UriParserUtils.parseURI(uri)).thenReturn(mockUriObject); + when(mockUriObject.getSecurityItem()).thenReturn(mockItem); + fakeWorkflow.setId(uri); + ResourceData mockRD = mock(ResourceData.class); + when(mockRD.getItem()).thenReturn(mockItem); + when(mockCatalogService.getResourceDataFromUri(uri)).thenReturn(mockRD); + + FakeWorkflow setupWrapupWorkflow = new FakeWorkflow(); + setupWrapupWorkflow.setWfid(111); + setupWrapupWorkflow.setEventId(2); + MockedStatic mockedStatic = mockStatic(PersistentWorkflowUtils.class); + // 使用 Mockito.doReturn().when() 来定义静态方法的行为 + mockedStatic.when(() -> PersistentWorkflowUtils.getOrCreateWorkflowData(eq(2), any(), any(), any(), any())) + .thenReturn(setupWrapupWorkflow); +// Mockito.doReturn(setupWrapupWorkflow).when(PersistentWorkflowUtils.class, "getOrCreateWorkflowData", eq(2), +// eq(mockUser), any(XFTItem.class), any(EventDetails.class)); + try (MockedStatic mockWorkflowUtils = mockStatic(WorkflowUtils.class)) { + mockWorkflowUtils.when(()->WorkflowUtils.buildOpenWorkflow(eq(mockUser), eq(xsiType), eq(id), eq(project), any(EventDetails.class))).thenReturn(setupWrapupWorkflow); + + try (MockedStatic mockWorkflowUtils1 = mockStatic(WorkflowUtils.class)) { + mockWorkflowUtils1.when(()->WorkflowUtils.getUniqueWorkflow(mockUser, setupWrapupWorkflow.getWorkflowId().toString())).thenReturn(setupWrapupWorkflow); + } + } + } } } From 3a5dd7caef5a47fd242f3d356079e9ae0b24520d Mon Sep 17 00:00:00 2001 From: Automated Build Date: Tue, 19 Aug 2025 13:43:22 -0500 Subject: [PATCH 28/44] Update to version 3.7.2-RC-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f9d3e685..3835f7c3 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.7.2-SNAPSHOT" +version "3.7.2-RC-SNAPSHOT" java { sourceCompatibility = JavaVersion.VERSION_1_8 From 3adb55b56277840ec4f156ba09b2d4c67b509c76 Mon Sep 17 00:00:00 2001 From: Automated Build Date: Tue, 19 Aug 2025 14:14:33 -0500 Subject: [PATCH 29/44] Update to version 3.7.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3835f7c3..58527b86 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.7.2-RC-SNAPSHOT" +version "3.7.2" java { sourceCompatibility = JavaVersion.VERSION_1_8 From 49947a9a439085f11045f30cfc4cc9f6b1120fa2 Mon Sep 17 00:00:00 2001 From: Automated Build Date: Fri, 21 Nov 2025 12:56:13 -0600 Subject: [PATCH 30/44] Update to version 3.7.3-RC-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a6f56738..53c53a32 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.7.3-SNAPSHOT" +version "3.7.3-RC-SNAPSHOT" java { sourceCompatibility = JavaVersion.VERSION_1_8 From 8590cb6c9c4522cb2b63e13595c537724717398d Mon Sep 17 00:00:00 2001 From: Automated Build Date: Fri, 21 Nov 2025 13:25:32 -0600 Subject: [PATCH 31/44] Update to version 3.7.3 and parent version 1.9.3 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 53c53a32..e7bf2664 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - vXnat = "1.9.2" + vXnat = "1.9.3" pluginAppName = "XNAT Container Service" } } @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.7.3-RC-SNAPSHOT" +version "3.7.3" java { sourceCompatibility = JavaVersion.VERSION_1_8 From 1f68296226b6984655458f064dcd0a034b624f77 Mon Sep 17 00:00:00 2001 From: Bin Zhang Date: Tue, 6 Jan 2026 12:00:11 -0600 Subject: [PATCH 32/44] Update version to 3.7.3-java21-RC-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d822a0c0..4b46a658 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.7.1-java21-RC-SNAPSHOT" +version "3.7.3-java21-RC-SNAPSHOT" java { sourceCompatibility = 21 From a7891e676ac6c73001bd7614c2d28c61c7d81af4 Mon Sep 17 00:00:00 2001 From: Bin Zhang Date: Tue, 6 Jan 2026 15:29:06 -0600 Subject: [PATCH 33/44] fix unitTest --- .../containers/api/DockerControlApiTest.java | 5 +- .../nrg/containers/config/MockJmsConfig.java | 16 ++++- .../jms/JmsConsumerExceptionTest.java | 28 +++++--- .../containers/rest/LaunchRestApiTest.java | 11 +-- .../nrg/containers/secrets/SecretsTest.java | 67 ++++++++++++------- 5 files changed, 84 insertions(+), 43 deletions(-) diff --git a/src/test/java/org/nrg/containers/api/DockerControlApiTest.java b/src/test/java/org/nrg/containers/api/DockerControlApiTest.java index 29e2ce73..3d6c3287 100644 --- a/src/test/java/org/nrg/containers/api/DockerControlApiTest.java +++ b/src/test/java/org/nrg/containers/api/DockerControlApiTest.java @@ -363,7 +363,7 @@ public void testCreate_resolvedCommand() throws Exception { expectedCreatedBuilder.serviceId(BACKEND_ID); } else if (backend == Backend.KUBERNETES) { when(kubernetesClient.createJob( - any(Container.class), eq(DockerControlApi.NumReplicas.ZERO), any(String.class), any(String.class) + any(Container.class), eq(DockerControlApi.NumReplicas.ZERO), any(), any() )).thenReturn(BACKEND_ID); expectedCreatedBuilder.serviceId(BACKEND_ID); @@ -476,12 +476,15 @@ private void testStart_swarmMode() throws Exception { final ServiceSpec serviceSpec = Mockito.mock(ServiceSpec.class); final ServiceSpec updatedSpec = Mockito.mock(ServiceSpec.class); final Service service = Mockito.mock(Service.class); + final com.github.dockerjava.api.model.ResourceVersion resourceVersion = Mockito.mock(com.github.dockerjava.api.model.ResourceVersion.class); final InspectServiceCmd inspectServiceCmd = Mockito.mock(InspectServiceCmd.class, Mockito.RETURNS_DEEP_STUBS); final UpdateServiceCmd updateServiceCmd = Mockito.mock(UpdateServiceCmd.class, Mockito.RETURNS_DEEP_STUBS); when(mockDockerJavaClient.inspectServiceCmd(BACKEND_ID)).thenReturn(inspectServiceCmd); when(mockDockerJavaClient.updateServiceCmd(BACKEND_ID, updatedSpec)).thenReturn(updateServiceCmd); when(inspectServiceCmd.exec()).thenReturn(service); when(service.getSpec()).thenReturn(serviceSpec); + when(service.getVersion()).thenReturn(resourceVersion); + when(resourceVersion.getIndex()).thenReturn(1L); when(serviceSpec.withMode(any(ServiceModeConfig.class))).thenReturn(updatedSpec); // Run the test diff --git a/src/test/java/org/nrg/containers/config/MockJmsConfig.java b/src/test/java/org/nrg/containers/config/MockJmsConfig.java index 2525f2d1..5f7a998f 100644 --- a/src/test/java/org/nrg/containers/config/MockJmsConfig.java +++ b/src/test/java/org/nrg/containers/config/MockJmsConfig.java @@ -2,14 +2,19 @@ import org.apache.activemq.command.ActiveMQQueue; import org.mockito.Mockito; +import org.nrg.containers.jms.errors.ContainerJmsErrorHandler; import org.nrg.containers.jms.listeners.ContainerFinalizingRequestListener; import org.nrg.containers.jms.listeners.ContainerStagingRequestListener; import org.nrg.containers.jms.requests.ContainerFinalizingRequest; import org.nrg.containers.jms.requests.ContainerStagingRequest; import org.nrg.containers.services.ContainerService; +import org.nrg.mail.services.MailService; +import org.nrg.xdat.preferences.NotificationsPreferences; +import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xdat.security.services.UserManagementServiceI; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.jms.core.BrowserCallback; import org.springframework.jms.core.JmsTemplate; import org.springframework.jms.core.MessagePostProcessor; @@ -48,11 +53,16 @@ public Destination containerFinalizingRequest() { @SuppressWarnings("unchecked") @Bean + @Primary public JmsTemplate mockJmsTemplate(Destination containerStagingRequest, final ContainerStagingRequestListener containerStagingRequestListener, Destination containerFinalizingRequest, final ContainerFinalizingRequestListener containerFinalizingRequestListener, - ExecutorService executorService) { + ExecutorService executorService, + final SiteConfigPreferences siteConfigPreferences, + final NotificationsPreferences notificationsPreferences, + final MailService mailService) { + final ContainerJmsErrorHandler errorHandler = new ContainerJmsErrorHandler(siteConfigPreferences, notificationsPreferences, mailService); JmsTemplate mockJmsTemplate = Mockito.mock(JmsTemplate.class); doAnswer( invocation -> { @@ -62,7 +72,7 @@ public JmsTemplate mockJmsTemplate(Destination containerStagingRequest, try { containerStagingRequestListener.onRequest(request); } catch (Exception e) { - // ignored + errorHandler.handleError(e); } }); return null; @@ -77,7 +87,7 @@ public JmsTemplate mockJmsTemplate(Destination containerStagingRequest, try { containerFinalizingRequestListener.onRequest(request); } catch (Exception e) { - // ignored + errorHandler.handleError(e); } }); return null; diff --git a/src/test/java/org/nrg/containers/jms/JmsConsumerExceptionTest.java b/src/test/java/org/nrg/containers/jms/JmsConsumerExceptionTest.java index d955add6..97760d2d 100644 --- a/src/test/java/org/nrg/containers/jms/JmsConsumerExceptionTest.java +++ b/src/test/java/org/nrg/containers/jms/JmsConsumerExceptionTest.java @@ -16,6 +16,7 @@ import org.nrg.containers.utils.TestingUtils; import org.nrg.mail.services.MailService; import org.nrg.xdat.entities.AliasToken; +import org.nrg.xdat.preferences.NotificationsPreferences; import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xdat.security.helpers.Users; import org.nrg.xdat.security.services.PermissionsServiceI; @@ -66,6 +67,7 @@ public class JmsConsumerExceptionTest { @Autowired private ContainerService containerService; @Autowired private MailService mockMailService; @Autowired private AliasTokenService mockAliasTokenService; + @Autowired private NotificationsPreferences mockNotificationsPreferences; private UserI mockUser; private FakeWorkflow fakeWorkflow; @@ -100,6 +102,9 @@ public void setup() throws Exception { when(mockSiteConfigPreferences.getSiteId()).thenReturn(FAKE_SITEID); when(mockSiteConfigPreferences.getAdminEmail()).thenReturn(FAKE_EMAIL); + // Mock notifications preferences to allow JMS error notifications + when(mockNotificationsPreferences.getSuppressJMSFailureNotifications()).thenReturn(false); + // Permissions when(mockPermissionsServiceI.canEdit(any(UserI.class), any(ItemI.class))).thenReturn(Boolean.TRUE); @@ -169,17 +174,24 @@ public void tearDownStaticMocks() { @DirtiesContext //CS-1031 public void testStagingConsumeFailure() throws Exception { - // setup jmsTemplate to throw exception + // Setup: Make userManagementServiceI throw a RuntimeException that won't be caught by the listener + // This will propagate to the JMS error handler, unlike exceptions thrown from within + // consumeResolveCommandAndLaunchContainer which are caught internally String exceptionMsg = "my tricky exception message"; -// PowerMockito.when(WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) -// .thenThrow(new RuntimeException(exceptionMsg)); - mockedWorkflowUtils.when(() -> WorkflowUtils.getUniqueWorkflow(mockUser, fakeWorkflow.getWorkflowId().toString())) + + // Reset the user management mock to throw an uncaught exception + Mockito.reset(mockUserManagementServiceI); + when(mockUserManagementServiceI.getUser(FAKE_USER)) .thenThrow(new RuntimeException(exceptionMsg)); - containerService.queueResolveCommandAndLaunchContainer(null, wrapper.id(), 0L, wrapper.name(), Collections.emptyMap(), mockUser, fakeWorkflow - ); + containerService.queueResolveCommandAndLaunchContainer(null, wrapper.id(), 0L, wrapper.name(), + Collections.emptyMap(), mockUser, fakeWorkflow); - Mockito.verify(mockMailService, timeout(500).times(1)).sendHtmlMessage(eq(FAKE_EMAIL), - eq(FAKE_EMAIL), eq(FAKE_SITEID + " JMS Error"), contains(exceptionMsg)); + // Verify that the error handler sends an email notification + Mockito.verify(mockMailService, timeout(5000).times(1)).sendHtmlMessage( + eq(FAKE_EMAIL), + eq(FAKE_EMAIL), + eq(FAKE_SITEID + " JMS Error"), + contains(exceptionMsg)); } } diff --git a/src/test/java/org/nrg/containers/rest/LaunchRestApiTest.java b/src/test/java/org/nrg/containers/rest/LaunchRestApiTest.java index 7f79509e..6293804a 100644 --- a/src/test/java/org/nrg/containers/rest/LaunchRestApiTest.java +++ b/src/test/java/org/nrg/containers/rest/LaunchRestApiTest.java @@ -53,6 +53,7 @@ import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -185,7 +186,7 @@ public void setup() throws Exception { // Mock queuing when(mockContainerService.createContainerWorkflow(eq(FAKE_XNAT_ID), eq(FAKE_ROOT), eq(WRAPPER_NAME), - eq(""), eq(mockAdmin), any(String.class), any(Long.class), anyInt())) + eq(""), eq(mockAdmin), any(String.class), anyLong(), anyInt())) .thenReturn(mockWorkflow); // We have to match any resolved command because spring will add a csrf token to the inputs. I don't know how to get that token in advance. @@ -204,7 +205,7 @@ public void testLaunchWithQueryParams() throws Exception { ); when(mockContainerService.launchContainer( - anyString(), any(Long.class), anyString(), eq(WRAPPER_ID), eq(FAKE_ROOT), + any(), anyLong(), any(), eq(WRAPPER_ID), eq(FAKE_ROOT), any(Map.class), any(UserI.class)) ).thenReturn(report); @@ -231,7 +232,7 @@ public void testLaunchWithParamsInBody() throws Exception { ); when(mockContainerService.launchContainer( - any(String.class), any(Long.class), any(String.class), eq(WRAPPER_ID), eq(FAKE_ROOT), + any(), anyLong(), any(), eq(WRAPPER_ID), eq(FAKE_ROOT), any(Map.class), any(UserI.class)) ).thenReturn(report); @@ -275,7 +276,7 @@ public void testBulkLaunch() throws Exception { .build(); when(mockContainerService.bulkLaunch( - any(String.class), any(Long.class), any(String.class), eq(WRAPPER_ID), eq(FAKE_ROOT), + any(), anyLong(), any(), eq(WRAPPER_ID), eq(FAKE_ROOT), anyMap(), any(UserI.class) )).thenReturn(report); @@ -326,7 +327,7 @@ public void testGetLaunchUI() throws Exception { .commandName(COMMAND_NAME) .image(IMAGE) .build(); - when(mockCommandResolutionService.preResolve(eq(project), any(long.class), any(String.class), eq(WRAPPER_ID), anyMap(), eq(mockAdmin))) + when(mockCommandResolutionService.preResolve(eq(project), anyLong(), any(), eq(WRAPPER_ID), anyMap(), eq(mockAdmin))) .thenReturn(partiallyResolvedCommand); final LaunchUi expectedLaunchUi = LaunchUi.create(partiallyResolvedCommand, diff --git a/src/test/java/org/nrg/containers/secrets/SecretsTest.java b/src/test/java/org/nrg/containers/secrets/SecretsTest.java index 9ceeec1e..ad0f1cb2 100644 --- a/src/test/java/org/nrg/containers/secrets/SecretsTest.java +++ b/src/test/java/org/nrg/containers/secrets/SecretsTest.java @@ -89,6 +89,8 @@ public class SecretsTest { private UserDataCache userDataCache = Mockito.mock(UserDataCache.class); private KubernetesClientImpl kubernetesClient = Mockito.mock(KubernetesClientImpl.class); private DockerServerBase.DockerServer dockerServer = Mockito.mock(DockerServerBase.DockerServer.class); + private org.nrg.containers.services.DockerHubService dockerHubService = Mockito.mock(org.nrg.containers.services.DockerHubService.class); + private org.nrg.containers.api.KubernetesClientFactory kubernetesClientFactory = Mockito.mock(org.nrg.containers.api.KubernetesClientFactory.class); private UserI user = Mockito.mock(UserI.class); private Backend backend = null; @@ -320,18 +322,26 @@ public void testLaunchSwarmJobWithEnvSecret() throws Exception { .build(); // Class under test - final DockerControlApi dockerControlApi = Mockito.spy(DockerControlApi.class); + final DockerControlApi dockerControlApi = Mockito.spy(new DockerControlApi(dockerServerService, dockerHubService, kubernetesClientFactory)); // Mock out DockerControlApi#getDockerClient(dockerServer) final DockerClient dockerClient = Mockito.mock(DockerClient.class); // PowerMockito.doReturn(dockerClient).when(dockerControlApi, "getDockerClient", dockerServer); -// Mockito.doReturn(dockerClient).when(dockerControlApi) -// .getDockerClient(any(DockerServerBase.DockerServer.class)); - when(dockerControlApi.getDockerClient()).thenReturn(dockerClient); + Mockito.doReturn(dockerClient).when(dockerControlApi) + .getDockerClient(any(DockerServerBase.DockerServer.class)); // PowerMockito.doReturn(dockerServer).when(dockerControlApi, "getServer"); when(dockerServerService.getServer()).thenReturn(dockerServer); + // Mock authConfig - return a valid DockerHub so authConfig doesn't need to access config + final org.nrg.containers.model.dockerhub.DockerHubBase.DockerHub mockDockerHub = Mockito.mock(org.nrg.containers.model.dockerhub.DockerHubBase.DockerHub.class); + when(mockDockerHub.url()).thenReturn("docker.io"); + when(mockDockerHub.username()).thenReturn(""); + when(mockDockerHub.password()).thenReturn(""); + when(mockDockerHub.token()).thenReturn(""); + when(mockDockerHub.email()).thenReturn(""); + when(dockerHubService.getByUrl(any())).thenReturn(mockDockerHub); + // Mock out backend call to create service final String containerId = RandomStringUtils.randomAlphanumeric(5); final CreateServiceResponse resp = Mockito.mock(CreateServiceResponse.class); @@ -342,19 +352,6 @@ public void testLaunchSwarmJobWithEnvSecret() throws Exception { Mockito.when(dockerClient.createServiceCmd(Mockito.any(ServiceSpec.class))).thenReturn(cmd); // Call method under test -// PowerMockito.doCallRealMethod().when(dockerControlApi, "createDockerSwarmService", toCreate, dockerServer, DockerControlApi.NumReplicas.ZERO); - Method method = DockerControlApi.class.getDeclaredMethod("createDockerSwarmService", Container.class, DockerServerBase.DockerServer.class, DockerControlApi.NumReplicas.class ); - method.setAccessible(true); - - Mockito.doCallRealMethod().when(dockerControlApi); - method.invoke(dockerControlApi, toCreate, dockerServer, DockerControlApi.NumReplicas.ZERO); -// Mockito.doCallRealMethod().when(dockerControlApi) -// .getClass() -// .getDeclaredMethod("createDockerSwarmService", Container.class, DockerServerBase.DockerServer.class, DockerControlApi.NumReplicas.class) -// .invoke(dockerControlApi, toCreate, dockerServer, DockerControlApi.NumReplicas.ZERO); - -// PowerMockito.doCallRealMethod().when(dockerControlApi).create(toCreate, user); - Mockito.doCallRealMethod().when(dockerControlApi).create(toCreate, user); dockerControlApi.create(toCreate, user); // Capture call to backend api mock @@ -396,7 +393,7 @@ public void testLaunchDockerContainerWithEnvSecret() throws Exception { .build(); // Class under test - final DockerControlApi dockerControlApi = Mockito.mock(DockerControlApi.class); + final DockerControlApi dockerControlApi = Mockito.spy(new DockerControlApi(dockerServerService, dockerHubService, kubernetesClientFactory)); // Mock out DockerControlApi#getDockerClient(dockerServer) final DockerClient dockerClient = Mockito.mock(DockerClient.class); @@ -408,6 +405,32 @@ public void testLaunchDockerContainerWithEnvSecret() throws Exception { // PowerMockito.doReturn(dockerServer).when(dockerControlApi, "getServer"); when(dockerServerService.getServer()).thenReturn(dockerServer); + // Mock authConfig - return a valid DockerHub so authConfig doesn't need to access config + final org.nrg.containers.model.dockerhub.DockerHubBase.DockerHub mockDockerHub2 = Mockito.mock(org.nrg.containers.model.dockerhub.DockerHubBase.DockerHub.class); + when(mockDockerHub2.url()).thenReturn("docker.io"); + when(mockDockerHub2.username()).thenReturn(""); + when(mockDockerHub2.password()).thenReturn(""); + when(mockDockerHub2.token()).thenReturn(""); + when(mockDockerHub2.email()).thenReturn(""); + when(dockerHubService.getByUrl(any())).thenReturn(mockDockerHub2); + + // Mock listImagesCmd for getAllImages call + final com.github.dockerjava.api.command.ListImagesCmd listImagesCmd = Mockito.mock(com.github.dockerjava.api.command.ListImagesCmd.class); + when(dockerClient.listImagesCmd()).thenReturn(listImagesCmd); + when(listImagesCmd.exec()).thenReturn(java.util.Collections.emptyList()); + + // Mock pullImageCmd for pullImage call + final com.github.dockerjava.api.command.PullImageCmd pullImageCmd = Mockito.mock(com.github.dockerjava.api.command.PullImageCmd.class); + final com.github.dockerjava.api.command.PullImageResultCallback pullCallback = Mockito.mock(com.github.dockerjava.api.command.PullImageResultCallback.class); + when(dockerClient.pullImageCmd(any(String.class))).thenReturn(pullImageCmd); + when(pullImageCmd.start()).thenReturn(pullCallback); + when(pullCallback.awaitCompletion()).thenReturn(pullCallback); + + // Mock inspectImageCmd for getImageById call + final com.github.dockerjava.api.command.InspectImageCmd inspectImageCmd = Mockito.mock(com.github.dockerjava.api.command.InspectImageCmd.class); + final com.github.dockerjava.api.command.InspectImageResponse inspectImageResponse = Mockito.mock(com.github.dockerjava.api.command.InspectImageResponse.class); + when(dockerClient.inspectImageCmd(any(String.class))).thenReturn(inspectImageCmd); + when(inspectImageCmd.exec()).thenReturn(inspectImageResponse); // Mock out backend call to create service final String containerId = RandomStringUtils.randomAlphanumeric(5); @@ -423,14 +446,6 @@ public void testLaunchDockerContainerWithEnvSecret() throws Exception { Mockito.when(dockerClient.createContainerCmd(dockerImage)).thenReturn(cmd); // Call method under test -// PowerMockito.doCallRealMethod().when(dockerControlApi, "crxeateDockerContainer", toCreate, dockerServer); - Method method = DockerControlApi.class.getDeclaredMethod("createDockerContainer", CreateContainerCmd.class, DockerServerBase.DockerServer.class); - method.setAccessible(true); - Mockito.doCallRealMethod().when(dockerControlApi); - method.invoke(dockerControlApi, toCreate, dockerServer); - -// PowerMockito.doCallRealMethod().when(dockerControlApi).create(toCreate, user); - Mockito.doCallRealMethod().when(dockerControlApi).create(toCreate, user); dockerControlApi.create(toCreate, user); // Capture call to backend api mock From a91b116710aeb688a76fc2ba83993568eaa8d76e Mon Sep 17 00:00:00 2001 From: Automated Build Date: Thu, 19 Mar 2026 14:10:36 -0500 Subject: [PATCH 34/44] Update to version 3.8.0-RC-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f146b900..54b9a3b3 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.8.0-SNAPSHOT" +version "3.8.0-RC-SNAPSHOT" java { sourceCompatibility = JavaVersion.VERSION_1_8 From 665e7f715ca15c7bab35efb24ec537d562b22506 Mon Sep 17 00:00:00 2001 From: Automated Build Date: Fri, 20 Mar 2026 14:27:58 -0500 Subject: [PATCH 35/44] Update to version 3.8.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 54b9a3b3..64c5ea53 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ plugins { } group "org.nrg.xnatx.plugins" -version "3.8.0-RC-SNAPSHOT" +version "3.8.0" java { sourceCompatibility = JavaVersion.VERSION_1_8 From 4c944652b4ec16d517820530ce684143d2b9e1e4 Mon Sep 17 00:00:00 2001 From: Bin Zhang Date: Mon, 23 Mar 2026 09:42:51 -0500 Subject: [PATCH 36/44] Force docker-java on 3.4.0.1 version --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 3e01566c..aeca313d 100644 --- a/build.gradle +++ b/build.gradle @@ -61,6 +61,7 @@ configurations { implementation.extendsFrom(implementAndInclude) all { resolutionStrategy.force "org.mockito:mockito-core:${vMockito}" + resolutionStrategy.force "com.github.docker-java:docker-java-api:${vDockerJava}" exclude group: "org.bouncycastle" } From 907f32f351d279fa38185807d56cd8d638334ffd Mon Sep 17 00:00:00 2001 From: Bin Zhang Date: Mon, 23 Mar 2026 10:28:20 -0500 Subject: [PATCH 37/44] Fix unitTest --- .../java/org/nrg/containers/config/MockJmsConfig.java | 5 +++-- .../org/nrg/containers/services/ContainerLogTest.java | 10 ++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/test/java/org/nrg/containers/config/MockJmsConfig.java b/src/test/java/org/nrg/containers/config/MockJmsConfig.java index 5f7a998f..c167e531 100644 --- a/src/test/java/org/nrg/containers/config/MockJmsConfig.java +++ b/src/test/java/org/nrg/containers/config/MockJmsConfig.java @@ -14,6 +14,7 @@ import org.nrg.xdat.security.services.UserManagementServiceI; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; import org.springframework.jms.core.BrowserCallback; import org.springframework.jms.core.JmsTemplate; @@ -55,9 +56,9 @@ public Destination containerFinalizingRequest() { @Bean @Primary public JmsTemplate mockJmsTemplate(Destination containerStagingRequest, - final ContainerStagingRequestListener containerStagingRequestListener, + @Lazy final ContainerStagingRequestListener containerStagingRequestListener, Destination containerFinalizingRequest, - final ContainerFinalizingRequestListener containerFinalizingRequestListener, + @Lazy final ContainerFinalizingRequestListener containerFinalizingRequestListener, ExecutorService executorService, final SiteConfigPreferences siteConfigPreferences, final NotificationsPreferences notificationsPreferences, diff --git a/src/test/java/org/nrg/containers/services/ContainerLogTest.java b/src/test/java/org/nrg/containers/services/ContainerLogTest.java index 302ea15d..92bcb0e5 100644 --- a/src/test/java/org/nrg/containers/services/ContainerLogTest.java +++ b/src/test/java/org/nrg/containers/services/ContainerLogTest.java @@ -10,7 +10,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.rules.TemporaryFolder; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; @@ -22,9 +21,8 @@ import org.junit.runners.Parameterized.Parameters; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import org.nrg.containers.api.ContainerControlApi; import org.nrg.containers.api.LogType; import org.nrg.containers.model.container.auto.Container; @@ -55,12 +53,12 @@ @Slf4j @RunWith(Enclosed.class) public class ContainerLogTest { - @ExtendWith(MockitoExtension.class) @Ignore - @MockitoSettings(strictness = Strictness.WARN) @RunWith(JUnit4.class) public static class BaseTest { + @Rule public MockitoRule rule = MockitoJUnit.rule(); + @Rule public TestRule watcher = new TestWatcher() { protected void starting(Description description) { From a2ae71df208141a004848316effa92bd24aeab29 Mon Sep 17 00:00:00 2001 From: Bin Zhang Date: Mon, 1 Jun 2026 20:32:37 -0500 Subject: [PATCH 38/44] Fix compile issue for 1.10.1-SNAPSHOT monorepo --- build.gradle | 46 +++++++++++++--------------------------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/build.gradle b/build.gradle index aeca313d..93b04c97 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - vXnat = "1.10.0-RC-SNAPSHOT" + vXnat = "1.10.1-SNAPSHOT" pluginAppName = "XNAT Container Service" } } @@ -13,12 +13,10 @@ plugins { id "io.spring.dependency-management" version "1.1.7" id "io.franzbecker.gradle-lombok" version "5.0.0" id "net.linguica.maven-settings" version "0.5" - id("org.openrewrite.rewrite") version("7.0.4") - } group "org.nrg.xnatx.plugins" -version "3.8.0-java21-RC-SNAPSHOT" +version "3.8.0-RC-SNAPSHOT" java { sourceCompatibility = 21 @@ -40,7 +38,6 @@ dependencyManagement { imports { mavenBom("org.nrg:parent:${vXnat}") { bomProperties([ - // "mockito.version": vMockito, "javassist.version": vJavassist ]) } @@ -51,10 +48,7 @@ def vDockerJava = "3.4.0.1" def vAutoValue = "1.5.4" def vKubernetesClient = "15.0.+" def vPowerMock = "2.0.9" -def vGson = dependencyManagement.importedProperties["gson.version"] -def vJackson = dependencyManagement.importedProperties["jackson.version"] -def vSpringSecurity = dependencyManagement.importedProperties["spring-security.version"] -def vActiveMQ = dependencyManagement.importedProperties["activemq.version"] +// importedProperties not available from Gradle java-platform BOM — versions managed by BOM directly // Use this configuration to put dependencies into the fat jar configurations { @@ -74,17 +68,14 @@ tasks.withType(Test).configureEach { } lombok { - version = dependencyManagement.importedProperties["lombok.version"] as String - sha256 = dependencyManagement.importedProperties["lombok.checksum"] as String + version = "1.18.34" + sha256 = "1ea5ad6c6afcff902d75072b2aaa6695585aebee9f12127e15c0036ba95d2918" } dependencies { annotationProcessor "org.nrg:framework" annotationProcessor "com.google.auto.value:auto-value:${vAutoValue}" - rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.1.0") - - compileOnly "com.google.auto.value:auto-value:${vAutoValue}" implementAndInclude "com.github.docker-java:docker-java-core:${vDockerJava}" @@ -122,9 +113,7 @@ dependencies { implementation ("org.nrg.xdat:core") { transitive = false } - implementation ("org.nrg.xnat:xnat-data-models") { - transitive = false - } + // xnat-data-models merged into org.nrg.xnat:web in monorepo — no longer a separate artifact implementation "org.nrg:prefs" implementation ("org.nrg:notify") { exclude group: "cglib" @@ -148,8 +137,8 @@ dependencies { implementation "org.springframework:spring-webmvc" implementation "org.springframework:spring-jms" implementation "org.springframework:spring-messaging" - implementation "javax.jms:javax.jms-api:2.0.1" - implementation "org.apache.activemq:activemq-all:${vActiveMQ}" + implementation "javax.jms:javax.jms-api" + implementation "org.apache.activemq:activemq-all" implementation "org.aspectj:aspectjweaver" implementation "io.springfox:springfox-swagger2" @@ -161,8 +150,8 @@ dependencies { implementation "com.fasterxml.jackson.core:jackson-annotations" implementation "com.fasterxml.jackson.core:jackson-core" implementation "com.fasterxml.jackson.core:jackson-databind" - implementation "com.fasterxml.jackson.module:jackson-module-jaxb-annotations:${vJackson}" - implementation "com.fasterxml.jackson.datatype:jackson-datatype-guava:${vJackson}" + implementation "com.fasterxml.jackson.module:jackson-module-jaxb-annotations" + implementation "com.fasterxml.jackson.datatype:jackson-datatype-guava" implementation "com.jayway.jsonpath:json-path" @@ -180,7 +169,7 @@ dependencies { implementation ("org.apache.velocity:velocity") { transitive = false } - implementation ("xalan:xalan") { + implementation ("xalan:xalan:2.7.2") { transitive = false } @@ -191,7 +180,7 @@ dependencies { runtimeOnly "org.apache.commons:commons-compress" runtimeOnly "org.apache.commons:commons-lang3" runtimeOnly "org.apache.httpcomponents:httpclient" - runtimeOnly "com.google.code.gson:gson:${vGson}" + runtimeOnly "com.google.code.gson:gson" runtimeOnly "com.google.code.findbugs:jsr305" runtimeOnly "org.yaml:snakeyaml:1.15+" @@ -202,7 +191,7 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation "org.hamcrest:hamcrest-library" testImplementation "org.springframework:spring-test" - testImplementation "org.springframework.security:spring-security-test:${vSpringSecurity}" + testImplementation "org.springframework.security:spring-security-test" testImplementation "org.springframework.security:spring-security-config" testImplementation "org.mockito:mockito-core:${vMockito}" testImplementation 'org.mockito:mockito-junit-jupiter:5.0.0' // JUnit 5 integration for Mockito @@ -213,20 +202,11 @@ dependencies { testImplementation "org.springframework:spring-orm" testImplementation "org.apache.commons:commons-dbcp2" testImplementation "commons-net:commons-net" -// testImplementation "org.powermock:powermock-module-junit4:${vPowerMock}" -// testImplementation "org.powermock:powermock-api-mockito2:${vPowerMock}" testImplementation "org.awaitility:awaitility:${vAwaitility}" testImplementation "org.testcontainers:junit-jupiter:1.19.0" testImplementation "org.testcontainers:postgresql:1.19.0" } -rewrite { - // activeRecipe("org.openrewrite.java.testing.mockito.Mockito1to5Migration") - activeRecipe("org.openrewrite.java.testing.mockito.ReplacePowerMockito") - - setExportDatatables(true) -} - test { useJUnitPlatform() systemProperty "integration", System.getProperty('integration') == "true" From 0126ecd2011a4fb6f0c287b052b5ddd7e06eb75f Mon Sep 17 00:00:00 2001 From: Bin Zhang Date: Wed, 24 Jun 2026 07:35:46 +0800 Subject: [PATCH 39/44] xnat-8763: bump to Gradle 9.4.1 + Gradle 9 manifest API - Gradle wrapper 8.5 -> 9.4.1 - Replace top-level manifest {} (removed in Gradle 9) with attribute map gitManifestAttrs; jar tasks use 'attributes gitManifestAttrs' Builds on top of using-monorepo branch's 1.10.1-SNAPSHOT adaptation (merged in via merge commit 71195a42). --- build.gradle | 34 ++++++++++++------------ gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/build.gradle b/build.gradle index 6fbfd531..edc84d42 100644 --- a/build.gradle +++ b/build.gradle @@ -250,19 +250,19 @@ try { isCleanTag = false } -ext.gitManifest = manifest { - attributes "Application-Name": pluginAppName, - "Build-Date": buildDate, - "Build-Number": buildNumber, - "Implementation-Version": project.version, - "Implementation-Sha": gitHash, - "Implementation-Sha-Full": gitHashFull, - "Implementation-Commit": commitDistance, - "Implementation-LastTag": lastTag, - "Implementation-Branch": branchName, - "Implementation-CleanTag": isCleanTag, - "Implementation-Dirty": isDirty -} +ext.gitManifestAttrs = [ + "Application-Name" : pluginAppName, + "Build-Date" : buildDate, + "Build-Number" : buildNumber, + "Implementation-Version" : project.version, + "Implementation-Sha" : gitHash, + "Implementation-Sha-Full" : gitHashFull, + "Implementation-Commit" : commitDistance, + "Implementation-LastTag" : lastTag, + "Implementation-Branch" : branchName, + "Implementation-CleanTag" : isCleanTag, + "Implementation-Dirty" : isDirty +] task fatJar(type: Jar) { zip64 true @@ -276,7 +276,7 @@ task fatJar(type: Jar) { } duplicatesStrategy "exclude" manifest { - from gitManifest + attributes gitManifestAttrs } with jar } @@ -288,7 +288,7 @@ sourceSets.main.java { jar{ manifest { - from gitManifest + attributes gitManifestAttrs } doLast { if (!gradle.taskGraph.hasTask(":fatJar")) { @@ -300,7 +300,7 @@ jar{ task sourceJar(type: Jar, dependsOn: classes) { archiveClassifier.set("sources") manifest { - from gitManifest + attributes gitManifestAttrs } from sourceSets.main.allSource } @@ -308,7 +308,7 @@ task sourceJar(type: Jar, dependsOn: classes) { task javadocJar(type: Jar, dependsOn: javadoc) { archiveClassifier.set("javadoc") manifest { - from gitManifest + attributes gitManifestAttrs } from javadoc.destinationDir } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a5952066..221c4f98 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From ef39cf36e5e9590d060ad10bdf9496a899e002d5 Mon Sep 17 00:00:00 2001 From: Bin Zhang Date: Wed, 24 Jun 2026 07:35:46 +0800 Subject: [PATCH 40/44] xnat-8763: add CI workflows (build-publish + cut-rc + promote) build-publish triggers on dev (not develop) for container-service. release-cut-rc defaults source_branch to dev. --- .github/workflows/build-publish.yml | 39 ++++++ .github/workflows/release-cut-rc.yml | 187 ++++++++++++++++++++++++++ .github/workflows/release-promote.yml | 165 +++++++++++++++++++++++ 3 files changed, 391 insertions(+) create mode 100644 .github/workflows/build-publish.yml create mode 100644 .github/workflows/release-cut-rc.yml create mode 100644 .github/workflows/release-promote.yml diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml new file mode 100644 index 00000000..5838ec22 --- /dev/null +++ b/.github/workflows/build-publish.yml @@ -0,0 +1,39 @@ +name: Build and Publish + +# Thin caller around the shared reusable workflow in +# NrgXnat/xnat-ci-workflows. Plugin variant — no GHCR Docker image +# (plugins ship a jar to JFrog only). + +on: + push: + branches: + - main + - dev # container-service uses 'dev' instead of 'develop' + - 'releases/*' # RC / release branches, e.g. releases/1.6.0-rc + workflow_dispatch: + inputs: + publish_jfrog: + description: 'Publish jar to JFrog Artifactory (nrgxnat)' + type: boolean + default: false + +permissions: + contents: read + +jobs: + build-publish: + uses: NrgXnat/xnat-ci-workflows/.github/workflows/gradle-build-publish.yml@v1 + with: + # ──── Artifact ──── + artifact-glob: 'build/libs/*.jar' + artifact-name: 'container-service-jar' + + # ──── JFrog ──── + # On any push to a triggered branch: publish. On dispatch: only when publish_jfrog ticked. + publish-jfrog: ${{ github.event_name == 'push' || inputs.publish_jfrog }} + + # publish-ghcr deliberately omitted — defaults to false in the reusable workflow. + + # secrets: inherit passes XNAT_ARTIFACTORY_USER/TOKEN (org-level) and + # the auto-injected GITHUB_TOKEN through to the reusable workflow. + secrets: inherit diff --git a/.github/workflows/release-cut-rc.yml b/.github/workflows/release-cut-rc.yml new file mode 100644 index 00000000..d97d4603 --- /dev/null +++ b/.github/workflows/release-cut-rc.yml @@ -0,0 +1,187 @@ +name: Release - Cut RC Branch + +# Cuts an RC branch from `source_branch` (default: develop) and bumps +# source_branch to the next dev version. Plugin variant of the monorepo +# release-cut-rc workflow — no dicom-edit6 bump; version lives in +# build.gradle (groovy DSL: `version "..."`). +# +# Inputs: +# next_dev_version - version source_branch should become after cutting +# (e.g. 1.7.0-SNAPSHOT, MUST end with -SNAPSHOT) +# source_branch - branch to cut from (default: develop) +# +# Produces: +# - new branch releases/-rc with version "-RC-SNAPSHOT" +# - source_branch with version "" + +on: + workflow_dispatch: + inputs: + next_dev_version: + description: 'Next develop SNAPSHOT version (e.g. 1.7.0-SNAPSHOT)' + type: string + required: true + source_branch: + description: 'Branch to cut from (default: dev)' + type: string + required: false + default: dev + trigger_build: + description: 'After cut, dispatch build/publish on RC and source_branch' + type: boolean + required: false + default: true + +permissions: + contents: write + actions: write # dispatch build-publish workflow + +jobs: + cut-rc: + runs-on: ubuntu-latest + + steps: + - name: Checkout ${{ inputs.source_branch }} + uses: actions/checkout@v4 + with: + ref: ${{ inputs.source_branch }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Read current state and derive versions + id: vars + run: | + set -euo pipefail + + # Groovy DSL: `version "1.6.0-SNAPSHOT"` + CURRENT_VERSION="$(grep -E '^version ' build.gradle | head -n1 | sed -E 's/^version "(.+)"$/\1/')" + if [[ -z "${CURRENT_VERSION}" ]]; then + echo "::error::Could not read version from build.gradle" + exit 1 + fi + + RELEASE_VERSION="${CURRENT_VERSION%-SNAPSHOT}" + RC_VERSION="${RELEASE_VERSION}-RC-SNAPSHOT" + RC_BRANCH="releases/${RELEASE_VERSION}-rc" + + { + echo "current_version=${CURRENT_VERSION}" + echo "release_version=${RELEASE_VERSION}" + echo "rc_version=${RC_VERSION}" + echo "rc_branch=${RC_BRANCH}" + } >> "$GITHUB_OUTPUT" + + echo "::notice::source=${{ inputs.source_branch }} current=${CURRENT_VERSION} → rc=${RC_VERSION} branch=${RC_BRANCH}" + + - name: Validate + env: + NEXT_DEV_VERSION: ${{ inputs.next_dev_version }} + CURRENT_VERSION: ${{ steps.vars.outputs.current_version }} + RC_BRANCH: ${{ steps.vars.outputs.rc_branch }} + run: | + set -euo pipefail + + if [[ "${CURRENT_VERSION}" != *-SNAPSHOT ]]; then + echo "::error::source_branch is on ${CURRENT_VERSION}; expected -SNAPSHOT. Refusing to cut RC from a release/RC branch." + exit 1 + fi + if [[ "${NEXT_DEV_VERSION}" != *-SNAPSHOT ]]; then + echo "::error::next_dev_version=${NEXT_DEV_VERSION} must end with -SNAPSHOT" + exit 1 + fi + if [[ "${NEXT_DEV_VERSION}" == "${CURRENT_VERSION}" ]]; then + echo "::error::next_dev_version equals current version (${CURRENT_VERSION}); nothing to bump" + exit 1 + fi + if git ls-remote --exit-code origin "refs/heads/${RC_BRANCH}" >/dev/null 2>&1; then + echo "::error::RC branch ${RC_BRANCH} already exists on origin" + exit 1 + fi + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Cut RC branch + env: + CURRENT_VERSION: ${{ steps.vars.outputs.current_version }} + RELEASE_VERSION: ${{ steps.vars.outputs.release_version }} + RC_VERSION: ${{ steps.vars.outputs.rc_version }} + RC_BRANCH: ${{ steps.vars.outputs.rc_branch }} + run: | + set -euo pipefail + + git switch -c "${RC_BRANCH}" + + sed -i "s|^version \"${CURRENT_VERSION}\"\$|version \"${RC_VERSION}\"|" build.gradle + grep -E '^version ' build.gradle + git add build.gradle + + git commit -m "chore(release): cut RC for ${RELEASE_VERSION}" + git push -u origin "${RC_BRANCH}" + + - name: Bump source_branch to next dev version + env: + SOURCE_BRANCH: ${{ inputs.source_branch }} + CURRENT_VERSION: ${{ steps.vars.outputs.current_version }} + NEXT_DEV_VERSION: ${{ inputs.next_dev_version }} + RELEASE_VERSION: ${{ steps.vars.outputs.release_version }} + run: | + set -euo pipefail + + git switch "${SOURCE_BRANCH}" + + sed -i "s|^version \"${CURRENT_VERSION}\"\$|version \"${NEXT_DEV_VERSION}\"|" build.gradle + grep -E '^version ' build.gradle + + git add build.gradle + git commit -m "chore(release): bump ${SOURCE_BRANCH} to ${NEXT_DEV_VERSION} after cutting ${RELEASE_VERSION}-rc" + git push origin "${SOURCE_BRANCH}" + + - name: Trigger build/publish on RC branch and source_branch + if: ${{ inputs.trigger_build }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + RC_BRANCH: ${{ steps.vars.outputs.rc_branch }} + SOURCE_BRANCH: ${{ inputs.source_branch }} + run: | + set -euo pipefail + + echo "::group::Dispatch build-publish on RC branch (${RC_BRANCH})" + gh workflow run build-publish.yml --repo "${REPO}" \ + --ref "${RC_BRANCH}" \ + -f publish_jfrog=true + echo "::endgroup::" + + echo "::group::Dispatch build-publish on source_branch (${SOURCE_BRANCH})" + gh workflow run build-publish.yml --repo "${REPO}" \ + --ref "${SOURCE_BRANCH}" \ + -f publish_jfrog=true + echo "::endgroup::" + + - name: Job summary + env: + SOURCE_BRANCH: ${{ inputs.source_branch }} + NEXT_DEV_VERSION: ${{ inputs.next_dev_version }} + RC_BRANCH: ${{ steps.vars.outputs.rc_branch }} + RC_VERSION: ${{ steps.vars.outputs.rc_version }} + RELEASE_VERSION: ${{ steps.vars.outputs.release_version }} + SERVER_URL: ${{ github.server_url }} + REPOSITORY: ${{ github.repository }} + run: | + { + echo "## Release RC cut: ${RELEASE_VERSION}" + echo "" + echo "**RC branch**: [\`${RC_BRANCH}\`](${SERVER_URL}/${REPOSITORY}/tree/${RC_BRANCH})" + echo "" + echo "| file | value |" + echo "|------|-------|" + echo "| \`build.gradle\` version | \`${RC_VERSION}\` |" + echo "" + echo "**${SOURCE_BRANCH}** bumped to: \`${NEXT_DEV_VERSION}\`" + echo "" + echo "### Next step" + echo "Stabilize on \`${RC_BRANCH}\`; when ready, dispatch the release-promote workflow." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release-promote.yml b/.github/workflows/release-promote.yml new file mode 100644 index 00000000..0f54e058 --- /dev/null +++ b/.github/workflows/release-promote.yml @@ -0,0 +1,165 @@ +name: Release - Promote RC to Main + +# Promotes a stabilized RC branch to a tagged release on main. +# Plugin variant of the monorepo release-promote workflow — no +# dicom-edit6 sync; version lives in build.gradle (groovy DSL: `version "..."`). +# +# Inputs: +# rc_branch - RC branch to promote (e.g. releases/1.6.0-rc); REQUIRED +# target_main - branch to merge into (default main) +# trigger_build - dispatch build/publish on target_main +# +# Produces: +# - RC branch with -RC-SNAPSHOT suffix stripped (release version string) +# - target_main fast-merged (--no-ff) with RC + tag (no v prefix) + +on: + workflow_dispatch: + inputs: + rc_branch: + description: 'RC branch to promote (e.g. releases/1.6.0-rc)' + type: string + required: true + target_main: + description: 'Branch to merge into (default main)' + type: string + required: false + default: main + trigger_build: + description: 'After promote, dispatch build/publish on target_main' + type: boolean + required: false + default: true + +permissions: + contents: write # push RC/main + create tag + actions: write # dispatch build-publish workflow + +jobs: + promote: + runs-on: ubuntu-latest + + steps: + - name: Checkout ${{ inputs.rc_branch }} + uses: actions/checkout@v4 + with: + ref: ${{ inputs.rc_branch }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Read RC state and derive release version + id: vars + env: + RC_BRANCH: ${{ inputs.rc_branch }} + run: | + set -euo pipefail + + RC_VERSION="$(grep -E '^version ' build.gradle | head -n1 | sed -E 's/^version "(.+)"$/\1/')" + if [[ -z "${RC_VERSION}" ]]; then + echo "::error::Could not read version from build.gradle" + exit 1 + fi + RELEASE_VERSION="${RC_VERSION%-RC-SNAPSHOT}" + + { + echo "rc_version=${RC_VERSION}" + echo "release_version=${RELEASE_VERSION}" + } >> "$GITHUB_OUTPUT" + + echo "::notice::rc=${RC_BRANCH} rc_version=${RC_VERSION} → release=${RELEASE_VERSION}" + + - name: Validate + env: + RC_BRANCH: ${{ inputs.rc_branch }} + RC_VERSION: ${{ steps.vars.outputs.rc_version }} + RELEASE_VERSION: ${{ steps.vars.outputs.release_version }} + run: | + set -euo pipefail + + if [[ ! "${RC_BRANCH}" =~ ^releases/.+-rc$ ]]; then + echo "::error::rc_branch=${RC_BRANCH} must match 'releases/*-rc'" + exit 1 + fi + if [[ "${RC_VERSION}" != *-RC-SNAPSHOT ]]; then + echo "::error::RC branch version=${RC_VERSION} must end with -RC-SNAPSHOT" + exit 1 + fi + if git ls-remote --exit-code --tags origin "refs/tags/${RELEASE_VERSION}" >/dev/null 2>&1; then + echo "::error::Tag ${RELEASE_VERSION} already exists on origin" + exit 1 + fi + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Finalize RC branch (strip -RC-SNAPSHOT) + env: + RC_BRANCH: ${{ inputs.rc_branch }} + RC_VERSION: ${{ steps.vars.outputs.rc_version }} + RELEASE_VERSION: ${{ steps.vars.outputs.release_version }} + run: | + set -euo pipefail + + sed -i "s|^version \"${RC_VERSION}\"\$|version \"${RELEASE_VERSION}\"|" build.gradle + grep -E '^version ' build.gradle + git add build.gradle + + git commit -m "chore(release): finalize ${RELEASE_VERSION}" + git push origin "HEAD:${RC_BRANCH}" + + - name: Merge RC into target_main and tag + env: + RC_BRANCH: ${{ inputs.rc_branch }} + TARGET_MAIN: ${{ inputs.target_main }} + RELEASE_VERSION: ${{ steps.vars.outputs.release_version }} + run: | + set -euo pipefail + + git fetch origin "${TARGET_MAIN}" + git switch "${TARGET_MAIN}" + git pull --ff-only origin "${TARGET_MAIN}" + + # --no-ff to create an explicit merge commit on release boundary; + # set -e + no -X strategy → fail fast on conflict (release manager intervenes) + git merge --no-ff "origin/${RC_BRANCH}" -m "release: ${RELEASE_VERSION}" + + git tag "${RELEASE_VERSION}" # no 'v' prefix (matches monorepo tag convention) + git push origin "${TARGET_MAIN}" + git push origin "refs/tags/${RELEASE_VERSION}" + + - name: Trigger build/publish on target_main + if: ${{ inputs.trigger_build }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + TARGET_MAIN: ${{ inputs.target_main }} + run: | + set -euo pipefail + + echo "::group::Dispatch build-publish on target_main (${TARGET_MAIN})" + gh workflow run build-publish.yml --repo "${REPO}" \ + --ref "${TARGET_MAIN}" \ + -f publish_jfrog=true + echo "::endgroup::" + + - name: Job summary + env: + RC_BRANCH: ${{ inputs.rc_branch }} + TARGET_MAIN: ${{ inputs.target_main }} + RELEASE_VERSION: ${{ steps.vars.outputs.release_version }} + SERVER_URL: ${{ github.server_url }} + REPOSITORY: ${{ github.repository }} + run: | + { + echo "## Release promoted: ${RELEASE_VERSION}" + echo "" + echo "**Tag**: [\`${RELEASE_VERSION}\`](${SERVER_URL}/${REPOSITORY}/releases/tag/${RELEASE_VERSION})" + echo "**${TARGET_MAIN}**: [HEAD](${SERVER_URL}/${REPOSITORY}/tree/${TARGET_MAIN})" + echo "**${RC_BRANCH}**: kept (finalized to release strings; available for future patches)" + echo "" + echo "| file | value |" + echo "|------|-------|" + echo "| \`build.gradle\` version | \`${RELEASE_VERSION}\` |" + } >> "$GITHUB_STEP_SUMMARY" From d44c77bae7d8f81e9cee956dcec0181b37f8dda7 Mon Sep 17 00:00:00 2001 From: Bin Zhang Date: Thu, 25 Jun 2026 00:03:28 +0800 Subject: [PATCH 41/44] xnat-8763: align publishing.repositories credentials with monorepo convention Read artifactoryUser/Token via project.findProperty (matches CI workflow's ORG_GRADLE_PROJECT_artifactoryUser/Token env-var mapping), with ARTIFACTORY_USER/TOKEN env-var fallback. Standardize URL to -local repos. Mirrors monorepo's buildSrc/buildlogic.java-common-conventions.gradle pattern. --- build.gradle | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index edc84d42..11fd5b5d 100644 --- a/build.gradle +++ b/build.gradle @@ -359,13 +359,17 @@ publishing { } repositories { maven { - if (project.version.endsWith("-SNAPSHOT")) { - url "https://nrgxnat.jfrog.io/nrgxnat/libs-snapshot-local" - } else { - url "https://nrgxnat.jfrog.io/nrgxnat/libs-release-local" - } - // The value for name must match in ~/.m2/settings.xml name = "XNAT_Artifactory" + url = project.version.toString().endsWith("-SNAPSHOT") + ? "https://nrgxnat.jfrog.io/nrgxnat/libs-snapshot-local" + : "https://nrgxnat.jfrog.io/nrgxnat/libs-release-local" + // CI injects ORG_GRADLE_PROJECT_artifactoryUser/Token env vars (Gradle maps + // these to project properties). Local devs can set artifactoryUser/Token in + // ~/.gradle/gradle.properties. Mirrors monorepo's buildSrc convention. + credentials { + username = (project.findProperty("artifactoryUser") ?: System.getenv("ARTIFACTORY_USER")) ?: "" + password = (project.findProperty("artifactoryToken") ?: System.getenv("ARTIFACTORY_TOKEN")) ?: "" + } } } } From c7bf42aedb062842f07585c53cc74bcd05683c5d Mon Sep 17 00:00:00 2001 From: Bin Zhang Date: Thu, 25 Jun 2026 22:01:27 +0800 Subject: [PATCH 42/44] xnat-8763: switch release-cut-rc + release-promote to thin caller pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace 187-line + 165-line inline workflows with ~37-line callers that delegate to NrgXnat/xnat-ci-workflows reusable workflows: - gradle-release-cut-rc.yml@v1 - gradle-release-promote.yml@v1 Total YAML saved per plugin: ~278 lines. Behavior unchanged — the reusable workflows were extracted verbatim from the previous inline scripts. --- .github/workflows/release-cut-rc.yml | 172 ++------------------------ .github/workflows/release-promote.yml | 148 ++-------------------- 2 files changed, 21 insertions(+), 299 deletions(-) diff --git a/.github/workflows/release-cut-rc.yml b/.github/workflows/release-cut-rc.yml index d97d4603..17090344 100644 --- a/.github/workflows/release-cut-rc.yml +++ b/.github/workflows/release-cut-rc.yml @@ -1,24 +1,15 @@ name: Release - Cut RC Branch -# Cuts an RC branch from `source_branch` (default: develop) and bumps -# source_branch to the next dev version. Plugin variant of the monorepo -# release-cut-rc workflow — no dicom-edit6 bump; version lives in -# build.gradle (groovy DSL: `version "..."`). -# -# Inputs: -# next_dev_version - version source_branch should become after cutting -# (e.g. 1.7.0-SNAPSHOT, MUST end with -SNAPSHOT) -# source_branch - branch to cut from (default: develop) -# -# Produces: -# - new branch releases/-rc with version "-RC-SNAPSHOT" -# - source_branch with version "" +# Thin caller around the shared reusable workflow in NrgXnat/xnat-ci-workflows. +# Cuts a `releases/-rc` branch from source_branch and bumps +# source_branch to the next dev version. All release-cut logic lives in the +# reusable workflow; this file only defines our triggers + inputs. on: workflow_dispatch: inputs: next_dev_version: - description: 'Next develop SNAPSHOT version (e.g. 1.7.0-SNAPSHOT)' + description: 'Next dev SNAPSHOT version (e.g. 1.7.0-SNAPSHOT)' type: string required: true source_branch: @@ -38,150 +29,9 @@ permissions: jobs: cut-rc: - runs-on: ubuntu-latest - - steps: - - name: Checkout ${{ inputs.source_branch }} - uses: actions/checkout@v4 - with: - ref: ${{ inputs.source_branch }} - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Read current state and derive versions - id: vars - run: | - set -euo pipefail - - # Groovy DSL: `version "1.6.0-SNAPSHOT"` - CURRENT_VERSION="$(grep -E '^version ' build.gradle | head -n1 | sed -E 's/^version "(.+)"$/\1/')" - if [[ -z "${CURRENT_VERSION}" ]]; then - echo "::error::Could not read version from build.gradle" - exit 1 - fi - - RELEASE_VERSION="${CURRENT_VERSION%-SNAPSHOT}" - RC_VERSION="${RELEASE_VERSION}-RC-SNAPSHOT" - RC_BRANCH="releases/${RELEASE_VERSION}-rc" - - { - echo "current_version=${CURRENT_VERSION}" - echo "release_version=${RELEASE_VERSION}" - echo "rc_version=${RC_VERSION}" - echo "rc_branch=${RC_BRANCH}" - } >> "$GITHUB_OUTPUT" - - echo "::notice::source=${{ inputs.source_branch }} current=${CURRENT_VERSION} → rc=${RC_VERSION} branch=${RC_BRANCH}" - - - name: Validate - env: - NEXT_DEV_VERSION: ${{ inputs.next_dev_version }} - CURRENT_VERSION: ${{ steps.vars.outputs.current_version }} - RC_BRANCH: ${{ steps.vars.outputs.rc_branch }} - run: | - set -euo pipefail - - if [[ "${CURRENT_VERSION}" != *-SNAPSHOT ]]; then - echo "::error::source_branch is on ${CURRENT_VERSION}; expected -SNAPSHOT. Refusing to cut RC from a release/RC branch." - exit 1 - fi - if [[ "${NEXT_DEV_VERSION}" != *-SNAPSHOT ]]; then - echo "::error::next_dev_version=${NEXT_DEV_VERSION} must end with -SNAPSHOT" - exit 1 - fi - if [[ "${NEXT_DEV_VERSION}" == "${CURRENT_VERSION}" ]]; then - echo "::error::next_dev_version equals current version (${CURRENT_VERSION}); nothing to bump" - exit 1 - fi - if git ls-remote --exit-code origin "refs/heads/${RC_BRANCH}" >/dev/null 2>&1; then - echo "::error::RC branch ${RC_BRANCH} already exists on origin" - exit 1 - fi - - - name: Configure git - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - - name: Cut RC branch - env: - CURRENT_VERSION: ${{ steps.vars.outputs.current_version }} - RELEASE_VERSION: ${{ steps.vars.outputs.release_version }} - RC_VERSION: ${{ steps.vars.outputs.rc_version }} - RC_BRANCH: ${{ steps.vars.outputs.rc_branch }} - run: | - set -euo pipefail - - git switch -c "${RC_BRANCH}" - - sed -i "s|^version \"${CURRENT_VERSION}\"\$|version \"${RC_VERSION}\"|" build.gradle - grep -E '^version ' build.gradle - git add build.gradle - - git commit -m "chore(release): cut RC for ${RELEASE_VERSION}" - git push -u origin "${RC_BRANCH}" - - - name: Bump source_branch to next dev version - env: - SOURCE_BRANCH: ${{ inputs.source_branch }} - CURRENT_VERSION: ${{ steps.vars.outputs.current_version }} - NEXT_DEV_VERSION: ${{ inputs.next_dev_version }} - RELEASE_VERSION: ${{ steps.vars.outputs.release_version }} - run: | - set -euo pipefail - - git switch "${SOURCE_BRANCH}" - - sed -i "s|^version \"${CURRENT_VERSION}\"\$|version \"${NEXT_DEV_VERSION}\"|" build.gradle - grep -E '^version ' build.gradle - - git add build.gradle - git commit -m "chore(release): bump ${SOURCE_BRANCH} to ${NEXT_DEV_VERSION} after cutting ${RELEASE_VERSION}-rc" - git push origin "${SOURCE_BRANCH}" - - - name: Trigger build/publish on RC branch and source_branch - if: ${{ inputs.trigger_build }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - RC_BRANCH: ${{ steps.vars.outputs.rc_branch }} - SOURCE_BRANCH: ${{ inputs.source_branch }} - run: | - set -euo pipefail - - echo "::group::Dispatch build-publish on RC branch (${RC_BRANCH})" - gh workflow run build-publish.yml --repo "${REPO}" \ - --ref "${RC_BRANCH}" \ - -f publish_jfrog=true - echo "::endgroup::" - - echo "::group::Dispatch build-publish on source_branch (${SOURCE_BRANCH})" - gh workflow run build-publish.yml --repo "${REPO}" \ - --ref "${SOURCE_BRANCH}" \ - -f publish_jfrog=true - echo "::endgroup::" - - - name: Job summary - env: - SOURCE_BRANCH: ${{ inputs.source_branch }} - NEXT_DEV_VERSION: ${{ inputs.next_dev_version }} - RC_BRANCH: ${{ steps.vars.outputs.rc_branch }} - RC_VERSION: ${{ steps.vars.outputs.rc_version }} - RELEASE_VERSION: ${{ steps.vars.outputs.release_version }} - SERVER_URL: ${{ github.server_url }} - REPOSITORY: ${{ github.repository }} - run: | - { - echo "## Release RC cut: ${RELEASE_VERSION}" - echo "" - echo "**RC branch**: [\`${RC_BRANCH}\`](${SERVER_URL}/${REPOSITORY}/tree/${RC_BRANCH})" - echo "" - echo "| file | value |" - echo "|------|-------|" - echo "| \`build.gradle\` version | \`${RC_VERSION}\` |" - echo "" - echo "**${SOURCE_BRANCH}** bumped to: \`${NEXT_DEV_VERSION}\`" - echo "" - echo "### Next step" - echo "Stabilize on \`${RC_BRANCH}\`; when ready, dispatch the release-promote workflow." - } >> "$GITHUB_STEP_SUMMARY" + uses: NrgXnat/xnat-ci-workflows/.github/workflows/gradle-release-cut-rc.yml@v1 + with: + next_dev_version: ${{ inputs.next_dev_version }} + source_branch: ${{ inputs.source_branch }} + trigger_build: ${{ inputs.trigger_build }} + secrets: inherit diff --git a/.github/workflows/release-promote.yml b/.github/workflows/release-promote.yml index 0f54e058..09416fbd 100644 --- a/.github/workflows/release-promote.yml +++ b/.github/workflows/release-promote.yml @@ -1,17 +1,9 @@ name: Release - Promote RC to Main -# Promotes a stabilized RC branch to a tagged release on main. -# Plugin variant of the monorepo release-promote workflow — no -# dicom-edit6 sync; version lives in build.gradle (groovy DSL: `version "..."`). -# -# Inputs: -# rc_branch - RC branch to promote (e.g. releases/1.6.0-rc); REQUIRED -# target_main - branch to merge into (default main) -# trigger_build - dispatch build/publish on target_main -# -# Produces: -# - RC branch with -RC-SNAPSHOT suffix stripped (release version string) -# - target_main fast-merged (--no-ff) with RC + tag (no v prefix) +# Thin caller around the shared reusable workflow in NrgXnat/xnat-ci-workflows. +# Promotes a stabilized RC branch to a tagged release on target_main. All +# release-promote logic lives in the reusable workflow; this file only defines +# our triggers + inputs. on: workflow_dispatch: @@ -37,129 +29,9 @@ permissions: jobs: promote: - runs-on: ubuntu-latest - - steps: - - name: Checkout ${{ inputs.rc_branch }} - uses: actions/checkout@v4 - with: - ref: ${{ inputs.rc_branch }} - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Read RC state and derive release version - id: vars - env: - RC_BRANCH: ${{ inputs.rc_branch }} - run: | - set -euo pipefail - - RC_VERSION="$(grep -E '^version ' build.gradle | head -n1 | sed -E 's/^version "(.+)"$/\1/')" - if [[ -z "${RC_VERSION}" ]]; then - echo "::error::Could not read version from build.gradle" - exit 1 - fi - RELEASE_VERSION="${RC_VERSION%-RC-SNAPSHOT}" - - { - echo "rc_version=${RC_VERSION}" - echo "release_version=${RELEASE_VERSION}" - } >> "$GITHUB_OUTPUT" - - echo "::notice::rc=${RC_BRANCH} rc_version=${RC_VERSION} → release=${RELEASE_VERSION}" - - - name: Validate - env: - RC_BRANCH: ${{ inputs.rc_branch }} - RC_VERSION: ${{ steps.vars.outputs.rc_version }} - RELEASE_VERSION: ${{ steps.vars.outputs.release_version }} - run: | - set -euo pipefail - - if [[ ! "${RC_BRANCH}" =~ ^releases/.+-rc$ ]]; then - echo "::error::rc_branch=${RC_BRANCH} must match 'releases/*-rc'" - exit 1 - fi - if [[ "${RC_VERSION}" != *-RC-SNAPSHOT ]]; then - echo "::error::RC branch version=${RC_VERSION} must end with -RC-SNAPSHOT" - exit 1 - fi - if git ls-remote --exit-code --tags origin "refs/tags/${RELEASE_VERSION}" >/dev/null 2>&1; then - echo "::error::Tag ${RELEASE_VERSION} already exists on origin" - exit 1 - fi - - - name: Configure git - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - - name: Finalize RC branch (strip -RC-SNAPSHOT) - env: - RC_BRANCH: ${{ inputs.rc_branch }} - RC_VERSION: ${{ steps.vars.outputs.rc_version }} - RELEASE_VERSION: ${{ steps.vars.outputs.release_version }} - run: | - set -euo pipefail - - sed -i "s|^version \"${RC_VERSION}\"\$|version \"${RELEASE_VERSION}\"|" build.gradle - grep -E '^version ' build.gradle - git add build.gradle - - git commit -m "chore(release): finalize ${RELEASE_VERSION}" - git push origin "HEAD:${RC_BRANCH}" - - - name: Merge RC into target_main and tag - env: - RC_BRANCH: ${{ inputs.rc_branch }} - TARGET_MAIN: ${{ inputs.target_main }} - RELEASE_VERSION: ${{ steps.vars.outputs.release_version }} - run: | - set -euo pipefail - - git fetch origin "${TARGET_MAIN}" - git switch "${TARGET_MAIN}" - git pull --ff-only origin "${TARGET_MAIN}" - - # --no-ff to create an explicit merge commit on release boundary; - # set -e + no -X strategy → fail fast on conflict (release manager intervenes) - git merge --no-ff "origin/${RC_BRANCH}" -m "release: ${RELEASE_VERSION}" - - git tag "${RELEASE_VERSION}" # no 'v' prefix (matches monorepo tag convention) - git push origin "${TARGET_MAIN}" - git push origin "refs/tags/${RELEASE_VERSION}" - - - name: Trigger build/publish on target_main - if: ${{ inputs.trigger_build }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - TARGET_MAIN: ${{ inputs.target_main }} - run: | - set -euo pipefail - - echo "::group::Dispatch build-publish on target_main (${TARGET_MAIN})" - gh workflow run build-publish.yml --repo "${REPO}" \ - --ref "${TARGET_MAIN}" \ - -f publish_jfrog=true - echo "::endgroup::" - - - name: Job summary - env: - RC_BRANCH: ${{ inputs.rc_branch }} - TARGET_MAIN: ${{ inputs.target_main }} - RELEASE_VERSION: ${{ steps.vars.outputs.release_version }} - SERVER_URL: ${{ github.server_url }} - REPOSITORY: ${{ github.repository }} - run: | - { - echo "## Release promoted: ${RELEASE_VERSION}" - echo "" - echo "**Tag**: [\`${RELEASE_VERSION}\`](${SERVER_URL}/${REPOSITORY}/releases/tag/${RELEASE_VERSION})" - echo "**${TARGET_MAIN}**: [HEAD](${SERVER_URL}/${REPOSITORY}/tree/${TARGET_MAIN})" - echo "**${RC_BRANCH}**: kept (finalized to release strings; available for future patches)" - echo "" - echo "| file | value |" - echo "|------|-------|" - echo "| \`build.gradle\` version | \`${RELEASE_VERSION}\` |" - } >> "$GITHUB_STEP_SUMMARY" + uses: NrgXnat/xnat-ci-workflows/.github/workflows/gradle-release-promote.yml@v1 + with: + rc_branch: ${{ inputs.rc_branch }} + target_main: ${{ inputs.target_main }} + trigger_build: ${{ inputs.trigger_build }} + secrets: inherit From dccfd342dbc28fd411c4553662a271af75c78709 Mon Sep 17 00:00:00 2001 From: Bin Zhang Date: Fri, 26 Jun 2026 23:22:20 +0800 Subject: [PATCH 43/44] xnat-8763: add 'packages: write' to build-publish.yml caller permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared reusable workflow gradle-build-publish.yml@v1 has a publish-ghcr job declaring 'permissions: contents: read, packages: write'. Even though that job is gated by 'if: inputs.publish-ghcr' (default false), GitHub Actions parses the permissions at workflow startup. If the caller doesn't grant 'packages: write', the run fails with startup_failure before any job runs. Validated by dispatching build-publish on mfa-plugin/fake-develop: - Without packages: write → startup_failure in 2s - With packages: write → reaches Stage 1 Gradle build Mirrors xnat monorepo's build-publish.yml caller pattern. --- .github/workflows/build-publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml index 5838ec22..e93b361d 100644 --- a/.github/workflows/build-publish.yml +++ b/.github/workflows/build-publish.yml @@ -19,6 +19,7 @@ on: permissions: contents: read + packages: write # required by reusable workflow's publish-ghcr job (gated by if: inputs.publish-ghcr but parsed at startup) jobs: build-publish: From 83e3c4edcb633b98af81ea81a5729e961dca65ea Mon Sep 17 00:00:00 2001 From: Bin Zhang Date: Mon, 29 Jun 2026 00:01:30 +0800 Subject: [PATCH 44/44] xnat-8763: remove vestigial net.linguica.maven-settings plugin --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index 11fd5b5d..59d64084 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,6 @@ plugins { id "com.palantir.git-version" version "3.3.0" id "io.spring.dependency-management" version "1.1.7" id "io.franzbecker.gradle-lombok" version "5.0.0" - id "net.linguica.maven-settings" version "0.5" } group "org.nrg.xnatx.plugins"