From 71fb537e226621550d1776914bdc1b87efe993c1 Mon Sep 17 00:00:00 2001 From: John Kasper Svergja Date: Fri, 9 Jan 2026 11:14:58 +0100 Subject: [PATCH 1/2] feat(helm): support for specifying flag on region, forceConflicts to start with The implementation allows one to easily add helm flags on the region - which will then be used during helm upgrade (i.e. install, suspend, resume). The reason to add it on the region is that these flags make sense for all services rather then on a catalog or service level (these kind of flags manipulate how helm functions, and we want the same result for all services, at least as of now). --- docs/region-configuration.md | 9 ++ .../helmwrapper/service/HelmFlags.java | 58 +++++++++ .../service/HelmInstallService.java | 114 +----------------- .../controller/api/mylab/MyLabController.java | 18 +-- .../onyxia/api/services/AppsService.java | 11 +- .../api/services/impl/HelmAppsService.java | 88 ++++---------- .../fr/insee/onyxia/model/region/Region.java | 24 ++++ 7 files changed, 129 insertions(+), 193 deletions(-) create mode 100644 helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/service/HelmFlags.java diff --git a/docs/region-configuration.md b/docs/region-configuration.md index c97f5ea3..d093752b 100644 --- a/docs/region-configuration.md +++ b/docs/region-configuration.md @@ -68,6 +68,7 @@ Users can work on Onyxia as a User or as a Group to which they belong. Each user | `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) | +| `helm` | | Helm flags used by Onyxia on every `helm upgrade` (install, suspend, resume). | See [Helm properties](#helm-properties) | @@ -142,6 +143,14 @@ Note : If you want Onyxia to create the ResourceQuota but not override it at eac | `gateways` | [] | List of istio gateways to be used. Should contain at least one element. E.g. `["istio-system/my-gateway"]` | +### Helm properties + +It can be used to add additional flags which will be used when installing, resuming and suspending services running Onyxia. + +| Key | Default | Description | +|------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `forceConflicts` | false | If set server-side apply will force changes against conflicts, also see https://helm.sh/docs/helm/helm_upgrade#options . This might be useful if you have mutating webhooks which take ownership of fields, which would normaly result in Helm 4 to fail (see https://helm.sh/community/hips/hip-0023#conflicts-and-forcing ). | + ## Data properties diff --git a/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/service/HelmFlags.java b/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/service/HelmFlags.java new file mode 100644 index 00000000..08de0ef4 --- /dev/null +++ b/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/service/HelmFlags.java @@ -0,0 +1,58 @@ +package io.github.inseefrlab.helmwrapper.service; + +public record HelmFlags( + boolean dryRun, + boolean skipTlsVerify, + String timeout, + String caFile, + boolean reuseValues, + boolean forceConflicts) { + + public static HelmFlags suspendAndResumeFlags( + boolean dryRun, + boolean skipTlsVerify, + String timeout, + String caFile, + boolean forceConflicts) { + return new HelmFlags(dryRun, skipTlsVerify, timeout, caFile, true, forceConflicts); + } + + public static HelmFlags installFlags( + boolean dryRun, + boolean skipTlsVerify, + String timeout, + String caFile, + boolean forceConflicts) { + return new HelmFlags(dryRun, skipTlsVerify, timeout, caFile, false, forceConflicts); + } + + /** + * @return cli ready string to use for helm upgrade, ending with a space such that it is safe to + * further append on + */ + public String toHelmUpgradeCliString() { + StringBuilder result = new StringBuilder(); + if (forceConflicts) { + result.append("--force-conflicts "); + } + + if (timeout != null) { + result.append("--timeout " + timeout + " "); + } + + if (skipTlsVerify) { + result.append("--insecure-skip-tls-verify "); + } else if (caFile != null) { + result.append("--ca-file " + System.getenv("CACERTS_DIR") + "/" + caFile + " "); + } + + if (dryRun) { + result.append("--dry-run "); + } + if (reuseValues) { + result.append("--reuse-values "); + } + + return result.toString(); + } +} diff --git a/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/service/HelmInstallService.java b/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/service/HelmInstallService.java index 664debb5..53b7c1f7 100644 --- a/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/service/HelmInstallService.java +++ b/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/service/HelmInstallService.java @@ -38,111 +38,15 @@ public class HelmInstallService { private static final String MANIFEST_INFO_TYPE = "manifest"; private static final String NOTES_INFO_TYPE = "notes"; - public void resume( - HelmConfiguration configuration, - String chart, - String namespace, - String name, - String version, - boolean dryRun, - final boolean skipTlsVerify, - String timeout, - String caFile) - throws InvalidExitValueException, - IOException, - InterruptedException, - TimeoutException, - IllegalArgumentException { - installChart( - configuration, - chart, - namespace, - name, - version, - dryRun, - null, - Map.of("global.suspend", "false"), - skipTlsVerify, - timeout, - caFile, - true); - } - - public void suspend( - HelmConfiguration configuration, - String chart, - String namespace, - String name, - String version, - boolean dryRun, - final boolean skipTlsVerify, - String timeout, - String caFile) - throws InvalidExitValueException, - IOException, - InterruptedException, - TimeoutException, - IllegalArgumentException { - installChart( - configuration, - chart, - namespace, - name, - version, - dryRun, - null, - Map.of("global.suspend", "true"), - skipTlsVerify, - timeout, - caFile, - true); - } - - public HelmInstaller installChart( - HelmConfiguration configuration, - String chart, - String namespace, - String name, - String version, - boolean dryRun, - File values, - Map env, - final boolean skipTlsVerify, - String timeout, - String caFile) - throws InvalidExitValueException, - IOException, - InterruptedException, - TimeoutException, - IllegalArgumentException { - return installChart( - configuration, - chart, - namespace, - name, - version, - dryRun, - values, - env, - skipTlsVerify, - timeout, - caFile, - false); - } - public HelmInstaller installChart( HelmConfiguration configuration, String chart, String namespace, String name, String version, - boolean dryRun, File values, Map env, - final boolean skipTlsVerify, - String timeout, - String caFile, - boolean reuseValues) + HelmFlags additionalFlags) throws InvalidExitValueException, IOException, InterruptedException, @@ -150,15 +54,7 @@ public HelmInstaller installChart( IllegalArgumentException { StringBuilder command = new StringBuilder("helm upgrade --install --history-max 0 "); - if (timeout != null) { - command.append("--timeout " + timeout + " "); - } - - if (skipTlsVerify) { - command.append("--insecure-skip-tls-verify "); - } else if (caFile != null) { - command.append("--ca-file " + System.getenv("CACERTS_DIR") + "/" + caFile + " "); - } + command.append(additionalFlags.toHelmUpgradeCliString()); if (name != null) { if (!helmNamePattern.matcher(name).matches() || name.length() > 53) { @@ -196,12 +92,6 @@ public HelmInstaller installChart( if (env != null) { command.append(buildEnvVar(env)); } - if (dryRun) { - command.append(" --dry-run"); - } - if (reuseValues) { - command.append(" --reuse-values"); - } String res = Command.executeAndGetResponseAsJson(configuration, command.toString()) .getOutput() diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/api/mylab/MyLabController.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/api/mylab/MyLabController.java index 657e94cf..4440c57e 100644 --- a/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/api/mylab/MyLabController.java +++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/api/mylab/MyLabController.java @@ -23,6 +23,7 @@ import io.fabric8.kubernetes.client.Watch; import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.WatcherException; +import io.github.inseefrlab.helmwrapper.service.HelmFlags; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; @@ -297,6 +298,13 @@ private void suspendOrResume(Region region, Project project, String serviceId, b throw new IllegalStateException("Catalog " + catalogId + " is not available anymore"); } + var flags = + HelmFlags.suspendAndResumeFlags( + false, + catalog.get().getSkipTlsVerify(), + catalog.get().getTimeout(), + catalog.get().getCaFile(), + region.getServices().getHelm().getForceConflicts()); if (suspend) { helmAppsService.suspend( region, @@ -306,10 +314,7 @@ private void suspendOrResume(Region region, Project project, String serviceId, b version, user, serviceId, - catalog.get().getSkipTlsVerify(), - catalog.get().getTimeout(), - catalog.get().getCaFile(), - false); + flags); } else { helmAppsService.resume( region, @@ -319,10 +324,7 @@ private void suspendOrResume(Region region, Project project, String serviceId, b version, user, serviceId, - catalog.get().getSkipTlsVerify(), - catalog.get().getTimeout(), - catalog.get().getCaFile(), - false); + flags); } } diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/services/AppsService.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/services/AppsService.java index 5927c96b..f3e28655 100644 --- a/onyxia-api/src/main/java/fr/insee/onyxia/api/services/AppsService.java +++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/services/AppsService.java @@ -11,6 +11,7 @@ import io.fabric8.kubernetes.api.model.Event; import io.fabric8.kubernetes.client.Watch; import io.fabric8.kubernetes.client.Watcher; +import io.github.inseefrlab.helmwrapper.service.HelmFlags; import io.github.inseefrlab.helmwrapper.service.HelmInstallService; import java.io.IOException; import java.text.ParseException; @@ -69,10 +70,7 @@ void resume( String version, User user, String serviceId, - boolean skipTlsVerify, - String timeout, - String caFile, - boolean dryRun) + HelmFlags flags) throws IOException, InterruptedException, TimeoutException; void suspend( @@ -83,9 +81,6 @@ void suspend( String version, User user, String serviceId, - boolean skipTlsVerify, - String timeout, - String caFile, - boolean dryRun) + HelmFlags flags) throws IOException, InterruptedException, TimeoutException; } 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 0af7584b..1c911855 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 @@ -29,6 +29,7 @@ import io.github.inseefrlab.helmwrapper.model.HelmInstaller; import io.github.inseefrlab.helmwrapper.model.HelmLs; import io.github.inseefrlab.helmwrapper.model.HelmReleaseInfo; +import io.github.inseefrlab.helmwrapper.service.HelmFlags; import io.github.inseefrlab.helmwrapper.service.HelmInstallService; import io.github.inseefrlab.helmwrapper.service.HelmInstallService.MultipleServiceFound; import java.io.File; @@ -123,12 +124,14 @@ public Collection installApp( namespaceId, requestDTO.getName(), pkg.getVersion(), - requestDTO.isDryRun(), values, null, - skipTlsVerify, - timeout, - caFile); + HelmFlags.installFlags( + requestDTO.isDryRun(), + skipTlsVerify, + timeout, + caFile, + region.getServices().getHelm().getForceConflicts())); InstallServiceEvent installServiceEvent = new InstallServiceEvent( user.getIdep(), @@ -413,24 +416,10 @@ public void suspend( String version, User user, String serviceId, - boolean skipTlsVerify, - String timeout, - String caFile, - boolean dryRun) + HelmFlags flags) throws IOException, InterruptedException, TimeoutException { suspendOrResume( - region, - project, - catalogId, - chartName, - version, - user, - serviceId, - skipTlsVerify, - timeout, - caFile, - dryRun, - true); + region, project, catalogId, chartName, version, user, serviceId, flags, true); } @Override @@ -442,24 +431,10 @@ public void resume( String version, User user, String serviceId, - boolean skipTlsVerify, - String timeout, - String caFile, - boolean dryRun) + HelmFlags flags) throws IOException, InterruptedException, TimeoutException { suspendOrResume( - region, - project, - catalogId, - chartName, - version, - user, - serviceId, - skipTlsVerify, - timeout, - caFile, - dryRun, - false); + region, project, catalogId, chartName, version, user, serviceId, flags, false); } public void suspendOrResume( @@ -470,39 +445,22 @@ public void suspendOrResume( String version, User user, String serviceId, - boolean skipTlsVerify, - String timeout, - String caFile, - boolean dryRun, + HelmFlags flags, boolean suspend) throws IOException, InterruptedException, TimeoutException { String namespaceId = kubernetesService.determineNamespaceAndCreateIfNeeded(region, project, user); - if (suspend) { - getHelmInstallService() - .suspend( - getHelmConfiguration(region, user, namespaceId), - catalogId + "/" + chartName, - namespaceId, - serviceId, - version, - dryRun, - skipTlsVerify, - timeout, - caFile); - } else { - getHelmInstallService() - .resume( - getHelmConfiguration(region, user, namespaceId), - catalogId + "/" + chartName, - namespaceId, - serviceId, - version, - dryRun, - skipTlsVerify, - timeout, - caFile); - } + var suspendEnv = Map.of(SUSPEND_KEY, suspend ? "true" : "false"); + getHelmInstallService() + .installChart( + getHelmConfiguration(region, user, namespaceId), + catalogId + "/" + chartName, + namespaceId, + serviceId, + version, + null, + suspendEnv, + flags); SuspendResumeServiceEvent event = new SuspendResumeServiceEvent( user.getIdep(), namespaceId, serviceId, chartName, catalogId, suspend); diff --git a/onyxia-model/src/main/java/fr/insee/onyxia/model/region/Region.java b/onyxia-model/src/main/java/fr/insee/onyxia/model/region/Region.java index 397c0edb..72a53781 100644 --- a/onyxia-model/src/main/java/fr/insee/onyxia/model/region/Region.java +++ b/onyxia-model/src/main/java/fr/insee/onyxia/model/region/Region.java @@ -178,6 +178,7 @@ public static class Services { private Map namespaceLabels = new HashMap<>(); private Map namespaceAnnotations = new HashMap<>(); private boolean userNamespace = true; + private String namespacePrefix = "user-"; private String groupNamespacePrefix = "projet-"; private String usernamePrefix; @@ -189,6 +190,7 @@ public static class Services { private Monitoring monitoring; private String allowedURIPattern = "^https://"; private Quotas quotas = new Quotas(); + private Helm helm = new Helm(); /*** * @Deprecated since v3 @@ -355,6 +357,14 @@ public void setQuotas(Quotas quotas) { this.quotas = quotas; } + public Helm getHelm() { + return helm; + } + + public void setHelm(Helm helm) { + this.helm = helm; + } + public static enum AuthenticationMode { @JsonProperty("impersonate") IMPERSONATE, @@ -443,6 +453,20 @@ public Map getRolesQuota() { return rolesQuota; } } + + public static class Helm { + + /** If set server-side apply will force changes against conflicts */ + private boolean forceConflicts = false; + + public boolean getForceConflicts() { + return forceConflicts; + } + + public void setForceConflicts(boolean forceConflicts) { + this.forceConflicts = forceConflicts; + } + } } public static class Monitoring { From c8b644e5187bad8e0c6b439f8072c92955c4b1e3 Mon Sep 17 00:00:00 2001 From: John Kasper Svergja Date: Mon, 12 Jan 2026 11:49:21 +0100 Subject: [PATCH 2/2] feat: add flag for server side forceConflicts requires that upgrade is done server side. Thus we want the ability to supply this flag --- .../helmwrapper/service/HelmFlags.java | 18 +++++++++++++----- .../controller/api/mylab/MyLabController.java | 3 ++- .../api/services/impl/HelmAppsService.java | 3 ++- .../fr/insee/onyxia/model/region/Region.java | 10 ++++++++++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/service/HelmFlags.java b/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/service/HelmFlags.java index 08de0ef4..f3e7d54a 100644 --- a/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/service/HelmFlags.java +++ b/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/service/HelmFlags.java @@ -6,15 +6,18 @@ public record HelmFlags( String timeout, String caFile, boolean reuseValues, - boolean forceConflicts) { + boolean forceConflicts, + String serverSide) { public static HelmFlags suspendAndResumeFlags( boolean dryRun, boolean skipTlsVerify, String timeout, String caFile, - boolean forceConflicts) { - return new HelmFlags(dryRun, skipTlsVerify, timeout, caFile, true, forceConflicts); + boolean forceConflicts, + String serverSide) { + return new HelmFlags( + dryRun, skipTlsVerify, timeout, caFile, true, forceConflicts, serverSide); } public static HelmFlags installFlags( @@ -22,8 +25,10 @@ public static HelmFlags installFlags( boolean skipTlsVerify, String timeout, String caFile, - boolean forceConflicts) { - return new HelmFlags(dryRun, skipTlsVerify, timeout, caFile, false, forceConflicts); + boolean forceConflicts, + String serverSide) { + return new HelmFlags( + dryRun, skipTlsVerify, timeout, caFile, false, forceConflicts, serverSide); } /** @@ -35,6 +40,9 @@ public String toHelmUpgradeCliString() { if (forceConflicts) { result.append("--force-conflicts "); } + if (serverSide != null) { + result.append(" --server-side " + serverSide + " "); + } if (timeout != null) { result.append("--timeout " + timeout + " "); diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/api/mylab/MyLabController.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/api/mylab/MyLabController.java index 4440c57e..0e0bc735 100644 --- a/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/api/mylab/MyLabController.java +++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/api/mylab/MyLabController.java @@ -304,7 +304,8 @@ private void suspendOrResume(Region region, Project project, String serviceId, b catalog.get().getSkipTlsVerify(), catalog.get().getTimeout(), catalog.get().getCaFile(), - region.getServices().getHelm().getForceConflicts()); + region.getServices().getHelm().getForceConflicts(), + region.getServices().getHelm().getServerSide()); if (suspend) { helmAppsService.suspend( region, 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 1c911855..1a1d7c83 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 @@ -131,7 +131,8 @@ public Collection installApp( skipTlsVerify, timeout, caFile, - region.getServices().getHelm().getForceConflicts())); + region.getServices().getHelm().getForceConflicts(), + region.getServices().getHelm().getServerSide())); InstallServiceEvent installServiceEvent = new InstallServiceEvent( user.getIdep(), diff --git a/onyxia-model/src/main/java/fr/insee/onyxia/model/region/Region.java b/onyxia-model/src/main/java/fr/insee/onyxia/model/region/Region.java index 72a53781..fa705c3b 100644 --- a/onyxia-model/src/main/java/fr/insee/onyxia/model/region/Region.java +++ b/onyxia-model/src/main/java/fr/insee/onyxia/model/region/Region.java @@ -459,6 +459,8 @@ public static class Helm { /** If set server-side apply will force changes against conflicts */ private boolean forceConflicts = false; + private String serverSide = "auto"; + public boolean getForceConflicts() { return forceConflicts; } @@ -466,6 +468,14 @@ public boolean getForceConflicts() { public void setForceConflicts(boolean forceConflicts) { this.forceConflicts = forceConflicts; } + + public String getServerSide() { + return serverSide; + } + + public void setServerSide(String serverSide) { + this.serverSide = serverSide; + } } }