diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index e4d8e6dc..e370cc49 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -3,8 +3,8 @@ version: 2
# whatever the default branch is (`main` only moves forward to releases);
# security updates always target the default branch, though
updates:
- # Dependencies in `pom.xml`. Java 8 and Spring Boot 2 stay until the move to
- # Spring Boot 3, which needs code changes
+ # Dependencies in `pom.xml`. Spring Boot 2 stays until the move to Spring
+ # Boot 3, which needs code changes
- package-ecosystem: maven
directory: /
schedule:
@@ -38,8 +38,9 @@ updates:
groups:
github-actions:
patterns: ["*"]
- # Base images in the `Dockerfile` (tag and digest). Java 8 stays until the
- # move to Spring Boot 3
+ # Base images in the `Dockerfile` (tag and digest). Java 21 (LTS), as in
+ # `pom.xml` and the CI job `Test`: a new major version of Java is a
+ # decision of its own
- package-ecosystem: docker
directory: /
schedule:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a9b28662..a098c8d0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -91,9 +91,10 @@ jobs:
docker run --rm --volume "${PWD}:/repo" --workdir /repo \
rhysd/actionlint:1.7.7@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9 \
-color
- # Java 8, as in `test`: the plugin versions are the last that run on it.
- # No Maven cache: it would share the key of `test` and could be saved
- # with only the lint plugins in it
+ # Java 8, unlike `test`: google-java-format 1.7 doesn't run on Java 21,
+ # and newer versions format the code differently (to be updated with the
+ # move to Spring Boot 3, #66). No Maven cache: it would share the key of
+ # `test` and could be saved with only the lint plugins in it
- name: Set up Java
uses: actions/setup-java@77dd5b2ce225ea11469928ac317850245a74ae78 # v6.2.0
with:
@@ -129,7 +130,7 @@ jobs:
uses: actions/setup-java@77dd5b2ce225ea11469928ac317850245a74ae78 # v6.2.0
with:
distribution: temurin
- java-version: "8"
+ java-version: "21"
cache: maven
- name: Build and run tests
run: ./mvnw -B clean verify
diff --git a/Dockerfile b/Dockerfile
index 673860dc..5dc4966f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
# Builds the application from source, so that a plain `docker build .` is all
-# that is needed. Java 8, as in `pom.xml`; the base images are pinned by digest
+# that is needed. Java 21, as in `pom.xml`; the base images are pinned by digest
# and updated by Dependabot
-FROM eclipse-temurin:8-jdk@sha256:e9255b50040337b0dec91027868dca1952ef5441bf0c8f878c5f790eaedbfea2 AS build
+FROM eclipse-temurin:21-jdk@sha256:0a3f329821dcadf54b2a3033a15e2e9f007069a9db43d7f51897e6b80e726b07 AS build
# The Maven wrapper downloads the .zip of Maven and checks its SHA-256 sum
# (`.mvn/wrapper/maven-wrapper.properties`); without `unzip` it would fetch the
# .tar.gz instead, which the sum does not match
@@ -15,13 +15,7 @@ COPY src src
RUN ./mvnw -B -Dmaven.test.skip=true package \
&& cp target/tesk-*.jar /app.jar
-FROM eclipse-temurin:8-jre@sha256:377fe8e36a4b1530d9d52020069367ef44313fa891a82ae102b1016a6a3a72f5
-# The pinned base image has a vulnerable apt (CVE-2026-109518; fixed by
-# 3.2.1ubuntu0.1 in the Ubuntu security updates); can go once Dependabot moves
-# the base image to a build with the fix
-RUN apt-get update && \
- apt-get install -y --no-install-recommends --only-upgrade apt libapt-pkg7.0 && \
- rm -rf /var/lib/apt/lists/*
+FROM eclipse-temurin:21-jre@sha256:18bb4a91a3dea3f03ee31d3755af34dfb40f904fc77ab56cdc2f092e32fff9c2
# Run as non-root user; group 0 owns the home directory, so that arbitrary
# user IDs (e.g., on OpenShift, always in group 0) can use it too. The app
# writes only to `/tmp`; the Kubernetes client reads `~/.kube/config` (or
diff --git a/README.md b/README.md
index 82a0fc41..bc0bb56a 100644
--- a/README.md
+++ b/README.md
@@ -59,8 +59,9 @@ To deploy TESK to a cluster, use the Helm chart of [TESK][tesk].
## Development
-TESK API builds and runs on Java 8. To build it and run the unit and
-integration tests (no cluster needed):
+TESK API builds and runs on Java 21 (the formatter and linter still need
+Java 8; see [Development][docs-development]). To build it and run the unit
+and integration tests (no cluster needed):
```bash
./mvnw clean verify
diff --git a/docs/deployment.md b/docs/deployment.md
index b9b760c3..0bc63f62 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -22,7 +22,7 @@ cosign verify elixircloud/tesk-api:1.2.0 \
For `dev` images, the identity is
`https://github.com/elixir-cloud-aai/tesk-api/.github/workflows/ci.yml@refs/heads/dev`.
-The image runs the application with Java 8 on [Eclipse
+The image runs the application with Java 21 on [Eclipse
Temurin](https://adoptium.net/) and listens on port 8080. `JAVA_OPTS` passes
options to the JVM. It runs as the non-root user `tesk` (UID 1000, group 0,
home directory `/home/tesk`); any other UID in group 0 works too, as on
@@ -66,7 +66,7 @@ needs to manage jobs, pods and persistent volume claims.
## Running locally
To run TESK API outside of Kubernetes, e.g. against a local cluster such as
-[kind](https://kind.sigs.k8s.io/) or [k3d](https://k3d.io/), with Java 8 and
+[kind](https://kind.sigs.k8s.io/) or [k3d](https://k3d.io/), with Java 21 and
a `KUBECONFIG` file for that cluster:
```bash
diff --git a/docs/development.md b/docs/development.md
index 7c9b3f10..66442b83 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -1,10 +1,9 @@
# Development
TESK API is a [Spring Boot](https://spring.io/projects/spring-boot) 2.7
-application on Java 8, built with Maven 3.9 (through the wrapper `./mvnw`,
-which downloads the version pinned in `.mvn/wrapper/maven-wrapper.properties`;
-Maven 3.9 still runs on Java 8). The server stub is generated from the TES
-specification with the [Swagger Codegen Maven
+application on Java 21, built with Maven 3.9 (through the wrapper `./mvnw`,
+which downloads the version pinned in `.mvn/wrapper/maven-wrapper.properties`).
+The server stub is generated from the TES specification with the [Swagger Codegen Maven
plugin](https://github.com/swagger-api/swagger-codegen/tree/master/modules/swagger-codegen-maven-plugin),
and the [Kubernetes Java client](https://github.com/kubernetes-client/java)
talks to the Kubernetes API.
@@ -66,10 +65,15 @@ neither):
./mvnw spotless:check checkstyle:check
```
-The plugin versions (spotless-maven-plugin 2.30.0 with google-java-format 1.7,
-Checkstyle 9.3) are the last that run on Java 8; newer ones need a newer JDK
-(the move to Spring Boot 3,
-[#66](https://github.com/elixir-cloud-aai/tesk-api/issues/66)).
+These two commands need Java 8 (e.g. `JAVA_HOME` pointing to a Java 8 JDK),
+as the CI job `Lint` runs them: google-java-format 1.7 (with
+spotless-maven-plugin 2.30.0) doesn't run on Java 21, and the newer versions
+that do format the code differently. The update, with the reformat it brings,
+is part of the move to Spring Boot 3
+([#66](https://github.com/elixir-cloud-aai/tesk-api/issues/66)). Building and
+testing need Java 21. Until then, write code that Java 8 can parse (no
+records, text blocks, `switch` expressions or `instanceof` patterns), or the
+formatter fails.
The commits that only reformatted the code are listed in
`.git-blame-ignore-revs`. GitHub skips them in its blame view; for `git blame`,
@@ -218,7 +222,10 @@ Validation](https://beanvalidation.org/) annotations beyond those derived from
the specification (e.g. `@Digits` and `@DecimalMin` on the resources of
`TesResources`); restore them by hand in the generated code, and reconcile
any other manual changes, e.g. the internal field `userIdFromLegacyLabel` of
-`TesTaskLog` (not serialized), which the access check of a task needs. Until
+`TesTaskLog` (not serialized), which the access check of a task needs, and
+the check `isUrlOrContentGiven` of `TesInput` (a generator that adds
+`@ScriptAssert(lang = "javascript")` instead brings back a check that fails
+on Java 15 and later, which have no JavaScript engine). Until
the experimental private image credential workaround remains supported, also
restore the `docker_registry`, `docker_login` and `docker_pass` fields,
validation, write-only handling, and experimental API descriptions in
diff --git a/docs/upgrading.md b/docs/upgrading.md
index a4045152..263acdaa 100644
--- a/docs/upgrading.md
+++ b/docs/upgrading.md
@@ -2,8 +2,21 @@
## To the next release
-From 1.2.0, clients may notice changed answers:
+From 1.2.0, deployments may have to check their JVM options, and clients may
+notice changed answers:
+- **Java 21.** The image runs TESK API on Java 21 instead of Java 8 (see
+ [Deployment](deployment.md)). If you pass options to the JVM in
+ `JAVA_OPTS`, check that Java 21 still accepts them: options that it
+ removed (e.g. `-XX:MaxPermSize`, `-XX:+UseConcMarkSweepGC`,
+ `-XX:+UseCGroupMemoryLimitForHeap`) stop the JVM
+ at startup. Without `JAVA_OPTS`, nothing changes.
+- **Inputs without a source.** An input with neither a `url` (that isn't
+ blank) nor a `content` (that isn't empty) is still refused with `400 Bad
+ Request`, but the message now names the check:
+ `Field 'inputs[0].urlOrContentGiven' URL or content required` instead of
+ `Field 'inputs[0]' URL or content required`. The messages are meant for
+ people; clients should rely on the status code, not on their text.
- **Wildcards without `path_prefix`.** An output whose `path` has wildcards
(`*`, `?`, `[`) but no `path_prefix` is refused with `400 Bad Request`,
unless its `url` is a `file:` URL or has no scheme, as TES 1.1 requires
diff --git a/pom.xml b/pom.xml
index 52be127d..81e454c1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@
2.17.1
UTF-8
UTF-8
- 1.8
+ 21
3.0.0
11.0.4
@@ -293,6 +293,10 @@
${maven-surefire-plugin.version}
false
+
+ @{argLine} --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED
@@ -396,7 +400,8 @@
+ (CI job Lint), on Java 8: google-java-format 1.7 does not run on Java 21, and
+ newer versions format differently (docs/development.md). -->
com.diffplug.spotless
spotless-maven-plugin
diff --git a/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java b/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java
index c0102052..0e657cca 100644
--- a/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java
+++ b/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java
@@ -1,11 +1,11 @@
package uk.ac.ebi.tsc.tesk.tes.model;
import java.util.Objects;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
-import org.hibernate.validator.constraints.ScriptAssert;
import javax.validation.Valid;
import javax.validation.constraints.*;
@@ -18,7 +18,6 @@
*/
@ApiModel(description = "Input describes Task input files.")
@javax.annotation.Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2021-03-24T17:10:08.716Z[Europe/London]")
-@ScriptAssert(lang = "javascript", script = "(_this.url != null && (_this.url.trim().length) > 0) || (_this.content != null && _this.content.length > 0)", message = "URL or content required")
public class TesInput {
@JsonProperty("name")
private String name;
@@ -191,6 +190,30 @@ public void setContent(String content) {
this.content = content;
}
+ /**
+ * An input needs a source: a `url` that isn't blank (white space as JavaScript's `trim` saw it
+ * before), or a `content` that isn't empty. A plain method rather than a script
+ * (`@ScriptAssert`), as Java 15 and later have no JavaScript engine.
+ *
+ * @return whether `url` or `content` is given
+ */
+ @JsonIgnore
+ @AssertTrue(message = "URL or content required")
+ public boolean isUrlOrContentGiven() {
+ return (url != null && !isJavaScriptBlank(url)) || (content != null && !content.isEmpty());
+ }
+
+ /**
+ * Whether a string holds only white space and line terminators as JavaScript's `trim` sees
+ * them (ECMAScript WhiteSpace and LineTerminator). Unlike {@link String#isBlank()}, this
+ * includes U+00A0, U+2007, U+202F and U+FEFF, and excludes U+001C to U+001F.
+ */
+ private static boolean isJavaScriptBlank(String s) {
+ return s.codePoints().allMatch(c -> c == '\t' || c == '\n' || c == 0x0B || c == '\f'
+ || c == '\r' || c == 0xFEFF || c == 0x2028 || c == 0x2029
+ || Character.getType(c) == Character.SPACE_SEPARATOR);
+ }
+
@Override
public boolean equals(Object o) {
diff --git a/src/test/java/uk/ac/ebi/tsc/tesk/tes/api/TasksApiControllerTest.java b/src/test/java/uk/ac/ebi/tsc/tesk/tes/api/TasksApiControllerTest.java
index 99d9b0b3..c26e47ec 100644
--- a/src/test/java/uk/ac/ebi/tsc/tesk/tes/api/TasksApiControllerTest.java
+++ b/src/test/java/uk/ac/ebi/tsc/tesk/tes/api/TasksApiControllerTest.java
@@ -243,7 +243,15 @@ public void createTask_invalid_inputs() throws Exception {
String[][] files =
new String[][] {
new String[] {"no_path.json", "inputs[0].path"},
- new String[] {"no_source.json", "URL or content required"},
+ new String[] {
+ "no_source.json", "inputs[0].urlOrContentGiven' URL or content required"
+ },
+ new String[] {
+ "blank_url.json", "inputs[0].urlOrContentGiven' URL or content required"
+ },
+ new String[] {
+ "empty_content.json", "inputs[0].urlOrContentGiven' URL or content required"
+ },
new String[] {"wrong_type.json", "inputs[0].type"},
new String[] {"empty_type.json", "inputs[0].type"}
};
diff --git a/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java b/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java
new file mode 100644
index 00000000..b30769f2
--- /dev/null
+++ b/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java
@@ -0,0 +1,32 @@
+package uk.ac.ebi.tsc.tesk.tes.model;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class TesInputTest {
+
+ @Test
+ public void urlOrContentGiven() {
+ assertTrue(new TesInput().url("s3://bucket/file").isUrlOrContentGiven());
+ assertTrue(new TesInput().content("data").isUrlOrContentGiven());
+ assertTrue(new TesInput().url("s3://bucket/file").content("data").isUrlOrContentGiven());
+ assertTrue(new TesInput().url(" ").content(" ").isUrlOrContentGiven());
+ // JavaScript's trim keeps the information separators U+001C to U+001F.
+ assertTrue(new TesInput().url("\u001c").isUrlOrContentGiven());
+ }
+
+ @Test
+ public void neitherUrlNorContentGiven() {
+ assertFalse(new TesInput().isUrlOrContentGiven());
+ assertFalse(new TesInput().url(" ").isUrlOrContentGiven());
+ assertFalse(new TesInput().url("\u3000\t").isUrlOrContentGiven());
+ assertFalse(
+ new TesInput()
+ .url("\t\n\u000b\f\r\u00a0\ufeff\u2007\u202f\u2028\u2029")
+ .isUrlOrContentGiven());
+ assertFalse(new TesInput().content("").isUrlOrContentGiven());
+ assertFalse(new TesInput().url("").content("").isUrlOrContentGiven());
+ }
+}
diff --git a/src/test/resources/invalid_tasks/inputs/blank_url.json b/src/test/resources/invalid_tasks/inputs/blank_url.json
new file mode 100644
index 00000000..c9f3a253
--- /dev/null
+++ b/src/test/resources/invalid_tasks/inputs/blank_url.json
@@ -0,0 +1,20 @@
+{
+ "executors": [
+ {
+ "image": "ubuntu",
+ "command": [
+ "echo",
+ "hello world"
+ ]
+ }
+ ],
+ "inputs": [
+ {
+ "name": "input1",
+ "description": "Example",
+ "path": "/sth/sth.json",
+ "type": "FILE",
+ "url": " "
+ }
+ ]
+}
diff --git a/src/test/resources/invalid_tasks/inputs/empty_content.json b/src/test/resources/invalid_tasks/inputs/empty_content.json
new file mode 100644
index 00000000..2161e399
--- /dev/null
+++ b/src/test/resources/invalid_tasks/inputs/empty_content.json
@@ -0,0 +1,20 @@
+{
+ "executors": [
+ {
+ "image": "ubuntu",
+ "command": [
+ "echo",
+ "hello world"
+ ]
+ }
+ ],
+ "inputs": [
+ {
+ "name": "input1",
+ "description": "Example",
+ "path": "/sth/sth.json",
+ "type": "FILE",
+ "content": ""
+ }
+ ]
+}