Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import fr.insee.onyxia.model.catalog.Pkg;
import fr.insee.onyxia.model.dto.CreateServiceDTO;
import fr.insee.onyxia.model.dto.ServicesListing;
import fr.insee.onyxia.model.dto.UpgradeServiceDTO;
import fr.insee.onyxia.model.helm.Chart;
import fr.insee.onyxia.model.project.Project;
import fr.insee.onyxia.model.region.Region;
Expand All @@ -36,7 +37,9 @@
import org.json.JSONObject;
import org.json.JSONTokener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@Tag(name = "My lab", description = "My services")
Expand Down Expand Up @@ -533,26 +536,7 @@
Map<String, Object> fusion = new HashMap<>();
fusion.putAll((Map<String, Object>) requestDTO.getOptions());

// If getConfig is null that means that no schema is provided : skip validation
if (pkg.getConfig() != null) {
JSONObject jsonSchema =
new JSONObject(
new JSONTokener(
jsonSchemaResolutionService
.resolveReferences(pkg.getConfig(), user.getRoles())
.toString()));

SchemaLoader loader =
SchemaLoader.builder()
.schemaJson(jsonSchema)
.draftV6Support() // or draftV7Support()
.build();
org.everit.json.schema.Schema schema = loader.load().build();
// Convert the options map to a JSONObject
JSONObject jsonObject = new JSONObject(fusion);
// Validate the options object against the schema
schema.validate(jsonObject);
}
validateOptionsAgainstSchema(pkg, user, fusion);

boolean skipTlsVerify = catalog.getSkipTlsVerify();
String caFile = catalog.getCaFile();
Expand All @@ -571,6 +555,124 @@
caFile);
}

private void validateOptionsAgainstSchema(Pkg pkg, User user, Map<String, Object> fusion)
throws Exception {

Check warning on line 559 in onyxia-api/src/main/java/fr/insee/onyxia/api/controller/api/mylab/MyLabController.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace generic exceptions with specific library exceptions or a custom exception.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia-api&issues=AaFHy4KtvVeOeStUFPsw&open=AaFHy4KtvVeOeStUFPsw&pullRequest=685
// If getConfig is null that means that no schema is provided : skip validation
if (pkg.getConfig() == null) {
return;
}
JSONObject jsonSchema =
new JSONObject(
new JSONTokener(
jsonSchemaResolutionService
.resolveReferences(pkg.getConfig(), user.getRoles())
.toString()));

SchemaLoader loader =
SchemaLoader.builder()
.schemaJson(jsonSchema)
.draftV6Support() // or draftV7Support()
.build();
org.everit.json.schema.Schema schema = loader.load().build();
// Convert the options map to a JSONObject
JSONObject jsonObject = new JSONObject(fusion);
// Validate the options object against the schema
schema.validate(jsonObject);
}

