Skip to content

Commit

Permalink
add user and group quotas possible in region
Browse files Browse the repository at this point in the history
  • Loading branch information
alexisdondon authored and fcomte committed Jun 12, 2023
1 parent 8c383d6 commit c46fb9a
Show file tree
Hide file tree
Showing 7 changed files with 1,100 additions and 940 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ node_modules
.idea
*.iml
.vscode
onyxia-api/src/main/resources/application.properties.local
onyxia-api/src/main/resources/regions.json.local
10 changes: 7 additions & 3 deletions docs/region-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,13 @@ When this feature is enabled, namespaces are created with **quotas**.

| Key | Default | Description |
| --------------------- | ------- | ------------------------------------------------------------------ |
| `enabled` | false | Whether or not users are subject to a resource limitation. Quotas can only be applied to users and not to groups. |
| `allowUserModification` | true | Whether or not the user can manually disable or change its own limitation. |
| `default` | | The quota is applied on the namespace at creation, before user modification or reset. New configuration will not be applied to existing namespaces. |
| `enabled` | false | Whether or not users are subject to a resource limitation. Quotas can only be applied to users and not to groups. (will be deprecated see userEnabled and groupEnabled) |
| `allowUserModification` | true | Whether or not the user can manually disable or change its own limitation or group limitation. |
| `default` | | This quota is applied on the namespace at creation, before user modification or reset. New configuration will not be applied to existing namespaces. (will be deprecated see userEnabled and groupEnabled) |
| `userEnabled` | false | Whether or not users are subject to a resource limitation. Enable this on user namespace only with user quota content based on kubernetes model . |
| `user` | false | This quota is applied on the user namespace at creation, before user modification or reset. New configuration will not be applied to already existing namespaces. |
| `groupEnabled` | false |Whether or not users are subject to a resource limitation. Enable this on group/project namespace only ith group quota content. |
| `group` | false | This quota is applied on the group namespace at creation, before user modification or reset. New configuration will not be applied to already existing namespaces. |

