diff --git a/README.md b/README.md
index 3909ae29..abe06939 100644
--- a/README.md
+++ b/README.md
@@ -115,6 +115,8 @@ Configurable properties :
| `logging.structured.format.console` | `` | Format for structured logging. Valid values : `ecs`, `gelf`, `logstash`. Leave empty for no structured logging (default). See https://docs.spring.io/spring-boot/reference/features/logging.html#features.logging.structured |
| `springdoc.swagger-ui.path` | `/` | Open API (swagger) UI path |
| `springdoc.swagger-ui.oauth.clientId` | `` | clientid used by swagger to authenticate the user, in general the same which is used by onyxia-ui is ok. |
+| `DEBUG_JMX` | `` | Enable JMX monitoring. This is useful for profiling the app to improve performance but is not intended for production / daily use. Once enabled (`true`), use `kubectl port-forward` + a profiler (e.g VisualVM) to profile the app. |
+| `JMX_PORT` | `10000` | Port used by JMX if enabled. |
## Onyxia API dependency to Helm
diff --git a/helm-wrapper/pom.xml b/helm-wrapper/pom.xml
index 1f09338d..2e37d73b 100644
--- a/helm-wrapper/pom.xml
+++ b/helm-wrapper/pom.xml
@@ -61,8 +61,22 @@
io.fabric8
kubernetes-client
+
+
+ io.fabric8
+ kubernetes-httpclient-vertx
+
+
+
+ io.fabric8
+ kubernetes-httpclient-okhttp
+ 7.0.0
+ runtime
+
+
+
org.apache.commons
commons-lang3
diff --git a/onyxia-api/entrypoint.sh b/onyxia-api/entrypoint.sh
index c4437bfe..56512b24 100755
--- a/onyxia-api/entrypoint.sh
+++ b/onyxia-api/entrypoint.sh
@@ -13,4 +13,9 @@ if [[ -n "$CACERTS_DIR" ]]; then
fi
# Run application
-java org.springframework.boot.loader.launch.JarLauncher
+if [ -n "$DEBUG_JMX" ]; then
+ JMX_PORT="${JMX_PORT:-10000}"
+ java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=$JMX_PORT -Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=false -Djava.rmi.server.hostname=127.0.0.1 org.springframework.boot.loader.launch.JarLauncher
+else
+ java org.springframework.boot.loader.launch.JarLauncher
+fi
\ No newline at end of file
diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/CatalogWrapper.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/CatalogWrapper.java
index 06bbecc0..589eada6 100644
--- a/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/CatalogWrapper.java
+++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/CatalogWrapper.java
@@ -8,7 +8,9 @@
import fr.insee.onyxia.model.helm.Repository;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.regex.Pattern;
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -54,6 +56,15 @@ public class CatalogWrapper {
@Schema(description = "only include charts with one or more of the given keywords")
private List includeKeywords = new ArrayList<>();
+ @Schema(description = "exclude any charts which have one or more of the given keywords")
+ private List excludeKeywords = new ArrayList<>();
+
+ @Schema(description = "only include charts with one or more of the given annotations")
+ private Map includeAnnotations = new HashMap<>();
+
+ @Schema(description = "exclude any charts which have one or more of the given annotations")
+ private Map excludeAnnotations = new HashMap<>();
+
@Schema(description = "Skip tls certificate checks for the repository")
private boolean skipTlsVerify;
@@ -205,6 +216,30 @@ public void setIncludeKeywords(List includeKeywords) {
this.includeKeywords = includeKeywords;
}
+ public void setExcludeKeywords(List excludeKeywords) {
+ this.excludeKeywords = excludeKeywords;
+ }
+
+ public List getExcludeKeywords() {
+ return excludeKeywords;
+ }
+
+ public Map getIncludeAnnotations() {
+ return includeAnnotations;
+ }
+
+ public void setIncludeAnnotations(Map includeAnnotations) {
+ this.includeAnnotations = includeAnnotations;
+ }
+
+ public Map getExcludeAnnotations() {
+ return excludeAnnotations;
+ }
+
+ public void setExcludeAnnotations(Map excludeAnnotations) {
+ this.excludeAnnotations = excludeAnnotations;
+ }
+
public boolean getSkipTlsVerify() {
return skipTlsVerify;
}
diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/kubernetes/KubernetesClientProvider.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/kubernetes/KubernetesClientProvider.java
index 42cbaabd..085cc8b7 100644
--- a/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/kubernetes/KubernetesClientProvider.java
+++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/kubernetes/KubernetesClientProvider.java
@@ -6,6 +6,8 @@
import io.fabric8.kubernetes.client.ConfigBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import java.util.HashMap;
+import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -16,6 +18,10 @@ public class KubernetesClientProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(KubernetesClientProvider.class);
+ private static Map rootKubernetesClientCache = new HashMap<>();
+
+ private static KubernetesClient userKubernetesClientCache = null;
+
/**
* This returns the root client which has extended permissions. Currently cluster-admin. User
* calls should be done using the userClient which only has user permissions.
@@ -23,12 +29,28 @@ public class KubernetesClientProvider {
* @param region
* @return
*/
- public KubernetesClient getRootClient(Region region) {
- final Config config = getDefaultConfiguration(region).build();
- return new KubernetesClientBuilder().withConfig(config).build();
+ public synchronized KubernetesClient getRootClient(Region region) {
+ if (!rootKubernetesClientCache.containsKey(region.getId())) {
+ final Config config = getDefaultConfiguration(region).build();
+ rootKubernetesClientCache.put(
+ region.getId(), new KubernetesClientBuilder().withConfig(config).build());
+ }
+ return rootKubernetesClientCache.get(region.getId());
}
public KubernetesClient getUserClient(Region region, User user) {
+ // In case of SERVICEACCOUNT authentication, we can safely mutualize and use a single
+ // KubernetesClient
+ if (region.getServices().getAuthenticationMode()
+ == Region.Services.AuthenticationMode.SERVICEACCOUNT) {
+ if (userKubernetesClientCache != null) {
+ return userKubernetesClientCache;
+ }
+ Config config = getDefaultConfiguration(region).build();
+ KubernetesClient client = new KubernetesClientBuilder().withConfig(config).build();
+ userKubernetesClientCache = client;
+ return client;
+ }
final Config config = getDefaultConfiguration(region).build();
String username = user.getIdep();
if (region.getServices().getUsernamePrefix() != null) {
diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/dao/universe/CatalogLoader.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/dao/universe/CatalogLoader.java
index 2feab6b2..64e9a78a 100644
--- a/onyxia-api/src/main/java/fr/insee/onyxia/api/dao/universe/CatalogLoader.java
+++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/dao/universe/CatalogLoader.java
@@ -29,6 +29,7 @@
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
@Service
@@ -76,19 +77,33 @@ private void updateHelmRepository(CatalogWrapper cw) {
excludedChart.equalsIgnoreCase(
entry.getKey())))
.filter(
- // If includeKeywords is defined, include only services where
- // the latest version has the desired keyword.
entry ->
- cw.getIncludeKeywords() == null
- || cw.getIncludeKeywords().isEmpty()
- || cw.getIncludeKeywords().stream()
- .anyMatch(
- include ->
- entry.getValue()
- .getFirst()
- .getKeywords()
- .contains(
- include)))
+ (CollectionUtils.isEmpty(cw.getIncludeKeywords())
+ || entry.getValue()
+ .getFirst()
+ .hasKeywords(
+ cw
+ .getIncludeKeywords()))
+ && (CollectionUtils.isEmpty(
+ cw.getIncludeAnnotations())
+ || entry.getValue()
+ .getFirst()
+ .hasAnnotations(
+ cw
+ .getIncludeAnnotations())))
+ .filter(
+ entry ->
+ CollectionUtils.isEmpty(cw.getExcludeKeywords())
+ || !entry.getValue()
+ .getFirst()
+ .hasKeywords(cw.getExcludeKeywords()))
+ .filter(
+ entry ->
+ CollectionUtils.isEmpty(cw.getExcludeAnnotations())
+ || !entry.getValue()
+ .getFirst()
+ .hasAnnotations(
+ cw.getExcludeAnnotations()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
// For each service, filter the multiple versions if needed then refresh remaining
// versions
diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/security/LogUserInfoFilter.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/security/LogUserInfoFilter.java
new file mode 100644
index 00000000..767a4f8d
--- /dev/null
+++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/security/LogUserInfoFilter.java
@@ -0,0 +1,42 @@
+package fr.insee.onyxia.api.security;
+
+import fr.insee.onyxia.api.configuration.properties.RegionsConfiguration;
+import fr.insee.onyxia.api.services.UserProvider;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import org.slf4j.MDC;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+@Component
+public class LogUserInfoFilter extends OncePerRequestFilter {
+
+ private UserProvider userProvider;
+
+ private RegionsConfiguration regionsConfiguration;
+
+ @Autowired
+ public LogUserInfoFilter(UserProvider userProvider, RegionsConfiguration regionsConfiguration) {
+ this.userProvider = userProvider;
+ this.regionsConfiguration = regionsConfiguration;
+ }
+
+ @Override
+ protected void doFilterInternal(
+ HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+ throws ServletException, IOException {
+ try {
+ MDC.put(
+ "username",
+ userProvider.getUser(regionsConfiguration.getDefaultRegion()).getIdep());
+ } catch (Exception ignored) {
+
+ }
+ filterChain.doFilter(request, response);
+ MDC.clear();
+ }
+}
diff --git a/onyxia-api/src/test/java/fr/insee/onyxia/api/UserControllerTest.java b/onyxia-api/src/test/java/fr/insee/onyxia/api/UserControllerTest.java
index d5f90ff6..35452393 100644
--- a/onyxia-api/src/test/java/fr/insee/onyxia/api/UserControllerTest.java
+++ b/onyxia-api/src/test/java/fr/insee/onyxia/api/UserControllerTest.java
@@ -12,6 +12,7 @@
import fr.insee.onyxia.api.configuration.SecurityConfig;
import fr.insee.onyxia.api.configuration.properties.RegionsConfiguration;
import fr.insee.onyxia.api.controller.api.user.UserController;
+import fr.insee.onyxia.api.services.UserProvider;
import fr.insee.onyxia.api.services.utils.HttpRequestUtils;
import fr.insee.onyxia.api.user.OnyxiaUserProvider;
import fr.insee.onyxia.model.OnyxiaUser;
@@ -41,6 +42,8 @@ public class UserControllerTest extends BaseTest {
@MockBean private OnyxiaUserProvider onyxiaUserProvider;
+ @MockBean private UserProvider userProvider;
+
@BeforeEach
public void setUp() {
User user = new User();
diff --git a/onyxia-api/src/test/java/fr/insee/onyxia/api/dao/universe/CatalogLoaderTest.java b/onyxia-api/src/test/java/fr/insee/onyxia/api/dao/universe/CatalogLoaderTest.java
index 4f852b3f..304b97dc 100644
--- a/onyxia-api/src/test/java/fr/insee/onyxia/api/dao/universe/CatalogLoaderTest.java
+++ b/onyxia-api/src/test/java/fr/insee/onyxia/api/dao/universe/CatalogLoaderTest.java
@@ -4,7 +4,7 @@
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.*;
-import static org.junit.jupiter.params.provider.Arguments.arguments;
+import static org.junit.jupiter.params.provider.Arguments.argumentSet;
import fr.insee.onyxia.api.configuration.CatalogWrapper;
import fr.insee.onyxia.api.configuration.CustomObjectMapper;
@@ -13,6 +13,7 @@
import fr.insee.onyxia.api.util.TestUtils;
import fr.insee.onyxia.model.helm.Chart;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -161,21 +162,144 @@ void packageOnClassPathNotFound() {
@ParameterizedTest
@MethodSource("includeKeywords")
- void filterIncludeKeywordsTest(List includeKeywords, Set expectedServices) {
+ @MethodSource("excludeKeywords")
+ @MethodSource("includeAnnotations")
+ @MethodSource("excludeAnnotations")
+ void filterServicesTest(
+ List includeKeywords,
+ List excludeKeywords,
+ Map includeAnnotations,
+ Map excludeAnnotations,
+ Set expectedServices) {
CatalogWrapper cw = new CatalogWrapper();
cw.setType("helm");
- cw.setLocation("classpath:/catalog-loader-test-with-keywords");
+ cw.setLocation("classpath:/catalog-loader-test-with-keywords-and-annotations");
cw.setIncludeKeywords(includeKeywords);
+ cw.setExcludeKeywords(excludeKeywords);
+ cw.setIncludeAnnotations(includeAnnotations);
+ cw.setExcludeAnnotations(excludeAnnotations);
catalogLoader.updateCatalog(cw);
assertEquals(expectedServices, cw.getCatalog().getEntries().keySet());
}
private static Stream includeKeywords() {
return Stream.of(
- arguments(List.of("CD"), Set.of("keepme")),
- arguments(List.of("CD", "Experimental"), Set.of("keepme", "excludeme")),
- arguments(List.of(), Set.of("keepme", "excludeme")),
- arguments(null, Set.of("keepme", "excludeme")),
- arguments(List.of("no one knows"), Set.of()));
+ argumentSet(
+ "One keyword to include",
+ List.of("CD"),
+ null,
+ null,
+ null,
+ Set.of("keepme")),
+ argumentSet(
+ "Two keywords to include",
+ List.of("CD", "Experimental"),
+ null,
+ null,
+ null,
+ Set.of("keepme", "excludeme")),
+ argumentSet(
+ "Empty list of keywords to include",
+ List.of(),
+ null,
+ null,
+ null,
+ Set.of("keepme", "excludeme")),
+ argumentSet(
+ "null for all filters",
+ null,
+ null,
+ null,
+ null,
+ Set.of("keepme", "excludeme")),
+ argumentSet(
+ "Unknown keyword to include",
+ List.of("no one knows"),
+ null,
+ null,
+ null,
+ Set.of()));
+ }
+
+ private static Stream excludeKeywords() {
+ return Stream.of(
+ argumentSet(
+ "One keyword to exclude",
+ null,
+ List.of("Experimental"),
+ null,
+ null,
+ Set.of("keepme")),
+ argumentSet(
+ "Exclusive keywords to include and exclude",
+ List.of("CD"),
+ List.of("Experimental"),
+ null,
+ null,
+ Set.of("keepme")),
+ argumentSet(
+ "Keyword to exclude takes precedence",
+ List.of("Experimental"),
+ List.of("Experimental"),
+ null,
+ null,
+ Set.of()),
+ argumentSet(
+ "Two keywords to exclude",
+ List.of("CD"),
+ List.of("CD", "Experimental"),
+ null,
+ null,
+ Set.of()),
+ argumentSet(
+ "Empty lists of keywords to include and exclude",
+ List.of(),
+ List.of(),
+ null,
+ null,
+ Set.of("keepme", "excludeme")),
+ argumentSet(
+ "Unknown keyword to exclude",
+ null,
+ List.of("no one knows"),
+ null,
+ null,
+ Set.of("keepme", "excludeme")));
+ }
+
+ private static Stream includeAnnotations() {
+ return Stream.of(
+ argumentSet(
+ "One annotation to include",
+ null,
+ null,
+ Map.of("lifecycle", "production"),
+ null,
+ Set.of("keepme")),
+ argumentSet(
+ "Exclude keyword takes precedence",
+ null,
+ List.of("CD"),
+ Map.of("lifecycle", "production"),
+ null,
+ Set.of()));
+ }
+
+ private static Stream excludeAnnotations() {
+ return Stream.of(
+ argumentSet(
+ "One annotation to exclude",
+ null,
+ null,
+ null,
+ Map.of("lifecycle", "production"),
+ Set.of("excludeme")),
+ argumentSet(
+ "Exclude annotation takes precedence",
+ null,
+ null,
+ Map.of("lifecycle", "production"),
+ Map.of("lifecycle", "production"),
+ Set.of()));
}
}
diff --git a/onyxia-api/src/test/resources/catalog-loader-test-with-keywords/catalogs.json5 b/onyxia-api/src/test/resources/catalog-loader-test-with-keywords-and-annotations/catalogs.json5
similarity index 100%
rename from onyxia-api/src/test/resources/catalog-loader-test-with-keywords/catalogs.json5
rename to onyxia-api/src/test/resources/catalog-loader-test-with-keywords-and-annotations/catalogs.json5
diff --git a/onyxia-api/src/test/resources/catalog-loader-test-with-keywords/index.yaml b/onyxia-api/src/test/resources/catalog-loader-test-with-keywords-and-annotations/index.yaml
similarity index 96%
rename from onyxia-api/src/test/resources/catalog-loader-test-with-keywords/index.yaml
rename to onyxia-api/src/test/resources/catalog-loader-test-with-keywords-and-annotations/index.yaml
index 0ec72541..57f698d1 100644
--- a/onyxia-api/src/test/resources/catalog-loader-test-with-keywords/index.yaml
+++ b/onyxia-api/src/test/resources/catalog-loader-test-with-keywords-and-annotations/index.yaml
@@ -28,6 +28,8 @@ entries:
maintainers:
- email: test@example.com
name: test
+ annotations:
+ lifecycle: production
- apiVersion: v2
appVersion: "1"
created: "2022-10-03T11:53:20.589754116Z"
@@ -55,6 +57,8 @@ entries:
maintainers:
- email: test@example.com
name: test
+ annotations:
+ lifecycle: production
- apiVersion: v2
appVersion: "2"
created: "2022-10-03T11:53:20.589754116Z"
@@ -79,6 +83,8 @@ entries:
urls:
- keepeme2.gz
version: 2.4.0
+ annotations:
+ lifecycle: experimental
excludeme:
- apiVersion: v2
appVersion: "1"
@@ -129,3 +135,5 @@ entries:
urls:
- excludeme2.gz
version: 2.4.0
+ annotations:
+ lifecycle: experimental
diff --git a/onyxia-model/src/main/java/fr/insee/onyxia/model/helm/Chart.java b/onyxia-model/src/main/java/fr/insee/onyxia/model/helm/Chart.java
index 266c7811..a066352b 100644
--- a/onyxia-model/src/main/java/fr/insee/onyxia/model/helm/Chart.java
+++ b/onyxia-model/src/main/java/fr/insee/onyxia/model/helm/Chart.java
@@ -22,7 +22,8 @@
"name",
"sources",
"urls",
- "version"
+ "version",
+ "annotations",
})
@Schema(description = "")
public class Chart extends Pkg {
@@ -277,6 +278,29 @@ public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
+ /**
+ * Does the chart have any of the given keywords?
+ *
+ * @param keywordsToCheck The list of keywords we're interested in.
+ * @return true if any of the given keywords appear in the keywords on the chart.
+ */
+ public Boolean hasKeywords(List keywordsToCheck) {
+ return getKeywords() != null
+ && keywordsToCheck.stream().anyMatch(keyword -> getKeywords().contains(keyword));
+ }
+
+ /**
+ * Does the chart have any of the given annotations?
+ *
+ * @param annotationsToCheck The map of annotations we're interested in.
+ * @return true if any of the given annotations appear in the annotations on the chart.
+ */
+ public Boolean hasAnnotations(Map annotationsToCheck) {
+ return getAnnotations() != null
+ && annotationsToCheck.entrySet().stream()
+ .anyMatch(annotation -> getAnnotations().entrySet().contains(annotation));
+ }
+
public static class Maintainer {
@JsonProperty("email")
private String email;