diff --git a/docs/SPECIFICATION.md b/docs/SPECIFICATION.md index 4cb6c8c2..839196ca 100644 --- a/docs/SPECIFICATION.md +++ b/docs/SPECIFICATION.md @@ -45,17 +45,6 @@ My Lab endpoints are available to authenticated users, offering a programmatic w The endpoints routed by `fr.insee.onyxia.api.controller.api.mylab.QuotaController`, are only functional if a quota feature is set on the Region, and allow users to: - `GET /my-lab/quota` get their quota on the namespace, which is implemented by a quota resource with the name `onyxia-quota` -- `POST /my-lab/quota` change their quota on the namepsace, if quota changing is enabled -- `POST /my-lab/quota/reset` reset their quota to the default one on the namespace - -### /onboarding/ - -The request to onboard process entails the creation of the Kubernetes namespace for the Group and the creation of a RoleBinding to that namespace giving the default ServiceAccount of that namespace admin permissions. There is a check in whether the user that made the request belongs to the group, based on the OIDC token, and the group onboarding process is triggered if so. - -*NOTE: this onboarding process is being done behind the scenes when requesting a first service, and the endpoint use is not the default behaviour.* - -If no group is given in the body, it is assumed that it is a user onboard, i.e., a namespace for a single user rather than a Group (Project), and the creation of the namespace and RoleBinding remains the same. However, in the user use-case, there is also the possibility to set Quotas on the namespace during creation, and these are obtained in the Region definition configuration. - ### /public/ Public endpoints are available to all, offering a programmatic way to comprehend the current configuration and the available services in Onyxia. They allow users to: diff --git a/docs/region-configuration.md b/docs/region-configuration.md index e367c687..13a8a616 100644 --- a/docs/region-configuration.md +++ b/docs/region-configuration.md @@ -50,10 +50,6 @@ Users can work on Onyxia as a User or as a Group to which they belong. Each user | Key | Default | Description | Example | |-------------------------------| ------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------| | `type` | | Type of the platform on which services are launched. Only Kubernetes is supported, Marathon has been removed. | "KUBERNETES" | -| `allowNamespaceCreation` | true | **Deprecated**, use the onboarding API instead. If true, the /onboarding endpoint is enabled and the user will have a namespace created on its first request on a service resource. Ignored (forced to false) with `serviceAccountInTargetNamespace` authentication. | true | -| `namespaceLabels` | | Static labels to add to the namespace (at creation and subsequent user logins) | {"zone":"prod"} | -| `namespaceAnnotations` | | Static annotations to add to the namespace (at creation and subsequent user logins) | {"zone":"prod"} | -| `namespaceAnnotationsDynamic` | | Dynamic annotations (currently only based on user JWT token) to add to the namespace (at creation and subsequent user logins). Annotations names will be prefixed with `onyxia_`. `onyxia_last_login_timestamp` is also added. | {"enabled": true, "userAttributes": ["sub", "email"] } | | `singleNamespace` | true | When true, all users share the same namespace on the service provider. This configuration can be used if a project works on its own Onyxia region. | | | `userNamespace` | true | When true, all users have a namespace for their work. This configuration can be used if you don't allow a user to have their own space to work and only use project space | | | `namespacePrefix` | "user-" | User has a personal namespace like namespacePrefix + userId (should only be used when not singleNamespace but not the case) | | @@ -92,30 +88,12 @@ It can be used to add additional features to Onyxia. It helps Onyxia users to di ### Quotas properties -When this feature is enabled, Onyxia creates and maintains a [ResourceQuota](https://kubernetes.io/docs/concepts/policy/resource-quotas/) in the user / group namespace. -This feature can be enabled for either or both `user` and `group` namespaces and, for user namespaces, quotas can depend on the user roles. - -| Key | Default | Description | Example | -|----------------| ------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------ | -| `userEnabled` | false | Whether or not `ResourceQuotas` should be created for user namespaces. | true | -| `user` | | Quota to apply to user namespaces. | `{ "count/pods": "5" }` | -| `roles` | | Map of quotas corresponding to user roles. In case the user has multiple of those roles, only the first one will be applied. If user has no role from this list then `user` quota will be applied. | `{"admin": { "count/pods": "999" }}` | -| `groupEnabled` | false | Whether or not `ResourceQuotas` should be created for group namespaces. | true | -| `group` | | Quota to apply to group namespaces. | `{ "count/pods": "5" }` | - -A quota follows the Kubernetes model which is composed of: -* "requests.memory" -* "requests.cpu" -* "limits.memory" -* "limits.cpu" -* "requests.storage" -* "count/pods" -* "requests.ephemeral-storage" -* "limits.ephemeral-storage" -* "requests.nvidia.com/gpu" -* "limits.nvidia.com/gpu" - -Note : If you want Onyxia to create the ResourceQuota but not override it at each user login / action (e.g you override it yourself for some specific users), you can add the annotation `onyxia_ignore` to the ResourceQuota. If this annotation is set on a ResourceQuota then Onyxia won't touch it anymore. +ResourceQuotas are created by the onboarding API in each namespace. These properties only enable the `GET /my-lab/quota` endpoint, which displays the [ResourceQuota](https://kubernetes.io/docs/concepts/policy/resource-quotas/) named `onyxia-quota` of the user / group namespace. + +| Key | Default | Description | +|----------------| ------- |----------------------------------------------------------------| +| `userEnabled` | false | Whether or not quotas are displayed for user namespaces. | +| `groupEnabled` | false | Whether or not quotas are displayed for group namespaces. | ### Expose properties diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/properties/RegionsConfiguration.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/properties/RegionsConfiguration.java index f47dbda5..5e0b79d9 100644 --- a/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/properties/RegionsConfiguration.java +++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/properties/RegionsConfiguration.java @@ -83,20 +83,6 @@ public void load() throws Exception { "Using the {} service account of each namespace for region {}.", region.getServices().getTargetNamespaceServiceAccount(), region.getId()); - if (!region.getServices().isSingleNamespace() - && region.getServices().isAllowNamespaceCreation()) { - // Namespaces and their service account are created by the onboarding - // API, the API has no permission to do it in this mode - LOGGER.warn( - "Namespace creation is disabled for region {}: the onboarding API must be used with serviceAccountInTargetNamespace authentication.", - region.getId()); - region.getServices().setAllowNamespaceCreation(false); - } - } else if (!region.getServices().isSingleNamespace() - && region.getServices().isAllowNamespaceCreation()) { - LOGGER.warn( - "Namespace creation by Onyxia API (allowNamespaceCreation) is deprecated for region {} and will be removed, use the onboarding API instead.", - region.getId()); } }); } diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/api/onboarding/OnboardingController.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/api/onboarding/OnboardingController.java deleted file mode 100644 index 4b16caf1..00000000 --- a/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/api/onboarding/OnboardingController.java +++ /dev/null @@ -1,102 +0,0 @@ -package fr.insee.onyxia.api.controller.api.onboarding; - -import fr.insee.onyxia.api.controller.exception.NamespaceAlreadyExistException; -import fr.insee.onyxia.api.controller.exception.OnboardingDisabledException; -import fr.insee.onyxia.api.services.UserProvider; -import fr.insee.onyxia.api.services.impl.kubernetes.KubernetesService; -import fr.insee.onyxia.model.User; -import fr.insee.onyxia.model.region.Region; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.enums.ParameterIn; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Onboarding", description = "Onboarding related services") -@RequestMapping("/onboarding") -@RestController -@SecurityRequirement(name = "auth") -public class OnboardingController { - - private final KubernetesService kubernetesService; - - private final UserProvider userProvider; - - @Autowired - public OnboardingController(KubernetesService kubernetesService, UserProvider userProvider) { - this.kubernetesService = kubernetesService; - this.userProvider = userProvider; - } - - /** - * @deprecated namespaces should be created by the onboarding API. Disabled with - * serviceAccountInTargetNamespace authentication. - */ - @Deprecated - @Operation( - summary = "Init a namespace for a user or a group.", - description = - "Deprecated: use the onboarding API instead. create or replace the namespace of the user or the namespace of a group if the user is in the requested group and the according rbac policies. with the group prefix / user prefix of the region", - deprecated = true, - parameters = { - @Parameter( - name = "ONYXIA-REGION", - description = - "The region used by the user, if not provided default to the first region configured.", - in = ParameterIn.HEADER, - schema = @Schema(name = "ONYXIA-REGION", type = "string")) - }) - @PostMapping - public void onboard( - @Parameter(hidden = true) Region region, @RequestBody OnboardingRequest request) { - if (region.getServices().isSingleNamespace()) { - throw new NamespaceAlreadyExistException(); - } - if (!region.getServices().isAllowNamespaceCreation()) { - throw new OnboardingDisabledException(); - } - - checkPermissions(region, request); - final KubernetesService.Owner owner = new KubernetesService.Owner(); - final User user = userProvider.getUser(region); - if (request.getGroup() != null) { - owner.setId(request.getGroup()); - owner.setType(KubernetesService.Owner.OwnerType.GROUP); - } else { - owner.setId(user.getIdep()); - owner.setType(KubernetesService.Owner.OwnerType.USER); - } - kubernetesService.createOrUpdateNamespace(region, owner, user); - } - - private void checkPermissions(Region region, OnboardingRequest request) - throws AccessDeniedException { - if (request.getGroup() != null - && !userProvider.getUser(region).getGroups().contains(request.getGroup())) { - throw new AccessDeniedException("User does not belong to group " + request.getGroup()); - } - } - - @Schema( - description = - "Specification on which namespace to create. If group is provided, create a group namespace, otherwise create the user namespace.") - public static class OnboardingRequest { - - @Schema private String group; - - public String getGroup() { - return group; - } - - public void setGroup(String group) { - this.group = group; - } - } -} diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/exception/NamespaceAlreadyExistException.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/exception/NamespaceAlreadyExistException.java deleted file mode 100644 index e155dedb..00000000 --- a/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/exception/NamespaceAlreadyExistException.java +++ /dev/null @@ -1,11 +0,0 @@ -package fr.insee.onyxia.api.controller.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.CONFLICT) -public class NamespaceAlreadyExistException extends RuntimeException { - public NamespaceAlreadyExistException() { - super("Namespace already exists"); - } -} diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/exception/OnboardingDisabledException.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/exception/OnboardingDisabledException.java deleted file mode 100644 index ec4491bf..00000000 --- a/onyxia-api/src/main/java/fr/insee/onyxia/api/controller/exception/OnboardingDisabledException.java +++ /dev/null @@ -1,11 +0,0 @@ -package fr.insee.onyxia.api.controller.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.BAD_REQUEST) -public class OnboardingDisabledException extends RuntimeException { - public OnboardingDisabledException() { - super("Onboarding is disabled on this onyxia instance"); - } -} diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/events/InitNamespaceEvent.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/events/InitNamespaceEvent.java deleted file mode 100644 index 01b0b02f..00000000 --- a/onyxia-api/src/main/java/fr/insee/onyxia/api/events/InitNamespaceEvent.java +++ /dev/null @@ -1,46 +0,0 @@ -package fr.insee.onyxia.api.events; - -public class InitNamespaceEvent extends OnyxiaEvent { - - public static final String TYPE = "onboarding.createNamespace"; - String region; - String namespace; - String username; - - public InitNamespaceEvent() {} - - public InitNamespaceEvent(String region, String namespace, String username) { - this.region = region; - this.namespace = namespace; - this.username = username; - } - - public String getRegion() { - return region; - } - - public void setRegion(String region) { - this.region = region; - } - - public String getNamespace() { - return namespace; - } - - public void setNamespace(String namespace) { - this.namespace = namespace; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - @Override - public String getType() { - return TYPE; - } -} 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..2a1e9f9a 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 @@ -112,8 +112,7 @@ public Collection installApp( File values = File.createTempFile("values", ".yaml"); mapperHelm.writeValue(values, fusion); - String namespaceId = - kubernetesService.determineNamespaceAndCreateIfNeeded(region, project, user); + String namespaceId = kubernetesService.determineNamespace(region, project, user); try { HelmInstaller res = getHelmInstallService() @@ -205,8 +204,7 @@ public CompletableFuture getUserServices( @Override public String getLogs( Region region, Project project, User user, String serviceId, String taskId) { - String namespace = - kubernetesService.determineNamespaceAndCreateIfNeeded(region, project, user); + String namespace = kubernetesService.determineNamespace(region, project, user); KubernetesClient client = kubernetesClientProvider.getUserClient(region, user, namespace); return client.pods().inNamespace(namespace).withName(taskId).getLog(); } @@ -217,8 +215,7 @@ public Watch getEvents( Project project, User user, Watcher watcher) { - String namespace = - kubernetesService.determineNamespaceAndCreateIfNeeded(region, project, user); + String namespace = kubernetesService.determineNamespace(region, project, user); KubernetesClient client = kubernetesClientProvider.getUserClient(region, user, namespace); return client.v1().events().inNamespace(namespace).watch(watcher); } @@ -229,8 +226,7 @@ public Service getUserService(Region region, Project project, User user, String if (serviceId.startsWith("/")) { serviceId = serviceId.substring(1); } - String namespace = - kubernetesService.determineNamespaceAndCreateIfNeeded(region, project, user); + String namespace = kubernetesService.determineNamespace(region, project, user); HelmLs result = getHelmInstallService() .getAppById( @@ -244,8 +240,7 @@ public Service getUserService(Region region, Project project, User user, String public UninstallService destroyService( Region region, Project project, User user, final String path, boolean bulk) throws Exception { - final String namespace = - kubernetesService.determineNamespaceAndCreateIfNeeded(region, project, user); + final String namespace = kubernetesService.determineNamespace(region, project, user); UninstallService result = new UninstallService(); result.setPath(path); int status = 0; @@ -375,8 +370,7 @@ public void share(Region region, Project project, User user, String serviceId, b private void patchOnyxiaSecret( Region region, Project project, User user, String serviceId, Map data) { - String namespaceId = - kubernetesService.determineNamespaceAndCreateIfNeeded(region, project, user); + String namespaceId = kubernetesService.determineNamespace(region, project, user); KubernetesClient client = kubernetesClientProvider.getUserClient(region, user, namespaceId); Secret secret = client.secrets() @@ -476,8 +470,7 @@ public void suspendOrResume( boolean dryRun, boolean suspend) throws IOException, InterruptedException, TimeoutException { - String namespaceId = - kubernetesService.determineNamespaceAndCreateIfNeeded(region, project, user); + String namespaceId = kubernetesService.determineNamespace(region, project, user); if (suspend) { getHelmInstallService() .suspend( diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/services/impl/kubernetes/KubernetesService.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/services/impl/kubernetes/KubernetesService.java index 8a6366fd..73d458fe 100644 --- a/onyxia-api/src/main/java/fr/insee/onyxia/api/services/impl/kubernetes/KubernetesService.java +++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/services/impl/kubernetes/KubernetesService.java @@ -2,26 +2,16 @@ import fr.insee.onyxia.api.configuration.kubernetes.KubernetesClientProvider; import fr.insee.onyxia.api.controller.exception.NamespaceNotFoundException; -import fr.insee.onyxia.api.events.InitNamespaceEvent; -import fr.insee.onyxia.api.events.OnyxiaEventPublisher; import fr.insee.onyxia.api.services.impl.HelmAppsService; import fr.insee.onyxia.model.User; import fr.insee.onyxia.model.project.Project; import fr.insee.onyxia.model.region.Region; -import fr.insee.onyxia.model.service.quota.Quota; import io.fabric8.kubernetes.api.model.*; -import io.fabric8.kubernetes.api.model.rbac.RoleBinding; -import io.fabric8.kubernetes.api.model.rbac.RoleBindingBuilder; -import io.fabric8.kubernetes.api.model.rbac.SubjectBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; -import io.fabric8.kubernetes.client.dsl.Resource; -import java.util.HashMap; import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -30,25 +20,19 @@ public class KubernetesService { private final KubernetesClientProvider kubernetesClientProvider; - final OnyxiaEventPublisher onyxiaEventPublisher; - private static final Logger LOGGER = LoggerFactory.getLogger(KubernetesService.class); public static final String ONYXIA_QUOTA = "onyxia-quota"; @Autowired - public KubernetesService( - KubernetesClientProvider kubernetesClientProvider, - OnyxiaEventPublisher onyxiaEventPublisher) { + public KubernetesService(KubernetesClientProvider kubernetesClientProvider) { this.kubernetesClientProvider = kubernetesClientProvider; - this.onyxiaEventPublisher = onyxiaEventPublisher; - } - - public String createOrUpdateNamespace(Region region, Owner owner, User user) { - final String namespaceId = getDefaultNamespace(region, owner); - return createNamespace(region, namespaceId, owner, user); } + /** + * Namespaces are created by the onboarding API: a project namespace that does not exist (or has + * not been onboarded yet) is reported as not found. + */ @NotNull - public String determineNamespaceAndCreateIfNeeded(Region region, Project project, User user) { + public String determineNamespace(Region region, Project project, User user) { if (region.getServices().isSingleNamespace()) { return getCurrentNamespace(region); } @@ -56,19 +40,7 @@ public String determineNamespaceAndCreateIfNeeded(Region region, Project project throw new NamespaceNotFoundException(); } if (!isNamespaceAlreadyExisting(region, user, project.getNamespace())) { - if (!region.getServices().isAllowNamespaceCreation()) { - throw new NamespaceNotFoundException(); - } else { - final KubernetesService.Owner owner = new KubernetesService.Owner(); - if (project.getGroup() != null) { - owner.setId(project.getGroup()); - owner.setType(Owner.OwnerType.GROUP); - } else { - owner.setId(user.getIdep()); - owner.setType(KubernetesService.Owner.OwnerType.USER); - } - createNamespace(region, project.getNamespace(), owner, user); - } + throw new NamespaceNotFoundException(); } return project.getNamespace(); } @@ -77,97 +49,6 @@ public String getCurrentNamespace(Region region) { return kubernetesClientProvider.getRootClient(region).getNamespace(); } - private String createNamespace(Region region, String namespaceId, Owner owner, User user) { - final String name = getNameFromOwner(region, owner); - - final KubernetesClient kubClient = kubernetesClientProvider.getRootClient(region); - - Map userMetadata = new HashMap<>(); - Region.Services.NamespaceAnnotationsDynamic namespaceAnnotationsDynamic = - region.getServices().getNamespaceAnnotationsDynamic(); - if (namespaceAnnotationsDynamic.isEnabled() && user != null) { - userMetadata.put( - "onyxia_last_login_timestamp", String.valueOf(System.currentTimeMillis())); - for (String claim : namespaceAnnotationsDynamic.getUserAttributes()) { - String claimValue = String.valueOf(user.getAttributes().getOrDefault(claim, "")); - userMetadata.put("onyxia_" + claim, claimValue); - } - } - - Resource namespace = - kubClient - .namespaces() - .resource( - new NamespaceBuilder() - .withNewMetadata() - .withName(namespaceId) - .withLabels(region.getServices().getNamespaceLabels()) - .addToLabels("onyxia_owner", owner.getId()) - .withAnnotations( - region.getServices().getNamespaceAnnotations()) - .addToAnnotations(userMetadata) - .endMetadata() - .build()); - boolean newNamespace = namespace.get() == null; - namespace.serverSideApply(); - - final RoleBinding bindingToCreate = - kubClient - .rbac() - .roleBindings() - .inNamespace(namespaceId) - .createOrReplace( - new RoleBindingBuilder() - .withNewMetadata() - .withLabels(Map.of("createdby", "onyxia")) - .withName("full_control_namespace") - .withNamespace(namespaceId) - .endMetadata() - .withSubjects( - new SubjectBuilder() - .withKind(getSubjectKind(owner)) - .withName(name) - .withApiGroup("rbac.authorization.k8s.io") - .withNamespace(namespaceId) - .build()) - .withNewRoleRef() - .withApiGroup("rbac.authorization.k8s.io") - .withKind("ClusterRole") - .withName("admin") - .endRoleRef() - .build()); - - final boolean userEnabled = - owner.getType() == Owner.OwnerType.USER - && region.getServices().getQuotas().isUserEnabled(); - final boolean groupEnabled = - owner.getType() == Owner.OwnerType.GROUP - && region.getServices().getQuotas().isGroupEnabled(); - - if (userEnabled) { - Quota quota = region.getServices().getQuotas().getUserQuota(); - for (String role : user.getRoles()) { - if (region.getServices().getQuotas().getRolesQuota().containsKey(role)) { - quota = region.getServices().getQuotas().getRolesQuota().get(role); - break; // take first role match - } - } - LOGGER.info("applying user enabled style quota"); - applyQuotas(namespaceId, kubClient, quota, true); - } else if (groupEnabled) { - final Quota quota = region.getServices().getQuotas().getGroupQuota(); - LOGGER.info("applying group enabled style quota"); - applyQuotas(namespaceId, kubClient, quota, true); - } - if (newNamespace) { - InitNamespaceEvent initNamespaceEvent = - new InitNamespaceEvent(region.getName(), namespaceId, owner.getId()); - onyxiaEventPublisher.publishEvent(initNamespaceEvent); - } - - return namespaceId; - } - private boolean isNamespaceAlreadyExisting(Region region, User user, String namespaceId) { if (isTargetNamespaceServiceAccountMode(region)) { // The namespace service account can read its own namespace. If the namespace does not @@ -210,57 +91,6 @@ private KubernetesClient getNamespaceClient(Region region, User user, String nam return kubernetesClientProvider.getRootClient(region); } - private void applyQuotas( - String namespaceId, - KubernetesClient kubClient, - Quota inputQuota, - boolean overrideExisting) { - final ResourceQuotaBuilder resourceQuotaBuilder = new ResourceQuotaBuilder(); - resourceQuotaBuilder - .withNewMetadata() - .withLabels(Map.of("createdby", "onyxia")) - .withName(ONYXIA_QUOTA) - .withNamespace(namespaceId) - .endMetadata(); - - final Map quotasToApply = inputQuota.asMap(); - - if (quotasToApply.entrySet().stream().filter(e -> e.getValue() != null).count() == 0) { - return; - } - - var resourceQuotaBuilderSpecNested = resourceQuotaBuilder.withNewSpec(); - quotasToApply.entrySet().stream() - .filter(e -> e.getValue() != null) - .forEach( - e -> - resourceQuotaBuilderSpecNested.addToHard( - e.getKey(), Quantity.parse(e.getValue()))); - resourceQuotaBuilderSpecNested.endSpec(); - - final ResourceQuota quota = resourceQuotaBuilder.build(); - ResourceQuota resourceQuota = - kubClient.resourceQuotas().inNamespace(namespaceId).withName(ONYXIA_QUOTA).get(); - if (resourceQuota != null - && resourceQuota.getMetadata().getAnnotations().containsKey("onyxia_ignore")) { - // The annotation onyxia_ignore can be set to prevent Onyxia from managing this - // resourcequota - return; - } - if (overrideExisting) { - kubClient.resourceQuotas().inNamespace(namespaceId).createOrReplace(quota); - } else { - try { - kubClient.resourceQuotas().inNamespace(namespaceId).create(quota); - } catch (final KubernetesClientException e) { - if (e.getCode() != 409) { - // This is not a "quota already in place" error - throw e; - } - } - } - } - public void createOnyxiaSecret( Region region, User user, @@ -302,47 +132,12 @@ public void createOnyxiaSecret( newSecret = kubClient.secrets().inNamespace(namespaceId).resource(newSecret).create(); } - private String getNameFromOwner(Region region, Owner owner) { - String username = owner.getId(); - if (owner.getType() == Owner.OwnerType.USER - && region.getServices().getUsernamePrefix() != null) { - username = region.getServices().getUsernamePrefix() + username; - } else if (owner.getType() == Owner.OwnerType.GROUP - && region.getServices().getGroupPrefix() != null) { - username = region.getServices().getGroupPrefix() + username; - } - return username; - } - - private String getDefaultNamespace(Region region, Owner owner) { - if (owner.getType() == Owner.OwnerType.USER) { - return region.getServices().getNamespacePrefix() + owner.getId(); - } else { - return region.getServices().getGroupNamespacePrefix() + owner.getId(); - } - } - - public void applyQuota(Region region, Project project, User user, Quota quota) { - final String namespace = determineNamespaceAndCreateIfNeeded(region, project, user); - final KubernetesClient kubClient = getNamespaceClient(region, user, namespace); - applyQuotas(namespace, kubClient, quota, true); - } - public ResourceQuota getOnyxiaQuota(Region region, Project project, User user) { - final String namespace = determineNamespaceAndCreateIfNeeded(region, project, user); + final String namespace = determineNamespace(region, project, user); final KubernetesClient kubClient = getNamespaceClient(region, user, namespace); return kubClient.resourceQuotas().inNamespace(namespace).withName(ONYXIA_QUOTA).get(); } - private String getSubjectKind(Owner owner) { - if (owner.getType() == Owner.OwnerType.GROUP) { - return "Group"; - } else if (owner.getType() == Owner.OwnerType.USER) { - return "User"; - } - throw new IllegalArgumentException("Owner type must be one of : USER, GROUP"); - } - public static class Owner { private String id; private OwnerType type; diff --git a/onyxia-api/src/main/resources/regions.json b/onyxia-api/src/main/resources/regions.json index 89107074..c5b7b57c 100644 --- a/onyxia-api/src/main/resources/regions.json +++ b/onyxia-api/src/main/resources/regions.json @@ -14,38 +14,9 @@ "usernamePrefix": "oidc-", "groupNamespacePrefix": "projet-", "authenticationMode": "serviceAccount", - "namespaceAnnotationsDynamic": { - "enabled": true, - "userAttributes": ["email","locale","groups"] - }, "quotas": { - "enabled": false, - "default": { - "requests.memory": "10Gi", - "requests.cpu": "10", - "limits.memory": "10Gi", - "limits.cpu": "10", - "requests.storage": "100Gi", - "count/pods": "50" - }, "userEnabled": false, - "user": { - "requests.memory": "11Gi", - "requests.cpu": "11", - "limits.memory": "11Gi", - "limits.cpu": "11", - "requests.storage": "101Gi", - "count/pods": "51" - }, - "groupEnabled": false, - "group": { - "requests.memory": "12Gi", - "requests.cpu": "12", - "limits.memory": "12Gi", - "limits.cpu": "12", - "requests.storage": "102Gi", - "count/pods": "52" - } + "groupEnabled": false }, "expose": { "domain": "fakedomain.kub.example.com", diff --git a/onyxia-api/src/test/java/fr/insee/onyxia/api/controller/api/onboarding/OnboardingControllerTest.java b/onyxia-api/src/test/java/fr/insee/onyxia/api/controller/api/onboarding/OnboardingControllerTest.java deleted file mode 100644 index 4302b347..00000000 --- a/onyxia-api/src/test/java/fr/insee/onyxia/api/controller/api/onboarding/OnboardingControllerTest.java +++ /dev/null @@ -1,114 +0,0 @@ -package fr.insee.onyxia.api.controller.api.onboarding; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.fasterxml.jackson.databind.ObjectMapper; -import fr.insee.onyxia.api.configuration.BaseTest; -import fr.insee.onyxia.api.configuration.SecurityConfig; -import fr.insee.onyxia.api.configuration.properties.RegionsConfiguration; -import fr.insee.onyxia.api.controller.exception.NamespaceAlreadyExistException; -import fr.insee.onyxia.api.services.UserProvider; -import fr.insee.onyxia.api.services.impl.kubernetes.KubernetesService; -import fr.insee.onyxia.api.services.utils.HttpRequestUtils; -import fr.insee.onyxia.api.user.OnyxiaUserProvider; -import fr.insee.onyxia.model.User; -import fr.insee.onyxia.model.region.Region; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.web.servlet.MockMvc; - -@WebMvcTest(OnboardingController.class) -class OnboardingControllerTest extends BaseTest { - - @Autowired private MockMvc mockMvc; - @Autowired private ObjectMapper mapper; - - @MockBean private UserProvider userProvider; - @MockBean private RegionsConfiguration regionsConfiguration; - - @MockBean private HttpRequestUtils httpRequestUtils; - @MockBean private SecurityConfig securityConfig; - @MockBean private OnyxiaUserProvider onyxiaUserProvider; - @MockBean private KubernetesService kubernetesService; - - @Test - public void should_not_create_namespace_when_single_project() throws Exception { - Region region = new Region(); - Region.Services servicesConfiguration = new Region.Services(); - servicesConfiguration.setSingleNamespace(true); - region.setServices(servicesConfiguration); - when(regionsConfiguration.getDefaultRegion()).thenReturn(region); - mockMvc.perform(post("/onboarding").content("{}").contentType(APPLICATION_JSON)) - .andExpect(status().isConflict()); - } - - @Test - public void should_not_create_namespace_when_allow_namespace_creation_is_false() - throws Exception { - Region region = new Region(); - Region.Services servicesConfiguration = new Region.Services(); - servicesConfiguration.setSingleNamespace(false); - servicesConfiguration.setAllowNamespaceCreation(false); - region.setServices(servicesConfiguration); - when(regionsConfiguration.getDefaultRegion()).thenReturn(region); - when(userProvider.getUser(any())).thenReturn(User.newInstance().setIdep("default").build()); - - mockMvc.perform(post("/onboarding").content("{}").contentType(APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - } - - @Test - public void should_not_create_namespace_when_user_is_not_member_of_group() throws Exception { - Region region = new Region(); - Region.Services servicesConfiguration = new Region.Services(); - servicesConfiguration.setSingleNamespace(false); - servicesConfiguration.setAllowNamespaceCreation(true); - region.setServices(servicesConfiguration); - - when(regionsConfiguration.getDefaultRegion()).thenReturn(region); - when(userProvider.getUser(any())).thenReturn(User.newInstance().setIdep("default").build()); - - var onboardingRequest = new OnboardingController.OnboardingRequest(); - onboardingRequest.setGroup("some-group"); - mockMvc.perform( - post("/onboarding") - .content(mapper.writeValueAsString(onboardingRequest)) - .contentType(APPLICATION_JSON)) - .andExpect(status().isForbidden()); - } - - @Test - public void should_not_create_namespace_when_already_exist() throws Exception { - Region region = new Region(); - Region.Services servicesConfiguration = new Region.Services(); - servicesConfiguration.setSingleNamespace(false); - servicesConfiguration.setAllowNamespaceCreation(true); - region.setServices(servicesConfiguration); - when(regionsConfiguration.getDefaultRegion()).thenReturn(region); - when(userProvider.getUser(any())).thenReturn(User.newInstance().setIdep("default").build()); - when(kubernetesService.createOrUpdateNamespace(any(), any(), any())) - .thenThrow(new NamespaceAlreadyExistException()); - - mockMvc.perform(post("/onboarding").content("{}").contentType(APPLICATION_JSON)) - .andExpect(status().isConflict()); - } - - @Test - public void should_create_namespace() throws Exception { - Region region = new Region(); - Region.Services servicesConfiguration = new Region.Services(); - servicesConfiguration.setSingleNamespace(false); - servicesConfiguration.setAllowNamespaceCreation(true); - region.setServices(servicesConfiguration); - when(regionsConfiguration.getDefaultRegion()).thenReturn(region); - when(userProvider.getUser(any())).thenReturn(User.newInstance().setIdep("default").build()); - mockMvc.perform(post("/onboarding").content("{}").contentType(APPLICATION_JSON)) - .andExpect(status().isOk()); - } -} 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..e7dcad3e 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 @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import fr.insee.onyxia.model.service.quota.Quota; import io.swagger.v3.oas.annotations.media.Schema; import java.util.ArrayList; import java.util.HashMap; @@ -174,9 +173,6 @@ public void setOnyxiaAPI(OnyxiaAPI onyxiaAPI) { public static class Services { private boolean singleNamespace = true; - private boolean allowNamespaceCreation = true; - private Map namespaceLabels = new HashMap<>(); - private Map namespaceAnnotations = new HashMap<>(); private boolean userNamespace = true; private String namespacePrefix = "user-"; private String groupNamespacePrefix = "projet-"; @@ -199,9 +195,6 @@ public static class Services { private K8sPublicEndpoint k8sPublicEndpoint = new K8sPublicEndpoint(); - private NamespaceAnnotationsDynamic namespaceAnnotationsDynamic = - new NamespaceAnnotationsDynamic(); - public boolean isSingleNamespace() { return singleNamespace; } @@ -218,39 +211,6 @@ public Object getDefaultConfiguration() { return defaultConfiguration; } - public boolean isAllowNamespaceCreation() { - return allowNamespaceCreation; - } - - public void setAllowNamespaceCreation(boolean allowNamespaceCreation) { - this.allowNamespaceCreation = allowNamespaceCreation; - } - - public NamespaceAnnotationsDynamic getNamespaceAnnotationsDynamic() { - return namespaceAnnotationsDynamic; - } - - public void setNamespaceAnnotationsDynamic( - NamespaceAnnotationsDynamic namespaceAnnotationsDynamic) { - this.namespaceAnnotationsDynamic = namespaceAnnotationsDynamic; - } - - public Map getNamespaceLabels() { - return namespaceLabels; - } - - public void getNamespaceLabels(Map namespaceLabels) { - this.namespaceLabels = namespaceLabels; - } - - public Map getNamespaceAnnotations() { - return namespaceAnnotations; - } - - public void getNamespaceAnnotations(Map namespaceAnnotations) { - this.namespaceAnnotations = namespaceAnnotations; - } - public boolean isUserNamespace() { return userNamespace; } @@ -367,42 +327,11 @@ public static enum AuthenticationMode { TOKEN_PASSTHROUGH } - public static class NamespaceAnnotationsDynamic { - private boolean enabled = true; - - private List userAttributes = new ArrayList<>(); - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public List getUserAttributes() { - return userAttributes; - } - - public void setUserAttributes(List userAttributes) { - this.userAttributes = userAttributes; - } - } - public static class Quotas { private boolean userEnabled = false; private boolean groupEnabled = false; - @JsonProperty("user") - private Quota userQuota; - - @JsonProperty("group") - private Quota groupQuota; - - @JsonProperty("roles") - private Map rolesQuota = new HashMap<>(); - public boolean isUserEnabled() { return userEnabled; } @@ -418,30 +347,6 @@ public boolean isGroupEnabled() { public void setGroupEnabled(boolean groupEnabled) { this.groupEnabled = groupEnabled; } - - public Quota getUserQuota() { - return userQuota; - } - - public void setUserQuota(Quota userQuota) { - this.userQuota = userQuota; - } - - public Quota getGroupQuota() { - return groupQuota; - } - - public void setGroupQuota(Quota groupQuota) { - this.groupQuota = groupQuota; - } - - public void setRolesQuota(Map rolesQuota) { - this.rolesQuota = rolesQuota; - } - - public Map getRolesQuota() { - return rolesQuota; - } } }