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 d3e51347..80184ef6 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 @@ -253,6 +253,19 @@ private void suspendOrResume(Region region, Project project, String serviceId, b } } + @GetMapping("/app/details") + public Service getAppDetails( + @Parameter(hidden = true) Region region, + @Parameter(hidden = true) Project project, + @RequestParam("serviceId") String serviceId) + throws Exception { + if (Service.ServiceType.KUBERNETES.equals(region.getServices().getType())) { + return helmAppsService.getUserServiceDetails( + region, project, userProvider.getUser(region), serviceId); + } + return null; + } + @Operation( summary = "Get the logs of a task in an installed service.", description = 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 8c24fe59..06889b1e 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 @@ -46,6 +46,9 @@ Collection installApp( Service getUserService(Region region, Project project, User user, String serviceId) throws Exception; + Service getUserServiceDetails(Region region, Project project, User user, String serviceId) + throws Exception; + UninstallService destroyService( Region region, Project project, User user, String path, boolean bulk) throws Exception; 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 b4d6cba0..0b017bce 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 @@ -27,6 +27,9 @@ import fr.insee.onyxia.model.project.Project; import fr.insee.onyxia.model.region.Region; import fr.insee.onyxia.model.service.*; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.OwnerReference; +import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.Watch; @@ -37,8 +40,11 @@ import io.github.inseefrlab.helmwrapper.model.HelmReleaseInfo; import io.github.inseefrlab.helmwrapper.service.HelmInstallService; import io.github.inseefrlab.helmwrapper.service.HelmInstallService.MultipleServiceFound; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -336,6 +342,170 @@ public Service getUserService(Region region, Project project, User user, String return getHelmApp(region, user, result); } + @Override + public Service getUserServiceDetails( + Region region, Project project, User user, String serviceId) + throws MultipleServiceFound, ParseException { + if (serviceId.startsWith("/")) { + serviceId = serviceId.substring(1); + } + String namespace = + kubernetesService.determineNamespaceAndCreateIfNeeded(region, project, user); + HelmLs result = + getHelmInstallService() + .getAppById(getHelmConfiguration(region, user), serviceId, namespace); + HelmReleaseInfo helmReleaseInfo = + getHelmInstallService() + .getAll( + getHelmConfiguration(region, user), + result.getName(), + result.getNamespace()); + Service res = getHelmApp(region, user, result); + KubernetesClient client = kubernetesClientProvider.getUserClient(region, user); + List hasMetadata; + try (InputStream inputStream = + new ByteArrayInputStream( + helmReleaseInfo.getManifest().getBytes(StandardCharsets.UTF_8))) { + hasMetadata = client.load(inputStream).items(); + } catch (IOException e) { + throw new RuntimeException("Exception during loading manifest", e); + } + List manifests = new ArrayList(hasMetadata); + res.setTemplates(manifests); + List> podInfoList = new ArrayList<>(); + + List pods = client.pods().inNamespace(namespace).list().getItems(); + for (Pod pod : pods) { + Map podInfo = new HashMap<>(); + podInfo.put("podName", pod.getMetadata().getName()); + podInfo.put("owners", getOwnerReferences(pod, client)); + podInfo.put("containers", getContainers(pod)); + podInfoList.add(podInfo); + } + res.setPodsAndOwners( + filterPodsByTopLevelOwnerReferences(podInfoList, hasMetadata, namespace, client)); + return res; + } + + private List> getContainers(Pod pod) { + List> containersInfo = new ArrayList<>(); + List containers = pod.getSpec().getContainers(); + List initContainers = + pod.getSpec().getInitContainers(); + + for (io.fabric8.kubernetes.api.model.Container container : containers) { + Map containerInfo = new HashMap<>(); + containerInfo.put("name", container.getName()); + containerInfo.put("image", container.getImage()); + containerInfo.put("resources", container.getResources()); + containerInfo.put("type", "container"); + + containersInfo.add(containerInfo); + } + + for (io.fabric8.kubernetes.api.model.Container initContainer : initContainers) { + Map initContainerInfo = new HashMap<>(); + initContainerInfo.put("name", initContainer.getName()); + initContainerInfo.put("image", initContainer.getImage()); + initContainerInfo.put("resources", initContainer.getResources()); + initContainerInfo.put("type", "initContainer"); + + containersInfo.add(initContainerInfo); + } + + return containersInfo; + } + + private List> getOwnerReferences( + HasMetadata resource, KubernetesClient client) { + List> owners = new ArrayList<>(); + + List ownerReferences = resource.getMetadata().getOwnerReferences(); + if (ownerReferences != null) { + for (OwnerReference ownerReference : ownerReferences) { + String kind = ownerReference.getKind(); + String name = ownerReference.getName(); + + // Fetch the owner resource + HasMetadata ownerResource = + fetchOwnerResource( + resource.getMetadata().getNamespace(), kind, name, client); + if (ownerResource != null) { + Map ownerInfo = new HashMap<>(); + ownerInfo.put("kind", kind); + ownerInfo.put("name", name); + ownerInfo.put("owners", getOwnerReferences(ownerResource, client)); + + owners.add(ownerInfo); + } + } + } + + return owners; + } + + private HasMetadata fetchOwnerResource( + String namespace, String kind, String name, KubernetesClient client) { + switch (kind) { + case "ReplicaSet": + return client.apps().replicaSets().inNamespace(namespace).withName(name).get(); + case "Deployment": + return client.apps().deployments().inNamespace(namespace).withName(name).get(); + case "StatefulSet": + return client.apps().statefulSets().inNamespace(namespace).withName(name).get(); + case "DaemonSet": + return client.apps().daemonSets().inNamespace(namespace).withName(name).get(); + case "Pod": + return client.pods().inNamespace(namespace).withName(name).get(); + default: + return null; // Handle other kinds if needed + } + } + + private List> filterPodsByTopLevelOwnerReferences( + List> podInfoList, + List resourceList, + String namespace, + KubernetesClient client) { + Set resourceNamesAndKinds = + resourceList.stream() + .map( + resource -> + resource.getKind() + "/" + resource.getMetadata().getName()) + .collect(Collectors.toSet()); + + return podInfoList.stream() + .filter( + podInfo -> { + List> owners = + (List>) podInfo.get("owners"); + return owners.stream() + .anyMatch( + owner -> + resourceNamesAndKinds.contains( + findTopLevelOwner( + (String) owner.get("kind"), + (String) owner.get("name"), + namespace, + client))); + }) + .collect(Collectors.toList()); + } + + private String findTopLevelOwner( + String kind, String name, String namespace, KubernetesClient client) { + HasMetadata resource = fetchOwnerResource(namespace, kind, name, client); + while (resource != null && !resource.getMetadata().getOwnerReferences().isEmpty()) { + OwnerReference ownerReference = resource.getMetadata().getOwnerReferences().get(0); + resource = + fetchOwnerResource( + namespace, ownerReference.getKind(), ownerReference.getName(), client); + } + return resource != null + ? resource.getKind() + "/" + resource.getMetadata().getName() + : null; + } + @Override public UninstallService destroyService( Region region, Project project, User user, final String path, boolean bulk) diff --git a/onyxia-model/src/main/java/fr/insee/onyxia/model/service/Service.java b/onyxia-model/src/main/java/fr/insee/onyxia/model/service/Service.java index 60f422a5..cbe85f4c 100644 --- a/onyxia-model/src/main/java/fr/insee/onyxia/model/service/Service.java +++ b/onyxia-model/src/main/java/fr/insee/onyxia/model/service/Service.java @@ -92,6 +92,11 @@ public class Service { @Schema(description = "") private Map labels; + @Schema(description = "") + private List> podsAndOwners; + + private List templates; + public String getId() { return id; } @@ -276,6 +281,22 @@ public void setSuspended(boolean suspended) { this.suspended = suspended; } + public List> getPodsAndOwners() { + return podsAndOwners; + } + + public void setPodsAndOwners(List> podsAndOwners) { + this.podsAndOwners = podsAndOwners; + } + + public List getTemplates() { + return templates; + } + + public void setTemplates(List templates) { + this.templates = templates; + } + public String getCatalogId() { return catalogId; }