@Operation(
summary = "Upgrade an existing service.",
description =
"Re-deploy an existing service with new parameters (values.yaml, chart version, friendly name, share). The service name and namespace are kept. If the chart version is not provided, the service is upgraded with its current chart version.",
parameters = {
@Parameter(
name = "ONYXIA-PROJECT",
description =
"Project associated with the namespace, defaults to user project.",
in = ParameterIn.HEADER,
schema =
@Schema(
name = "ONYXIA-PROJECT",
type = "string",
description = "Generated project id.",
example = "project-id-example"))
})
@PutMapping("/app/{serviceId}")
public Object upgradeApp(
@Parameter(hidden = true) Region region,
@Parameter(hidden = true) Project project,
@PathVariable String serviceId,
@RequestBody UpgradeServiceDTO requestDTO)
throws Exception {
User user = userProvider.getUser(region);
Service userService = helmAppsService.getUserService(region, project, user, serviceId);
if (!serviceId.equals(userService.getId())) {
throw new NotFoundException();
}
String chart = userService.getChart();
int split = chart.lastIndexOf('-');
String chartName = chart.substring(0, split);
String currentVersion = chart.substring(split + 1);
Comment on lines +613 to +615
if (!(requestDTO.getOptions() instanceof Map<?, ?>)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "options must be an object");
}
String version =
requestDTO.getPackageVersion() != null
? requestDTO.getPackageVersion()
: currentVersion;
String catalogId = userService.getCatalogId();
// This code is for legacy compat for services that were created with Onyxia < v2.7.0
// before introduction of Onyxia's secret
if (catalogId == null) {
List<CatalogWrapper> elligibleCatalogs =
catalogService.getCatalogs(region, user).getCatalogs().stream()
.filter(
catalog ->
catalog.getCatalog()
.getPackageByName(chartName)
.isPresent())
.toList();
if (elligibleCatalogs.isEmpty()) {
throw new NotFoundException();
}
if (elligibleCatalogs.size() > 1) {
throw new IllegalStateException("Chart is present in multiple catalogs, abort");
}
CatalogWrapper catalog = elligibleCatalogs.getFirst();
catalogId = catalog.getId();
}
Optional<CatalogWrapper> catalog = catalogService.getCatalogById(catalogId, user);
if (catalog.isEmpty()) {
throw new IllegalStateException("Catalog " + catalogId + " is not available anymore");
}

Pkg pkg =
catalog.get()
.getCatalog()
.getPackageByNameAndVersion(chartName, version)
.orElseThrow(NotFoundException::new);

Map<String, Object> fusion = new HashMap<>();
fusion.putAll((Map<String, Object>) requestDTO.getOptions());

validateOptionsAgainstSchema(pkg, user, fusion);

return helmAppsService.upgradeApp(
region,
project,
catalogId,
chartName,
version,
user,
serviceId,
fusion,
requestDTO.getFriendlyName(),
requestDTO.getShare(),
catalog.get().getSkipTlsVerify(),
catalog.get().getTimeout(),
catalog.get().getCaFile());
}

public static class SuspendOrResumeRequestDTO {
private String serviceID;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package fr.insee.onyxia.api.events;

public class UpgradeServiceEvent extends InstallServiceEvent {

public static final String TYPE = "service.upgrade";

public UpgradeServiceEvent() {}

public UpgradeServiceEvent(
String username,
String namespace,
String releaseName,
String packageName,
String catalogId,
String friendlyName) {
super(username, namespace, releaseName, packageName, catalogId, friendlyName);
}

@Override
public String getType() {
return TYPE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@
final String caFile)
throws Exception;

Collection<Object> upgradeApp(

Check warning on line 47 in onyxia-api/src/main/java/fr/insee/onyxia/api/services/AppsService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Method has 13 parameters, which is greater than 7 authorized.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia-api&issues=AaFHy4IwvVeOeStUFPsu&open=AaFHy4IwvVeOeStUFPsu&pullRequest=685
Region region,
Project project,
String catalogId,
String chartName,
String version,
User user,
String serviceId,
Map<String, Object> fusion,
String friendlyName,
Boolean share,
final boolean skipTlsVerify,
String timeout,
final String caFile)
throws Exception;

Check warning on line 61 in onyxia-api/src/main/java/fr/insee/onyxia/api/services/AppsService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace generic exceptions with specific library exceptions or a custom exception.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia-api&issues=AaFHy4IwvVeOeStUFPst&open=AaFHy4IwvVeOeStUFPst&pullRequest=685

Service getUserService(Region region, Project project, User user, String serviceId)
throws Exception;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import fr.insee.onyxia.api.events.OnyxiaEventPublisher;
import fr.insee.onyxia.api.events.SuspendResumeServiceEvent;
import fr.insee.onyxia.api.events.UninstallServiceEvent;
import fr.insee.onyxia.api.events.UpgradeServiceEvent;
import fr.insee.onyxia.api.services.AppsService;
import fr.insee.onyxia.api.services.impl.kubernetes.KubernetesService;
import fr.insee.onyxia.api.services.utils.Base64Utils;
Expand Down Expand Up @@ -157,6 +158,72 @@
}
}

