diff --git a/docs/region-configuration.md b/docs/region-configuration.md
index d6234116..4cd4f549 100644
--- a/docs/region-configuration.md
+++ b/docs/region-configuration.md
@@ -11,20 +11,14 @@ See [regions.json](/onyxia-api/src/main/resources/regions.json) for a complete e
- [Region configuration](#region-configuration)
- [Main region properties](#main-region-properties)
- [Services properties](#services-properties)
- - [CustomInitScript properties](#custominitscript-properties)
- [Server properties](#server-properties)
- [K8sPublicEndpoint properties](#k8spublicendpoint-properties)
- [Quotas properties](#quotas-properties)
- [Expose properties](#expose-properties)
- [istio](#istio)
- [CertManager](#certManager)
- - [Default configuration properties](#default-configuration-properties)
- - [Kafka](#kafka)
- - [Sliders](#sliders)
- - [Resources](#resources)
- [Data properties](#data-properties)
- [S3](#s3)
- - [Atlas](#atlas)
- [Vault properties](#vault-properties)
- [Git properties](#git-properties)
- [ProxyConfiguration properties](#proxyconfiguration-properties)
@@ -70,34 +64,14 @@ Users can work on Onyxia as a User or as a Group to which they belong. Each user
| `groupPrefix` | | not used | |
| `authenticationMode` | serviceAccount | serviceAccount, impersonate or tokenPassthrough : on serviceAccount mode Onyxia API uses its own serviceAccount (by default admin or cluster-admin), with impersonate mode Onyxia requests the API with user's permissions (helm option `--kube-as-user`). With tokenPassthrough, the authentication token is passed to the API server. | |
| `expose` | | When users request to expose their service, only subdomain of this object domain are allowed | See [Expose properties](#expose-properties) |
-| `monitoring` | | Define the URL pattern of the monitoring service that is to be launched with each service. Only for client purposes. | {URLPattern: "https://$NAMESPACE-$INSTANCE.mymonitoring.sspcloud.fr"} |
-| `initScript` | | Define where to fetch a script that will be launched on some service on startup. | "https://inseefrlab.github.io/onyxia/onyxia-init.sh" |
+| `monitoring` | | Define the URL pattern of the monitoring service that is to be launched with each service. Only for client purposes. | {URLPattern: "https://$NAMESPACE-$INSTANCE.mymonitoring.sspcloud.fr"} | |
| `allowedURIPattern` | "^https://" | Init scripts set by the user have to respect this pattern. | |
| `server` | | Define the configuration of the services provider API server, this value is not served on the API as it contains credentials for the API. | See [Server properties](#server-properties) |
| `k8sPublicEndpoint` | | Define external access to Kubernetes API if available. It helps Onyxia users to directly connect to Kubernetes outside the datalab | See [K8sPublicEndpoint properties](#k8sPublicEndpoint-properties) |
| `quotas` | | Properties setting quotas on how many resources a user can get on the services provider. | See [Quotas properties](#quotas-properties) |
-| `defaultConfiguration` | | Default configuration on services that a user can override. For client purposes only. | See [Default Configuration](#default-configuration-properties) |
-| `customInitScript` | | This can be used to customize user environments using a regional script executed by some users' pods. | See [CustomInitScript properties](#custominitscript-properties)
-| `openshiftSCC` | | This can be used to inject SCC (Security Context Constraints) in openshift clusters | See [OpenshiftSCC properties](#openshiftSCC-properties) |
-| `customValues` | | This can be used to specify custom values that will be available for helm chart injection in the web app. Nested values are supported. | ` "customValues": {"myCustomKey": "myValue", "myNestedCustomKey": {"nestedKey": "nestedValue"} }` |
-### CustomInitScript properties
-These properties define how to reach the **service provider API**.
-
-| Key | Description | Example |
-| --------------------- | ------------------------------------------------------------------ | ---- |
-| `URL` | URL of the init script | "api.kub.sspcloud.fr" |
-| `checksum` | checksum of the init script | |
-
-### OpenshiftSCC properties
-These properties define if SCC should be injected in services for openshift clusters
-
-| Key | Description | Example |
-| --------------------- | ------------------------------------------------------------------ | ---- |
-| `enabled` | defaults to `false` | `true` |
-| `scc` | name of the SCC | `anyuid` |
### Server properties
@@ -168,62 +142,6 @@ A quota follows the Kubernetes model which is composed of:
-### Default configuration properties
-
-| Key | Default | Description |
-| --------------------- | ------- | ------------------------------------------------------------------ |
-| `IPProtection` | false | Whether or not the default behavior of the reverse proxy serving the service is to block a request from an IP other than the one from which it has been created. For client purposes only. |
-| `networkPolicy` | false | Whether or not services can be reached by pods outside of the current namespace. For client purposes only. |
-| `from` | NA | List of allowed sources (Kubernetes network policies format for from) to reach user HTTP services. Used to allow ingress access to users' services |
-| `nodeSelector` | NA | This node selector can be injected in a service to restrain on which node it can be launched |
-| `tolerations` | NA | This node selector can be injected in a service to force it to run on nodes with this taint |
-| `startupProbe` | NA | This startup probe can be injected into a service. It can help you in an environment with a slow network to specify a long duration before killing a container |
-| `kafka` | | See [Kafka](#kafka) |
-| `sliders` | | See [Sliders](#sliders) |
-| `Resources` | | See [Resources](#resources) |
-
-#### Kafka
-
-Kafka can be used to get some events in the user chart like Hive metastore.
-
-| Key | Default | Description |
-| --------------------- | ------- | ------------------------------------------------------------------ |
-| `URL` | N.A | brokerURL |
-| `topicName` | N.A | topic name for those events |
-
-#### Sliders
-
-Sliders specify some slider parameters that may overwrite some defaults.
-
-| Key | Default | Description |
-| --------------------- | ------- | ------------------------------------------------------------------ |
-| `cpu` | N.A | cpu slider parameters |
-| `memory` | N.A | memory slider parameters |
-| `gpu` | N.A | gpu slider parameters |
-| `disk` | N.A | disk slider parameters |
-
-
-| Key | Default | Description |
-| --------------------- | ------- | ------------------------------------------------------------------ |
-| `sliderMin` | N.A | sliderMin |
-| `sliderMax` | N.A | sliderMax |
-| `sliderStep` | N.A | sliderStep |
-| `sliderUnit` | N.A | sliderUnit |
-
-#### Resources
-
-Resources specify some values that may overwrite some defaults.
-
-| Key | Default | Description |
-| --------------------- | ------- | ------------------------------------------------------------------ |
-| `cpuRequest` | N.A | overwrite default CPU request if asked by helm-charts |
-| `cpuLimit` | N.A | overwrite default CPU limit if asked by helm-charts |
-| `memoryRequest` | N.A | overwrite default memory request if asked by helm-charts |
-| `memoryLimit` | N.A | overwrite default memory limit if asked by helm-charts |
-| `disk` | N.A | overwrite default disk size if asked by helm-charts |
-| `gpu` | N.A | overwrite default GPU if asked by helm-charts |
-
-
## Data properties
### S3
@@ -367,17 +285,7 @@ type Region = {
};
};
```
-
-### Atlas
-
-Atlas is a data management tool.
-
-It can be used to add additional features to the file explorer to transform it into a data explorer
-
-| Key | Default | Description | Example |
-| --------------------- | ------- | ------------------------------------------------------------------ | ---- |
-| `URL` | | URL of the atlas service for the region. | "https://atlas.change.me" |
-| `oidcConfiguration` | | Allow override of openidconnect authentication for this specific service. If not defined then global Onyxia authentication will be used. | {clientID: "onyxia", issuerURI: "https://auth.lab.sspcloud.fr/auth"} |
+|
## Vault properties
diff --git a/onyxia-api/pom.xml b/onyxia-api/pom.xml
index 5f1bbb7f..8e84ef99 100644
--- a/onyxia-api/pom.xml
+++ b/onyxia-api/pom.xml
@@ -100,6 +100,18 @@
jackson-databind
+
+ com.github.erosb
+ everit-json-schema
+ 1.14.2
+
+
+
+ javax.annotation
+ javax.annotation-api
+ 1.3.2
+
+
io.fabric8
kubernetes-server-mock
diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/RestExceptionHandler.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/RestExceptionHandler.java
index 8e684ea3..e23c703b 100644
--- a/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/RestExceptionHandler.java
+++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/RestExceptionHandler.java
@@ -1,6 +1,11 @@
package fr.insee.onyxia.api.controller;
+import fr.insee.onyxia.api.controller.exception.SchemaNotFoundException;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.everit.json.schema.ValidationException;
import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@@ -12,4 +17,59 @@ public class RestExceptionHandler {
@ResponseStatus(value = HttpStatus.FORBIDDEN)
@ExceptionHandler(AccessDeniedException.class)
public void handleAccessDeniedException(Exception ignored) {}
+
+ @ExceptionHandler(SchemaNotFoundException.class)
+ public ResponseEntity handleSchemaNotFoundException(SchemaNotFoundException ex) {
+ return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
+ }
+
+ @ExceptionHandler(ValidationException.class)
+ public ResponseEntity handleValidationException(ValidationException ex) {
+ List errors =
+ ex.getCausingExceptions().stream()
+ .map(ValidationException::getMessage)
+ .collect(Collectors.toList());
+
+ ErrorResponse errorResponse =
+ new ErrorResponse(HttpStatus.BAD_REQUEST.value(), "Validation failed", errors);
+
+ return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
+ }
+
+ // Define the ErrorResponse class within the GlobalExceptionHandler
+ public static class ErrorResponse {
+ private int status;
+ private String message;
+ private List errors;
+
+ public ErrorResponse(int status, String message, List errors) {
+ this.status = status;
+ this.message = message;
+ this.errors = errors;
+ }
+
+ public int getStatus() {
+ return status;
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public List getErrors() {
+ return errors;
+ }
+
+ public void setErrors(List errors) {
+ this.errors = errors;
+ }
+ }
}
diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/exception/SchemaNotFoundException.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/exception/SchemaNotFoundException.java
new file mode 100644
index 00000000..3387fe49
--- /dev/null
+++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/exception/SchemaNotFoundException.java
@@ -0,0 +1,8 @@
+package fr.insee.onyxia.api.controller.exception;
+
+public class SchemaNotFoundException extends RuntimeException {
+
+ public SchemaNotFoundException(String schemaName) {
+ super("Schema not found: " + schemaName);
+ }
+}
diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/pub/JsonSchemaController.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/pub/JsonSchemaController.java
new file mode 100644
index 00000000..00f4f695
--- /dev/null
+++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/pub/JsonSchemaController.java
@@ -0,0 +1,37 @@
+package fr.insee.onyxia.api.controller.pub;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import fr.insee.onyxia.api.controller.exception.SchemaNotFoundException;
+import fr.insee.onyxia.api.services.JsonSchemaRegistryService;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/public/schemas")
+public class JsonSchemaController {
+
+ private final JsonSchemaRegistryService jsonSchemaRegistryService;
+
+ @Autowired
+ public JsonSchemaController(JsonSchemaRegistryService jsonSchemaRegistryService) {
+ this.jsonSchemaRegistryService = jsonSchemaRegistryService;
+ }
+
+ @GetMapping
+ public Map listSchemas() {
+ return jsonSchemaRegistryService.listSchemas();
+ }
+
+ @GetMapping("/{schemaName}")
+ public JsonNode getSchema(@PathVariable String schemaName) {
+ JsonNode schema = jsonSchemaRegistryService.getSchema(schemaName);
+ if (schema == null) {
+ throw new SchemaNotFoundException(schemaName);
+ }
+ return schema;
+ }
+}
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 5eaa802a..93173209 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
@@ -4,19 +4,25 @@
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.zafarkhaja.semver.Version;
import fr.insee.onyxia.api.configuration.CatalogWrapper;
+import fr.insee.onyxia.api.services.JsonSchemaResolutionService;
import fr.insee.onyxia.model.catalog.Pkg;
import fr.insee.onyxia.model.helm.Chart;
import fr.insee.onyxia.model.helm.Repository;
import java.io.*;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Optional;
+import java.util.*;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+import org.everit.json.schema.Schema;
+import org.everit.json.schema.loader.SchemaLoader;
+import org.json.JSONObject;
+import org.json.JSONTokener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -31,13 +37,16 @@ public class CatalogLoader {
private static final Logger LOGGER = LoggerFactory.getLogger(CatalogLoader.class);
private final ResourceLoader resourceLoader;
-
+ private final JsonSchemaResolutionService jsonSchemaResolutionService;
private final ObjectMapper mapperHelm;
public CatalogLoader(
- ResourceLoader resourceLoader, @Qualifier("helm") ObjectMapper mapperHelm) {
+ ResourceLoader resourceLoader,
+ @Qualifier("helm") ObjectMapper mapperHelm,
+ JsonSchemaResolutionService jsonSchemaResolutionService) {
this.resourceLoader = resourceLoader;
this.mapperHelm = mapperHelm;
+ this.jsonSchemaResolutionService = jsonSchemaResolutionService;
}
public void updateCatalog(CatalogWrapper cw) {
@@ -184,10 +193,11 @@ public void extractDataFromTgz(InputStream in, Chart chart) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
-
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
tarIn.transferTo(baos);
- chart.setConfig(mapper.readTree(baos.toString(UTF_8)));
+ chart.setConfig(
+ jsonSchemaResolutionService.resolveReferences(
+ mapper.readTree(baos.toString(UTF_8))));
}
} else if (entryName.endsWith(chartName + "/values.yaml")
&& !entryName.endsWith("charts/" + chartName + "/values.yaml")) {
@@ -199,4 +209,80 @@ public void extractDataFromTgz(InputStream in, Chart chart) throws IOException {
}
}
}
+
+ public JsonNode resolveInternalReferences(JsonNode schemaNode, ObjectMapper objectMapper)
+ throws IOException {
+ // Convert the main schema JSON node to JSONObject
+ JSONObject schemaJson = new JSONObject(new JSONTokener(schemaNode.toString()));
+
+ // Create a SchemaLoader
+ SchemaLoader loader =
+ SchemaLoader.builder()
+ .schemaJson(schemaJson)
+ .resolutionScope("file:///") // Setting a base URI for relative references
+ .build();
+
+ // Load and expand the schema
+ Schema schema = loader.load().build();
+
+ // Convert the resolved schema back to JsonNode
+ JSONObject resolvedSchemaJson = new JSONObject(schema.toString());
+ return objectMapper.readTree(resolvedSchemaJson.toString());
+ }
+
+ public JsonNode resolveInternalReferences(JsonNode schemaNode) {
+ return resolveInternalReferences(schemaNode, schemaNode);
+ }
+
+ private JsonNode resolveInternalReferences(JsonNode schemaNode, JsonNode rootNode) {
+ if (schemaNode.isObject()) {
+ ObjectNode objectNode = (ObjectNode) schemaNode;
+ Map updates = new HashMap<>();
+
+ Iterator> fields = objectNode.fields();
+ while (fields.hasNext()) {
+ Map.Entry field = fields.next();
+ if (field.getKey().equals("$ref") && field.getValue().isTextual()) {
+ String ref = field.getValue().asText();
+ if (ref.startsWith("#/definitions/")) {
+ String refName = ref.substring("#/definitions/".length());
+ JsonNode refNode = rootNode.at("/definitions/" + refName);
+ if (!refNode.isMissingNode()) {
+ JsonNode resolvedNode =
+ resolveInternalReferences(refNode.deepCopy(), rootNode);
+ updates.putAll(convertToMap((ObjectNode) resolvedNode));
+ updates.put("$ref", null);
+ }
+ }
+ } else {
+ updates.put(
+ field.getKey(), resolveInternalReferences(field.getValue(), rootNode));
+ }
+ }
+
+ for (Map.Entry update : updates.entrySet()) {
+ if (update.getValue() == null) {
+ objectNode.remove(update.getKey());
+ } else {
+ objectNode.set(update.getKey(), update.getValue());
+ }
+ }
+ } else if (schemaNode.isArray()) {
+ ArrayNode arrayNode = (ArrayNode) schemaNode;
+ for (int i = 0; i < arrayNode.size(); i++) {
+ arrayNode.set(i, resolveInternalReferences(arrayNode.get(i), rootNode));
+ }
+ }
+ return schemaNode;
+ }
+
+ private Map convertToMap(ObjectNode objectNode) {
+ Map map = new HashMap<>();
+ Iterator> fields = objectNode.fields();
+ while (fields.hasNext()) {
+ Map.Entry field = fields.next();
+ map.put(field.getKey(), field.getValue());
+ }
+ return map;
+ }
}
diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/services/JsonSchemaRegistryService.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/services/JsonSchemaRegistryService.java
new file mode 100644
index 00000000..315ea49d
--- /dev/null
+++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/services/JsonSchemaRegistryService.java
@@ -0,0 +1,89 @@
+package fr.insee.onyxia.api.services;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Service
+public class JsonSchemaRegistryService {
+
+ private static final String SCHEMA_DIRECTORY = "/schemas"; // Resource directory
+ private final ObjectMapper objectMapper;
+ private final Map schemaRegistry;
+
+ @Value("${external.schema.directory:/external-schemas}") // External directory path
+ private String externalSchemaDirectory;
+
+ public JsonSchemaRegistryService() {
+ this.objectMapper = new ObjectMapper();
+ this.schemaRegistry = new HashMap<>();
+ }
+
+ @PostConstruct
+ private void loadSchemas() throws IOException, URISyntaxException {
+ // Load initial schemas from resources
+ loadResourceSchemas();
+
+ // Load schemas from the external directory if it exists
+ loadExternalSchemas();
+ }
+
+ private void loadResourceSchemas() throws IOException, URISyntaxException {
+ Path resourcePath =
+ Paths.get(JsonSchemaRegistryService.class.getResource(SCHEMA_DIRECTORY).toURI());
+ Files.walk(resourcePath)
+ .filter(Files::isRegularFile)
+ .forEach(path -> loadSchema(resourcePath, path));
+ }
+
+ private void loadExternalSchemas() throws IOException {
+ Path externalPath = Paths.get(externalSchemaDirectory);
+ if (Files.exists(externalPath)) {
+ Files.walk(externalPath)
+ .filter(Files::isRegularFile)
+ .forEach(path -> loadSchema(externalPath, path));
+ }
+ }
+
+ private void loadSchema(Path basePath, Path path) {
+ try {
+ JsonNode schema = objectMapper.readTree(path.toFile());
+ String key = generateKey(basePath, path);
+ schemaRegistry.put(key, schema);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load schema: " + path.getFileName(), e);
+ }
+ }
+
+ private String generateKey(Path basePath, Path path) {
+ // Remove the base directory part from the path and replace the file separators with dots
+ Path relativePath = basePath.relativize(path);
+ return relativePath.toString().replace(File.separatorChar, '/');
+ }
+
+ public void refreshExternalSchemas() throws IOException {
+ loadExternalSchemas();
+ }
+
+ public Map listSchemas() {
+ return new HashMap<>(schemaRegistry);
+ }
+
+ public JsonNode getSchema(String schemaName) {
+ return schemaRegistry.get(schemaName);
+ }
+
+ public void overwriteSchema(String schemaName, JsonNode newSchema) {
+ schemaRegistry.put(schemaName, newSchema);
+ }
+}
diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/services/JsonSchemaResolutionService.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/services/JsonSchemaResolutionService.java
new file mode 100644
index 00000000..9c43b77b
--- /dev/null
+++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/services/JsonSchemaResolutionService.java
@@ -0,0 +1,95 @@
+package fr.insee.onyxia.api.services;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class JsonSchemaResolutionService {
+
+ private final ObjectMapper objectMapper;
+ private final JsonSchemaRegistryService registryService;
+
+ @Autowired
+ public JsonSchemaResolutionService(JsonSchemaRegistryService registryService) {
+ this.objectMapper = new ObjectMapper();
+ this.registryService = registryService;
+ }
+
+ public JsonNode resolveReferences(JsonNode schemaNode) {
+ return resolveReferences(schemaNode, schemaNode);
+ }
+
+ private JsonNode resolveReferences(JsonNode schemaNode, JsonNode rootNode) {
+ if (schemaNode.isObject()) {
+ ObjectNode objectNode = (ObjectNode) schemaNode;
+ Iterator> fields = objectNode.fields();
+ Map updates = new HashMap<>();
+
+ while (fields.hasNext()) {
+ Map.Entry field = fields.next();
+ JsonNode fieldValue = field.getValue();
+
+ if (field.getKey().equals("$ref") && fieldValue.isTextual()) {
+ String ref = fieldValue.asText();
+ JsonNode refNode = null;
+ if (ref.startsWith("#/definitions/")) {
+ refNode = rootNode.at(ref.substring(1));
+ } else {
+ refNode = registryService.getSchema(ref);
+ }
+
+ if (refNode != null && !refNode.isMissingNode()) {
+ JsonNode resolvedNode = resolveReferences(refNode.deepCopy(), rootNode);
+ updates.putAll(convertToMap((ObjectNode) resolvedNode));
+ updates.put("$ref", null);
+ }
+ } else if (fieldValue.isObject()
+ && fieldValue.has("x-onyxia")
+ && fieldValue.get("x-onyxia").has("overwriteSchemaWith")) {
+ String overrideSchemaName =
+ fieldValue.get("x-onyxia").get("overwriteSchemaWith").asText();
+ JsonNode overrideSchemaNode = registryService.getSchema(overrideSchemaName);
+
+ if (overrideSchemaNode != null && !overrideSchemaNode.isMissingNode()) {
+ JsonNode resolvedNode =
+ resolveReferences(overrideSchemaNode.deepCopy(), rootNode);
+ updates.put(field.getKey(), resolvedNode);
+ }
+ } else if (fieldValue.isObject() || fieldValue.isArray()) {
+ updates.put(field.getKey(), resolveReferences(fieldValue, rootNode));
+ }
+ }
+
+ for (Map.Entry update : updates.entrySet()) {
+ if (update.getValue() == null) {
+ objectNode.remove(update.getKey());
+ } else {
+ objectNode.set(update.getKey(), update.getValue());
+ }
+ }
+ } else if (schemaNode.isArray()) {
+ ArrayNode arrayNode = (ArrayNode) schemaNode;
+ for (int i = 0; i < arrayNode.size(); i++) {
+ arrayNode.set(i, resolveReferences(arrayNode.get(i), rootNode));
+ }
+ }
+ return schemaNode;
+ }
+
+ private Map convertToMap(ObjectNode objectNode) {
+ Map map = new HashMap<>();
+ Iterator> fields = objectNode.fields();
+ while (fields.hasNext()) {
+ Map.Entry field = fields.next();
+ map.put(field.getKey(), field.getValue());
+ }
+ return map;
+ }
+}
diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/services/impl/HelmAppsService.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/services/impl/HelmAppsService.java
index 5be9b072..817ee04b 100644
--- a/onyxia-api/src/main/java/fr/insee/onyxia/api/services/impl/HelmAppsService.java
+++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/services/impl/HelmAppsService.java
@@ -40,6 +40,11 @@
import java.util.concurrent.TimeoutException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.FastDateFormat;
+import org.everit.json.schema.Schema;
+import org.everit.json.schema.ValidationException;
+import org.everit.json.schema.loader.SchemaLoader;
+import org.json.JSONObject;
+import org.json.JSONTokener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -104,7 +109,15 @@ public Collection