Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ updates:
# Held back until the move to Spring Boot 3 (#66): the next major
# versions of these need Java 11 or 17, or Spring Framework 6 or 7
# (#91). Remove these rules with #66
- dependency-name: io.kubernetes:client-java
update-types: ["version-update:semver-major"]
- dependency-name: org.springframework.security:*
update-types: ["version-update:semver-major"]
- dependency-name: com.github.tomakehurst:wiremock
Expand Down
7 changes: 1 addition & 6 deletions .trivyignore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
# the affected package versions (`purls`), so that other packages with the same
# vulnerability are still reported.
#
# All of these are fixed only in Spring Boot 3 / Spring Framework 6 and later
# (or, for snakeyaml, in the 2.x line, which the Kubernetes client can't use),
# All of these are fixed only in Spring Boot 3 / Spring Framework 6 and later,
# while `pom.xml` has the last open source releases of the Spring Boot 2.7 line.
#
# See https://trivy.dev/latest/docs/configuration/filtering/#trivyignoreyaml
Expand Down Expand Up @@ -59,7 +58,3 @@ vulnerabilities:
purls: ["pkg:maven/org.springframework/spring-webmvc@5.3.39"]
statement: Concerns XsltView; the app has no views
expired_at: 2027-01-31
- id: CVE-2022-1471
purls: ["pkg:maven/org.yaml/snakeyaml@1.33"]
statement: Needs YAML from an attacker; the app only reads files and ConfigMaps of the deployer
expired_at: 2027-01-31
15 changes: 15 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ Resource | Verbs | For
`secrets` | `create`, `delete` | Private executor image credentials, as Secrets owned by the taskmaster Job (`delete` for `OwnerReferencesPermissionEnforcement`)
`limitranges` | `list` | Raising requested memory to the namespace's minimum (without the right, it logs an error and goes on)

TESK API has version 28 of the client, which supports Kubernetes 1.37. It
also works with older and newer Kubernetes versions:

- It reads jobs, pods, ConfigMaps and LimitRanges, and ignores the fields
that newer Kubernetes versions add to them. (The client itself would refuse
such objects, see
[kubernetes-client/java#4941](https://github.com/kubernetes-client/java/issues/4941).)
- The jobs, ConfigMaps and Secrets that it creates, and its patches, use
only fields that Kubernetes has had for years.
- The objects that it creates list empty fields, e.g. `args: []`, which
Kubernetes treats as unset. A Kubernetes version that doesn't know such a
field drops it, and may answer with a warning. The executors' jobs in the
taskmaster's input leave them out, as before: the taskmaster tells unset
fields (e.g. no limits, which it sets to the requests) by their absence.

Passing private executor image credentials in TES requests is an
[experimental workaround for proof-of-concept use only](api.md#private-executor-images),
not a feature for production clients.
Expand Down
11 changes: 11 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ plugin](https://github.com/swagger-api/swagger-codegen/tree/master/modules/swagg
and the [Kubernetes Java client](https://github.com/kubernetes-client/java)
talks to the Kubernetes API.

The Kubernetes client needs newer versions of some libraries than Spring
Boot 2.7 manages (SnakeYAML, OkHttp, Kotlin, Gson, Commons Lang and Commons
Codec): `pom.xml` sets them to those of the client's parent POM
(`io.kubernetes:client-java-parent`), so update them together with the
client. The models of the client refuse JSON
with fields they don't know; `KubernetesJson` installs a Gson that ignores
them (see [Deployment](deployment.md#kubernetes)), so test fixtures of
Kubernetes objects may leave out fields that Kubernetes requires. The
executors' jobs in the taskmaster's input are written without the empty
lists and maps of the client's models (`GSON_WITHOUT_EMPTY_COLLECTIONS`).

The wrapper is the `only-script` type of the Apache Maven Wrapper, so there is
no binary `maven-wrapper.jar` in the repository. It needs `curl` or `wget` and
`unzip` (without `unzip` it would fetch the `.tar.gz`, which does not match the
Expand Down
12 changes: 12 additions & 0 deletions docs/upgrading.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ notice changed answers:
see [Configuration](configuration.md)). An older taskmaster stops at the
failure: the task then ends `SYSTEM_ERROR`, unless the failed executor was
the last one and the task has no outputs, which completes as asked.
- **Kubernetes client.** TESK API has version 28 of the Kubernetes Java
client instead of 11 (see [Deployment](deployment.md#kubernetes)), and
SnakeYAML 2 instead of 1.33. Nothing changes for deployments: it needs the
same rights, and works with the same Kubernetes versions. The jobs,
ConfigMaps and Secrets that it creates list empty fields (e.g. `args: []`),
which Kubernetes treats as unset; the taskmaster's input stays as it
was. Run outside a cluster with a token in its kubeconfig file (e.g.
after `oc login`; not with client certificates), TESK API can now cancel
tasks: it sent the token without its `Bearer` prefix in the patches,
which Kubernetes refused. A task whose taskmaster
job has no creation time has no `creation_time`, instead of the current
time; Kubernetes sets it for every job, so this only matters for tests.

## To 1.2.0

Expand Down
49 changes: 9 additions & 40 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<java.version>21</java.version>
<!-- Dependency versions -->
<springfox-swagger.version>3.0.0</springfox-swagger.version>
<kubernetes.client-java.version>11.0.4</kubernetes.client-java.version>
<kubernetes.client-java.version>28.0.0</kubernetes.client-java.version>

<guava.version>33.8.0-jre</guava.version>
<!-- Newer than what Spring Boot 2.7 manages: the last releases on its line.
Expand All @@ -35,57 +35,26 @@
<tomcat.version>9.0.123</tomcat.version>
<logback.version>1.2.13</logback.version>
<jackson-bom.version>2.18.12</jackson-bom.version>
<!-- The last 1.x: the Kubernetes client needs the API of snakeyaml 1.x -->
<snakeyaml.version>1.33</snakeyaml.version>
<!-- What the Kubernetes client needs, newer than what Spring Boot 2.7 manages (the client's
parent POM, io.kubernetes:client-java-parent): update them together with it -->
<snakeyaml.version>2.7</snakeyaml.version>
<okhttp.version>5.5.0</okhttp.version>
<kotlin.version>2.1.21</kotlin.version>
<gson.version>2.14.0</gson.version>
<commons-lang3.version>3.21.0</commons-lang3.version>
<commons-codec.version>1.23.0</commons-codec.version>
<maven-surefire-plugin.version>3.6.1</maven-surefire-plugin.version>
<apache.commons.lang3.version>3.12.0</apache.commons.lang3.version>
<apache.commons.collections4.version>4.6.0</apache.commons.collections4.version>
</properties>

<dependencyManagement>
<dependencies>
<!-- Transitive dependencies with known vulnerabilities in the versions they ask for -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.36.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.22.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.28.0</version>
</dependency>
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.9.7</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-ext-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<!-- bcpkix must match bcprov, and needs bcutil since 1.69 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcutil-jdk15on</artifactId>
<version>1.70</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/uk/ac/ebi/tsc/tesk/config/GsonConfig.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package uk.ac.ebi.tsc.tesk.config;

import com.google.gson.Gson;
import io.kubernetes.client.openapi.JSON;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import uk.ac.ebi.tsc.tesk.k8s.config.KubernetesJson;

/** @author Ania Niewielska <aniewielska@ebi.ac.uk> */
@Configuration
public class GsonConfig {

@Bean
Gson gson() {
return new JSON().getGson();
return KubernetesJson.GSON;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
@Configuration
public class KubernetesClientConfig {

/**
* Lets the Kubernetes client read objects of Kubernetes versions newer than itself, before any
* of the API clients below is used
*/
public KubernetesClientConfig() {
KubernetesJson.install();
}

@Bean
@Primary
public ApiClient kubernetesApiClient() throws IOException {
Expand All @@ -38,13 +46,13 @@ public CoreV1Api coreApi(ApiClient apiClient) {

/**
* Workaround to enable merge patching (Kube client does JSON patch only - headers hardcoded)
* Separate instance of ApiClient only for running PATCH calls
* Separate instance of ApiClient only for running PATCH calls, with the connection and
* credentials of the default one
*/
@Bean(name = "patchApiClient")
public ApiClient patchApiClient() throws IOException {
public ApiClient patchApiClient(ApiClient kubernetesApiClient) {

ApiClient defaultClient = Config.defaultClient();
return new ApiClientWithMergePatching(defaultClient);
return new ApiClientWithMergePatching(kubernetesApiClient);
}

/** Separate object to perform merge-patching of Jobs */
Expand All @@ -71,7 +79,11 @@ private static class ApiClientWithMergePatching extends ApiClient {
ApiClientWithMergePatching(ApiClient apiClient) {
this.setBasePath(apiClient.getBasePath());
this.setHttpClient(apiClient.getHttpClient());
this.setApiKey(((ApiKeyAuth) apiClient.getAuthentication("BearerToken")).getApiKey());
// the token (from a kubeconfig file; in a cluster, the HTTP client adds it) with its
// prefix ("Bearer"), without which Kubernetes refuses it
ApiKeyAuth auth = (ApiKeyAuth) apiClient.getAuthentication("BearerToken");
this.setApiKeyPrefix(auth.getApiKeyPrefix());
this.setApiKey(auth.getApiKey());
}

@Override
Expand Down
89 changes: 89 additions & 0 deletions src/main/java/uk/ac/ebi/tsc/tesk/k8s/config/KubernetesJson.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package uk.ac.ebi.tsc.tesk.k8s.config;

import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import io.kubernetes.client.openapi.JSON;
import java.io.IOException;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.Collection;
import java.util.Map;

/**
* Gson for the objects of the Kubernetes API that ignores fields it doesn't know, as the Kubernetes
* client did up to version 19 (and as client-go does).
*
* <p>The models of the Kubernetes client since version 20 refuse JSON with fields that they don't
* know, so every object that a newer Kubernetes version extends (e.g. with a field of a job's or
* pod's status) would make reading it fail (https://github.com/kubernetes-client/java/issues/4941).
* TESK API reads jobs, pods, ConfigMaps and LimitRanges, and only creates objects from its own
* templates and patches them with its own patches, so ignoring unknown fields loses nothing. It
* doesn't check required fields either. Everything else is as in the client's Gson: times (RFC
* 3339), byte arrays (Base64), and the client's handling of Status objects and managed fields.
*/
public final class KubernetesJson {

/** The client's Gson, for its adapters of times and byte arrays */
private static final Gson CLIENT_GSON = JSON.getGson();

public static final Gson GSON =
JSON.createGson()
.registerTypeAdapter(
OffsetDateTime.class, CLIENT_GSON.getAdapter(OffsetDateTime.class))
.registerTypeAdapter(LocalDate.class, CLIENT_GSON.getAdapter(LocalDate.class))
.registerTypeAdapter(byte[].class, CLIENT_GSON.getAdapter(byte[].class))
.create();

/**
* {@link #GSON}, but leaving out lists and maps that are empty, as the Kubernetes client did up
* to version 19: its models now start with empty lists and maps instead of none. For the
* executors' jobs in the taskmaster's input, as the taskmaster tells "no limits" (which it sets
* to the requests) from limits by whether the key is there. Objects without fields (e.g. {@code
* emptyDir: {}}) are kept.
*/
public static final Gson GSON_WITHOUT_EMPTY_COLLECTIONS =
GSON.newBuilder().registerTypeAdapterFactory(new WithoutEmptyCollections()).create();

private KubernetesJson() {}

/** Writes empty lists and maps as null, which Gson leaves out of objects */
private static final class WithoutEmptyCollections implements TypeAdapterFactory {

@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<?> raw = type.getRawType();
if (!Collection.class.isAssignableFrom(raw) && !Map.class.isAssignableFrom(raw)) {
return null;
}
TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
return new TypeAdapter<T>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
if (value instanceof Collection && ((Collection<?>) value).isEmpty()
|| value instanceof Map && ((Map<?, ?>) value).isEmpty()) {
out.nullValue();
} else {
delegate.write(out, value);
}
}

@Override
public T read(JsonReader in) throws IOException {
return delegate.read(in);
}
};
}
}

/**
* Lets the Kubernetes client read responses with {@link #GSON}: its Gson is global (static),
* shared by all API clients
*/
public static void install() {
JSON.setGson(GSON);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ private static V1PodSecurityContext createSecurityContext(
confmap_namespace = System.getenv(envConfigMapNamespace);
}
V1ConfigMap config =
coreApi.readNamespacedConfigMap(
System.getenv(envConfigMap), confmap_namespace, null, false, false);
coreApi.readNamespacedConfigMap(System.getenv(envConfigMap), confmap_namespace)
.execute();

String config_key = "securityContext";
if (System.getenv(envConfigMapKey) != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.kubernetes.client.openapi.models.V1Pod;
import io.kubernetes.client.openapi.models.V1PodCondition;
import io.kubernetes.client.openapi.models.V1PodStatus;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
Expand All @@ -23,7 +24,6 @@
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.joda.time.DateTime;
import uk.ac.ebi.tsc.tesk.k8s.constant.K8sConstants;
import uk.ac.ebi.tsc.tesk.k8s.convert.data.Job;
import uk.ac.ebi.tsc.tesk.k8s.convert.data.Task;
Expand Down Expand Up @@ -99,11 +99,12 @@ private static boolean hasEnded(V1Pod pod) {
if (pod.getStatus() != null && ENDED_POD_PHASES.contains(pod.getStatus().getPhase())) {
return true;
}
DateTime deletion =
OffsetDateTime deletion =
Optional.ofNullable(pod.getMetadata())
.map(V1ObjectMeta::getDeletionTimestamp)
.orElse(null);
return deletion != null && deletion.plusSeconds(DELETION_SLACK_SECONDS).isBeforeNow();
return deletion != null
&& deletion.plusSeconds(DELETION_SLACK_SECONDS).isBefore(OffsetDateTime.now());
}

/**
Expand Down
Loading
Loading