@Override
public Collection<Object> upgradeApp(
Region region,
Project project,
String catalogId,
String chartName,
String version,
User user,
String serviceId,
Map<String, Object> fusion,
String friendlyName,
Boolean share,
final boolean skipTlsVerify,
String timeout,
final String caFile)
throws IOException, TimeoutException, InterruptedException {

File values = File.createTempFile("values", ".yaml");

Check failure on line 178 in onyxia-api/src/main/java/fr/insee/onyxia/api/services/impl/HelmAppsService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make sure publicly writable directories are used safely here.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia-api&issues=AaFHy4EWvVeOeStUFPss&open=AaFHy4EWvVeOeStUFPss&pullRequest=685
mapperHelm.writeValue(values, fusion);
String namespaceId =
kubernetesService.determineNamespaceAndCreateIfNeeded(region, project, user);
try {
HelmInstaller res =
getHelmInstallService()
.installChart(
getHelmConfiguration(region, user),
catalogId + "/" + chartName,
namespaceId,
serviceId,
version,
false,
values,
null,
skipTlsVerify,
timeout,
caFile,
false);
UpgradeServiceEvent upgradeServiceEvent =
new UpgradeServiceEvent(
user.getIdep(),
namespaceId,
serviceId,
chartName,
catalogId,
friendlyName);
onyxiaEventPublisher.publishEvent(upgradeServiceEvent);
Map<String, String> metadata = new HashMap<>();
if (friendlyName != null) {
metadata.put(FRIENDLY_NAME, friendlyName);
}
if (share != null) {
metadata.put(SHARE, String.valueOf(share));
}
if (!metadata.isEmpty()) {
patchOnyxiaSecret(region, project, user, serviceId, metadata);
}
return List.of(res.getManifest());
} catch (IllegalArgumentException e) {
throw new AccessDeniedException(e.getMessage());
} finally {
if (!values.delete()) {
LOGGER.warn("Failed to delete values file, path {}", values.getAbsolutePath());
}
}
}

@Override
public CompletableFuture<ServicesListing> getUserServices(
Region region, Project project, User user) throws IOException, IllegalAccessException {
Expand Down Expand Up @@ -589,7 +656,12 @@
c -> {
Container container = new Container();
container.setName(c.getName());
container.setReady(c.getReady());
container.setReady(
pod.getMetadata()
.getDeletionTimestamp()
== null
&& Boolean.TRUE.equals(
c.getReady()));
currentTask.getContainers().add(container);
});
currentTask.setStatus(status);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package fr.insee.onyxia.model.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.swagger.v3.oas.annotations.media.Schema;

@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = "Parameter for service upgrade (re-deploying an existing service)")
public class UpgradeServiceDTO {
@Schema(
description =
"Version of the helm package to upgrade to. If null, the service is upgraded with its current chart version.",
required = false)
String packageVersion;

@Schema(description = "Content of values.yaml", required = true)
Object options;

@Schema(
description =
"When true, all users of the namespace will list this service. If null, the current value is kept.",
required = false)
Boolean share;

@Schema(
description = "A friendly name for the service. If null, the current value is kept.",
required = false)
String friendlyName;

public String getPackageVersion() {
return packageVersion;
}

public void setPackageVersion(String packageVersion) {
this.packageVersion = packageVersion;
}

public Object getOptions() {
return options;
}

public void setOptions(Object options) {
this.options = options;
}

public Boolean getShare() {
return share;
}

public void setShare(Boolean share) {
this.share = share;
}

public String getFriendlyName() {
return friendlyName;
}

public void setFriendlyName(String friendlyName) {
this.friendlyName = friendlyName;
}
}
Loading