A quota follows the Kubernetes model which is composed of:
"requests.memory"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import fr.insee.onyxia.api.services.UserProvider;
import fr.insee.onyxia.api.services.impl.kubernetes.KubernetesService;
import fr.insee.onyxia.api.services.impl.kubernetes.KubernetesService.Owner;
import fr.insee.onyxia.model.project.Project;
import fr.insee.onyxia.model.region.Region;
import fr.insee.onyxia.model.service.quota.Quota;
Expand All @@ -16,16 +17,24 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
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 = "My lab", description = "My services")
@RequestMapping("/my-lab/quota")
@RestController
@SecurityRequirement(name = "auth")
public class QuotaController {

private final Logger logger = LoggerFactory.getLogger(QuotaController.class);

@Autowired private KubernetesService kubernetesService;

@Autowired private UserProvider userProvider;
Expand All @@ -50,16 +59,24 @@ public class QuotaController {
@GetMapping
public QuotaUsage getQuota(
@Parameter(hidden = true) Region region, @Parameter(hidden = true) Project project) {
checkQuotaIsEnabled(region);
ResourceQuota resourceQuota =
kubernetesService.getOnyxiaQuota(region, project, userProvider.getUser(region));
final Owner owner = getOwner(region, project);
ResourceQuota resourceQuota = null;
if (owner.getType() == Owner.OwnerType.USER) {
checkUserQuotaIsEnabled(region);
resourceQuota =
kubernetesService.getOnyxiaQuota(region, project, userProvider.getUser(region));
} else if (owner.getType() == Owner.OwnerType.GROUP) {
checkGroupQuotaIsEnabled(region);
resourceQuota =
kubernetesService.getOnyxiaQuota(region, project, userProvider.getUser(region));
}
if (resourceQuota == null) {
return null;
}

QuotaUsage quotaUsage = new QuotaUsage();
Quota spec = new Quota();
Quota usage = new Quota();
final QuotaUsage quotaUsage = new QuotaUsage();
final Quota spec = new Quota();
final Quota usage = new Quota();
mapKubQuotaToQuota(resourceQuota.getStatus().getHard(), spec);
mapKubQuotaToQuota(resourceQuota.getStatus().getUsed(), usage);
quotaUsage.setSpec(spec);
Expand Down Expand Up @@ -91,8 +108,13 @@ public void applyQuota(
@Parameter(hidden = true) Project project,
@RequestBody Quota quota)
throws IllegalAccessException {
checkQuotaIsEnabled(region);
checkQuotaModificationIsAllowed(region);
final Owner owner = getOwner(region, project);
if (owner.getType() == Owner.OwnerType.USER) {
checkUserQuotaIsEnabled(region);
} else if (owner.getType() == Owner.OwnerType.GROUP) {
checkGroupQuotaIsEnabled(region);
}
kubernetesService.applyQuota(region, project, userProvider.getUser(region), quota);
}

Expand All @@ -116,22 +138,47 @@ public void applyQuota(
@PostMapping("/reset")
public void resetQuota(
@Parameter(hidden = true) Region region, @Parameter(hidden = true) Project project) {
checkQuotaIsEnabled(region);
checkQuotaModificationIsAllowed(region);
if (!region.getServices().getQuotas().isAllowUserModification()) {
throw new AccessDeniedException(
"User modification is not allowed on this installation");
final Owner owner = getOwner(region, project);
if (owner.getType() == Owner.OwnerType.USER) {
checkUserQuotaIsEnabled(region);
if (region.getServices().getQuotas().isEnabled()) {
logger.warn(
"resetting to old enabled style quota, this parameter will be deprecated");
kubernetesService.applyQuota(
region,
project,
userProvider.getUser(region),
region.getServices().getQuotas().getDefaultQuota());
} else {
logger.info("resetting to user enabled style quota");
kubernetesService.applyQuota(
region,
project,
userProvider.getUser(region),
region.getServices().getQuotas().getUserQuota());
}
} else if (owner.getType() == Owner.OwnerType.GROUP) {
logger.info("resetting to group enabled style quota");
checkGroupQuotaIsEnabled(region);
kubernetesService.applyQuota(
region,
project,
userProvider.getUser(region),
region.getServices().getQuotas().getGroupQuota());
}
kubernetesService.applyQuota(
region,
project,
userProvider.getUser(region),
region.getServices().getQuotas().getDefaultQuota());
}

private void checkQuotaIsEnabled(Region region) {
if (!region.getServices().getQuotas().isEnabled()) {
throw new AccessDeniedException("Quotas are not active on this installation");
private void checkUserQuotaIsEnabled(Region region) {
if (!region.getServices().getQuotas().isEnabled()
&& !region.getServices().getQuotas().isUserEnabled()) {
throw new AccessDeniedException("User Quotas are not active on this installation");
}
}

private void checkGroupQuotaIsEnabled(Region region) {
if (!region.getServices().getQuotas().isGroupEnabled()) {
throw new AccessDeniedException("Group Quotas are not active on this installation");
}
}

Expand All @@ -142,8 +189,20 @@ private void checkQuotaModificationIsAllowed(Region region) {
}
}

private Owner getOwner(Region region, Project project) {
final KubernetesService.Owner owner = new KubernetesService.Owner();
if (project.getGroup() != null) {
owner.setId(project.getGroup());
owner.setType(Owner.OwnerType.GROUP);
} else {
owner.setId(userProvider.getUser(region).getIdep());
owner.setType(KubernetesService.Owner.OwnerType.USER);
}
return owner;
}

private void mapKubQuotaToQuota(Map<String, Quantity> resourceQuota, Quota quota) {
Map<String, String> rawData = new HashMap<>();
final Map<String, String> rawData = new HashMap<>();
resourceQuota.entrySet().forEach(e -> rawData.put(e.getKey(), e.getValue().toString()));
quota.loadFromMap(rawData);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public void onboard(
throw new OnboardingDisabledException();
}
checkPermissions(region, request);
KubernetesService.Owner owner = new KubernetesService.Owner();
final KubernetesService.Owner owner = new KubernetesService.Owner();
if (request.getGroup() != null) {
owner.setId(request.getGroup());
owner.setType(KubernetesService.Owner.OwnerType.GROUP);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
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.NamespaceBuilder;
import io.fabric8.kubernetes.api.model.Quantity;
import io.fabric8.kubernetes.api.model.ResourceQuota;
import io.fabric8.kubernetes.api.model.ResourceQuotaBuilder;
import io.fabric8.kubernetes.api.model.ResourceQuotaFluent;
import io.fabric8.kubernetes.api.model.rbac.RoleBinding;
import io.fabric8.kubernetes.api.model.rbac.RoleBindingBuilder;
import io.fabric8.kubernetes.api.model.rbac.SubjectBuilder;
Expand All @@ -16,6 +20,8 @@
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;

Expand All @@ -24,8 +30,10 @@ public class KubernetesService {

@Autowired private KubernetesClientProvider kubernetesClientProvider;

private final Logger logger = LoggerFactory.getLogger(KubernetesService.class);

public String createDefaultNamespace(Region region, Owner owner) {
String namespaceId = getDefaultNamespace(region, owner);
final String namespaceId = getDefaultNamespace(region, owner);
if (isNamespaceAlreadyExisting(region, namespaceId)) {
throw new NamespaceAlreadyExistException();
}
Expand All @@ -44,7 +52,7 @@ public String determineNamespaceAndCreateIfNeeded(Region region, Project project
if (!region.getServices().isAllowNamespaceCreation()) {
throw new NamespaceNotFoundException();
} else {
KubernetesService.Owner owner = new KubernetesService.Owner();
final KubernetesService.Owner owner = new KubernetesService.Owner();
if (project.getGroup() != null) {
owner.setId(project.getGroup());
owner.setType(Owner.OwnerType.GROUP);
Expand All @@ -63,9 +71,9 @@ public String getCurrentNamespace(Region region) {
}

private String createNamespace(Region region, String namespaceId, Owner owner) {
String name = getNameFromOwner(region, owner);
final String name = getNameFromOwner(region, owner);

KubernetesClient kubClient = kubernetesClientProvider.getRootClient(region);
final KubernetesClient kubClient = kubernetesClientProvider.getRootClient(region);

kubClient
.namespaces()
Expand All @@ -80,7 +88,7 @@ private String createNamespace(Region region, String namespaceId, Owner owner) {
.build())
.create();

RoleBinding bindingToCreate =
final RoleBinding bindingToCreate =
kubClient
.rbac()
.roleBindings()
Expand All @@ -106,14 +114,40 @@ private String createNamespace(Region region, String namespaceId, Owner owner) {
.endRoleRef()
.build());

// Currently, no quotas for groups
if (owner.getType() == Owner.OwnerType.USER
&& region.getServices().getQuotas().isEnabled()) {
Quota defaultQuota = region.getServices().getQuotas().getDefaultQuota();
// could be deleted if enabled and default quota is deprecated
final boolean oldEnabled =
owner.getType() == Owner.OwnerType.USER
&& region.getServices().getQuotas().isEnabled();
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 (oldEnabled) {
final Quota quota = region.getServices().getQuotas().getDefaultQuota();
logger.warn("applying old enabled style quota, this parameter will be deprecated");
applyQuotas(
namespaceId,
kubClient,
quota,
!region.getServices().getQuotas().isAllowUserModification());
} else if (userEnabled) {
final Quota quota = region.getServices().getQuotas().getUserQuota();
logger.info("applying user enabled style quota");
applyQuotas(
namespaceId,
kubClient,
quota,
!region.getServices().getQuotas().isAllowUserModification());
} else if (groupEnabled) {
final Quota quota = region.getServices().getQuotas().getGroupQuota();
logger.info("applying user enabled style quota");
applyQuotas(
namespaceId,
kubClient,
defaultQuota,
quota,
!region.getServices().getQuotas().isAllowUserModification());
}

Expand All @@ -135,21 +169,21 @@ private void applyQuotas(
KubernetesClient kubClient,
Quota inputQuota,
boolean overrideExisting) {
ResourceQuotaBuilder resourceQuotaBuilder = new ResourceQuotaBuilder();
final ResourceQuotaBuilder resourceQuotaBuilder = new ResourceQuotaBuilder();
resourceQuotaBuilder
.withNewMetadata()
.withLabels(Map.of("createdby", "onyxia"))
.withName("onyxia-quota")
.withNamespace(namespaceId)
.endMetadata();

Map<String, String> quotasToApply = inputQuota.asMap();
final Map<String, String> quotasToApply = inputQuota.asMap();

if (quotasToApply.entrySet().stream().filter(e -> e.getValue() != null).count() == 0) {
return;
}

ResourceQuotaFluent.SpecNested<ResourceQuotaBuilder> resourceQuotaBuilderSpecNested =
final ResourceQuotaFluent.SpecNested<ResourceQuotaBuilder> resourceQuotaBuilderSpecNested =
resourceQuotaBuilder.withNewSpec();
quotasToApply.entrySet().stream()
.filter(e -> e.getValue() != null)
Expand All @@ -159,13 +193,13 @@ private void applyQuotas(
e.getKey(), Quantity.parse(e.getValue())));
resourceQuotaBuilderSpecNested.endSpec();

ResourceQuota quota = resourceQuotaBuilder.build();
final ResourceQuota quota = resourceQuotaBuilder.build();
if (overrideExisting) {
kubClient.resourceQuotas().inNamespace(namespaceId).createOrReplace(quota);
} else {
try {
kubClient.resourceQuotas().inNamespace(namespaceId).create(quota);
} catch (KubernetesClientException e) {
} catch (final KubernetesClientException e) {
if (e.getCode() != 409) {
// This is not a "quota already in place" error
throw e;
Expand Down Expand Up @@ -195,14 +229,14 @@ private String getDefaultNamespace(Region region, Owner owner) {
}

public void applyQuota(Region region, Project project, User user, Quota quota) {
KubernetesClient kubClient = kubernetesClientProvider.getRootClient(region);
String namespace = determineNamespaceAndCreateIfNeeded(region, project, user);
final KubernetesClient kubClient = kubernetesClientProvider.getRootClient(region);
final String namespace = determineNamespaceAndCreateIfNeeded(region, project, user);
applyQuotas(namespace, kubClient, quota, true);
}

public ResourceQuota getOnyxiaQuota(Region region, Project project, User user) {
KubernetesClient kubClient = kubernetesClientProvider.getRootClient(region);
String namespace = determineNamespaceAndCreateIfNeeded(region, project, user);
final KubernetesClient kubClient = kubernetesClientProvider.getRootClient(region);
final String namespace = determineNamespaceAndCreateIfNeeded(region, project, user);
return kubClient.resourceQuotas().inNamespace(namespace).withName("onyxia-quota").get();
}

Expand Down
Loading

0 comments on commit c46fb9a

Please sign in to comment.