diff --git a/src/main/java/net/geant/nmaas/externalservices/kubernetes/ClusterMonitoringJob.java b/src/main/java/net/geant/nmaas/externalservices/kubernetes/ClusterMonitoringJob.java index bc88e1f2cf3e0291e3edd79b75b5d98d87bf6c8e..2423eb67c11c24480e5e0cc06320cbde16bfd02c 100644 --- a/src/main/java/net/geant/nmaas/externalservices/kubernetes/ClusterMonitoringJob.java +++ b/src/main/java/net/geant/nmaas/externalservices/kubernetes/ClusterMonitoringJob.java @@ -16,7 +16,9 @@ public class ClusterMonitoringJob implements Job { @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { - log.error("Triggering cluster health check..."); + log.info("Triggering cluster health check..."); + remoteClusterManager.restoreFileIfMissing(); + log.info("File checked, everything looks fine. Next stage: Update clusters state."); remoteClusterManager.updateAllClusterState(); } } diff --git a/src/main/java/net/geant/nmaas/externalservices/kubernetes/RemoteClusterManager.java b/src/main/java/net/geant/nmaas/externalservices/kubernetes/RemoteClusterManager.java index f9ddb9cf405c294741bf1764ae2e0898bd8dfb73..f4e74e07cd9034b089f902e7eabeb0af72a0073c 100644 --- a/src/main/java/net/geant/nmaas/externalservices/kubernetes/RemoteClusterManager.java +++ b/src/main/java/net/geant/nmaas/externalservices/kubernetes/RemoteClusterManager.java @@ -3,30 +3,31 @@ package net.geant.nmaas.externalservices.kubernetes; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import io.fabric8.kubernetes.client.Config; -import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.KubernetesClientException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import net.geant.nmaas.externalservices.kubernetes.entities.KCluster; import net.geant.nmaas.externalservices.kubernetes.api.model.RemoteClusterView; +import net.geant.nmaas.externalservices.kubernetes.entities.KCluster; import net.geant.nmaas.externalservices.kubernetes.entities.KClusterDeployment; import net.geant.nmaas.externalservices.kubernetes.entities.KClusterIngress; import net.geant.nmaas.externalservices.kubernetes.entities.KClusterState; import net.geant.nmaas.externalservices.kubernetes.repositories.KClusterRepository; +import net.geant.nmaas.notifications.MailAttributes; +import net.geant.nmaas.notifications.NotificationEvent; +import net.geant.nmaas.notifications.templates.MailType; +import net.geant.nmaas.portal.api.domain.UserView; import net.geant.nmaas.portal.persistent.entity.Domain; import net.geant.nmaas.portal.service.DomainService; import org.modelmapper.ModelMapper; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.InterruptedIOException; -import java.net.SocketTimeoutException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -36,7 +37,10 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.OffsetDateTime; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -48,13 +52,15 @@ public class RemoteClusterManager { private final static ModelMapper modelMapper = new ModelMapper(); - private final KClusterRepository KClusterRepository; + private final KClusterRepository clusterRepository; private final KubernetesClusterIngressManager kClusterIngressManager; private final KubernetesClusterDeploymentManager kClusterDeploymentManager; private final DomainService domainService; + private final ApplicationEventPublisher eventPublisher; + public RemoteClusterView getClusterView(Long id) { - Optional<KCluster> cluster = KClusterRepository.findById(id); + Optional<KCluster> cluster = clusterRepository.findById(id); if (cluster.isPresent()) { return toView(cluster.get()); } else { @@ -63,12 +69,12 @@ public class RemoteClusterManager { } public List<RemoteClusterView> getAllClusterView() { - List<KCluster> clusters = KClusterRepository.findAll(); + List<KCluster> clusters = clusterRepository.findAll(); return clusters.stream().map(RemoteClusterManager::toView).collect(Collectors.toList()); } public KCluster getCluster(Long id) { - Optional<KCluster> cluster = KClusterRepository.findById(id); + Optional<KCluster> cluster = clusterRepository.findById(id); if (cluster.isPresent()) { return cluster.get(); } else { @@ -99,8 +105,9 @@ public class RemoteClusterManager { log.debug("Filed saved in: {}", savedPath); entity.setPathConfigFile(savedPath); - KCluster cluster = this.KClusterRepository.save(entity); + KCluster cluster = this.clusterRepository.save(entity); log.debug("Cluster saved: {}", cluster.toString()); + sendMail(cluster, MailType.REMOTE_CLUSTER_WELCOME_SUPPORT); return toView(cluster); } @@ -126,10 +133,11 @@ public class RemoteClusterManager { .creationDate(OffsetDateTime.now()) .modificationDate(OffsetDateTime.now()) .codename(configView.getClusters().stream().findFirst().get().getName()) - .clusterConfigFile( new String(file.getBytes())) + .clusterConfigFile(new String(file.getBytes())) .deployment(deployment) .ingress(ingress) .state(KClusterState.UNKNOWN) + .contactEmail(view.getContactEmail()) .currentStateSince(OffsetDateTime.now()) .domains(view.getDomainNames().stream().map(d -> { Optional<Domain> dom = domainService.findDomain(d); @@ -153,7 +161,7 @@ public class RemoteClusterManager { } public RemoteClusterView updateCluster(RemoteClusterView cluster, Long id) { - Optional<KCluster> entity = KClusterRepository.findById(id); + Optional<KCluster> entity = clusterRepository.findById(id); if (entity.isPresent()) { checkRequest(entity.get(), cluster, id); @@ -176,7 +184,7 @@ public class RemoteClusterManager { updated.setDeployment(modelMapper.map(cluster.getDeployment(), KClusterDeployment.class)); - updated = KClusterRepository.save(updated); + updated = clusterRepository.save(updated); //TODO : implement file update logic return toView(updated); @@ -239,17 +247,18 @@ public class RemoteClusterManager { } public void updateAllClusterState() { - List<KCluster> kClusters = KClusterRepository.findAll(); + List<KCluster> kClusters = clusterRepository.findAll(); kClusters.forEach(cluster -> { Config config = null; try { config = Config.fromKubeconfig(Files.readString(Path.of(cluster.getPathConfigFile()))); - } catch (IOException e) { - throw new RuntimeException(e); + } catch (IOException e) { + log.error("IO error with accesing the file {}", e.getMessage()); + updateStateIfNeeded(cluster, KClusterState.UNKNOWN); } try { KubernetesClient client = new KubernetesClientBuilder().withConfig(config).build(); - log.debug("Get kubernetes version , something works {}",client.getKubernetesVersion().getPlatform()); + log.debug("Get kubernetes version , something works {}", client.getKubernetesVersion().getPlatform()); //try to download kubernetes version to make sure connection to cluster is working updateStateIfNeeded(cluster, KClusterState.UP); @@ -258,7 +267,8 @@ public class RemoteClusterManager { log.error(e.getMessage()); updateStateIfNeeded(cluster, KClusterState.DOWN); - } catch (RuntimeException ex ) { + + } catch (RuntimeException ex) { log.error("Runtime error while checking health of cluster {}", ex.getMessage()); updateStateIfNeeded(cluster, KClusterState.UNKNOWN); @@ -267,7 +277,7 @@ public class RemoteClusterManager { } }); - KClusterRepository.saveAll(kClusters); + clusterRepository.saveAll(kClusters); } @@ -275,7 +285,50 @@ public class RemoteClusterManager { if (!cluster.getState().equals(newState)) { cluster.setState(newState); cluster.setCurrentStateSince(OffsetDateTime.now()); + if(cluster.getState().equals(KClusterState.DOWN) || cluster.getState().equals(KClusterState.UNKNOWN)) { + sendMail(cluster, MailType.REMOTE_CLUSTER_UNAVAILABLE); + }; } } + private void sendMail(KCluster kCluster, MailType mailType) { + UserView recipient = UserView.builder().email(kCluster.getContactEmail()).username(kCluster.getContactEmail()).selectedLanguage("EN").build(); + Map<String, Object> attr = new HashMap<>(); + attr.put("clusterId", kCluster.getId()); + attr.put("clusterCodename", kCluster.getCodename()); + attr.put("clusterName", kCluster.getName()); + MailAttributes mailAttributes = MailAttributes.builder() + .mailType(mailType) + .otherAttributes(attr) + .addressees(Collections.singletonList(recipient)) + .build(); + + this.eventPublisher.publishEvent(new NotificationEvent(this, mailAttributes)); + + } + + public void restoreFileIfMissing() { + List<KCluster> clusters = clusterRepository.findAll(); + clusters.forEach(cluster -> { + if (!isFileAvailable(cluster.getPathConfigFile())) { + MultipartFile file = new StringMultipartFile("file", + "config.yaml", + "application/x-yaml", + cluster.getClusterConfigFile()); + try { + String savedPath = saveFileToTmp(file); + cluster.setPathConfigFile(savedPath); + this.clusterRepository.save(cluster); + } catch (IOException | NoSuchAlgorithmException e) { + log.error("Problem with resaved kubernetes config file from string to TMP folder. {}", e.getMessage()); + } + } + }); + } + + + public boolean isFileAvailable(String pathStr) { + Path path = Paths.get(pathStr); + return Files.exists(path) && Files.isRegularFile(path) && Files.isReadable(path); + } } \ No newline at end of file diff --git a/src/main/java/net/geant/nmaas/externalservices/kubernetes/StringMultipartFile.java b/src/main/java/net/geant/nmaas/externalservices/kubernetes/StringMultipartFile.java new file mode 100644 index 0000000000000000000000000000000000000000..42b7fea0646fff4a0df32035a1f21d373e374069 --- /dev/null +++ b/src/main/java/net/geant/nmaas/externalservices/kubernetes/StringMultipartFile.java @@ -0,0 +1,67 @@ +package net.geant.nmaas.externalservices.kubernetes; + +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class StringMultipartFile implements MultipartFile { + + private final byte[] content; + private final String name; + private final String originalFilename; + private final String contentType; + + public StringMultipartFile(String name, String originalFilename, String contentType, String contentStr) { + this.name = name; + this.originalFilename = originalFilename; + this.contentType = contentType; + this.content = contentStr.getBytes(StandardCharsets.UTF_8); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getOriginalFilename() { + return originalFilename; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public boolean isEmpty() { + return content.length == 0; + } + + @Override + public long getSize() { + return content.length; + } + + @Override + public byte[] getBytes() { + return content; + } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(content); + } + + @Override + public void transferTo(File dest) throws IOException, IllegalStateException { + try (FileOutputStream out = new FileOutputStream(dest)) { + out.write(content); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/geant/nmaas/externalservices/kubernetes/api/model/RemoteClusterView.java b/src/main/java/net/geant/nmaas/externalservices/kubernetes/api/model/RemoteClusterView.java index 563190078758521fb6bff87ff3c44e0ff414c8a2..9404460b9ca0327cf8b23ca1124398451f02d924 100644 --- a/src/main/java/net/geant/nmaas/externalservices/kubernetes/api/model/RemoteClusterView.java +++ b/src/main/java/net/geant/nmaas/externalservices/kubernetes/api/model/RemoteClusterView.java @@ -43,4 +43,6 @@ public class RemoteClusterView { private OffsetDateTime currentStateSince; + private String contactEmail; + } \ No newline at end of file diff --git a/src/main/java/net/geant/nmaas/externalservices/kubernetes/entities/KCluster.java b/src/main/java/net/geant/nmaas/externalservices/kubernetes/entities/KCluster.java index e5e81ce51debe3b7d3c1379917b8b3878861c637..a8aab72d92d81621e423631089e8772c38215c3c 100644 --- a/src/main/java/net/geant/nmaas/externalservices/kubernetes/entities/KCluster.java +++ b/src/main/java/net/geant/nmaas/externalservices/kubernetes/entities/KCluster.java @@ -80,6 +80,9 @@ public class KCluster { @Column(nullable = false) private OffsetDateTime currentStateSince; + @Column(nullable = false) + private String contactEmail; + public List<Domain> getDomains() { return domains != null ? domains : new ArrayList<>(); } @@ -97,6 +100,7 @@ public class KCluster { ", clusterConfigFile='" + clusterConfigFile + '\'' + ", pathConfigFile='" + pathConfigFile + '\'' + ", state='" + state + '\'' + + ", email='" + contactEmail + '\'' + ", ingress=" + (ingress != null ? ingress.getId() : "null") + ", deployment=" + (deployment != null ? deployment.getId() : "null") + '}'; diff --git a/src/main/java/net/geant/nmaas/notifications/templates/MailType.java b/src/main/java/net/geant/nmaas/notifications/templates/MailType.java index 9291f9814ef56e4379faf28360b94ff2b8fab817..feec41f299d8e4b8409febe38d67bd4bdaa9be90 100644 --- a/src/main/java/net/geant/nmaas/notifications/templates/MailType.java +++ b/src/main/java/net/geant/nmaas/notifications/templates/MailType.java @@ -25,5 +25,7 @@ public enum MailType { NEW_BULK_SSO_LOGIN, NEW_BULK_LOGIN, - VLAB_REQUEST + VLAB_REQUEST, + REMOTE_CLUSTER_UNAVAILABLE, + REMOTE_CLUSTER_WELCOME_SUPPORT } diff --git a/src/main/java/net/geant/nmaas/portal/service/impl/DashboardServiceImpl.java b/src/main/java/net/geant/nmaas/portal/service/impl/DashboardServiceImpl.java index 0862efacea6fdea90ed6ee0e147a2771c0778d37..1569cfee20dba94bf5c523dcffd7610d72c12bd3 100644 --- a/src/main/java/net/geant/nmaas/portal/service/impl/DashboardServiceImpl.java +++ b/src/main/java/net/geant/nmaas/portal/service/impl/DashboardServiceImpl.java @@ -64,6 +64,9 @@ public class DashboardServiceImpl implements DashboardService { applicationDeploymentCountPerName.put(name, appInstanceRepo.countByName(name)); }); + //filter not deployed application + applicationDeploymentCountPerName.entrySet().removeIf(app -> app.getValue() == 0); + return DashboardView.builder() .domainsCount(domainRepository.count()) diff --git a/src/main/resources/db/migration/common/V1.8.0_20250515_1230__AddContactEmailToCluster.sql b/src/main/resources/db/migration/common/V1.8.0_20250515_1230__AddContactEmailToCluster.sql new file mode 100644 index 0000000000000000000000000000000000000000..ad0284c06de346a25bcee80ae39624fec5a79b88 --- /dev/null +++ b/src/main/resources/db/migration/common/V1.8.0_20250515_1230__AddContactEmailToCluster.sql @@ -0,0 +1 @@ +alter table k_cluster add COLUMN contact_email varchar(255) not null; \ No newline at end of file diff --git a/src/test/shell/data/i18n/de.json b/src/test/shell/data/i18n/de.json index 76e4a329fb6dc4fc2ce6fd31d92995e1b75cc306..4d8597ce9478703a17493293fef4a55df1d87057 100644 --- a/src/test/shell/data/i18n/de.json +++ b/src/test/shell/data/i18n/de.json @@ -190,7 +190,9 @@ "STATE" : "State", "UP" : "Up", "DOWN" : "Down", - "UNKNOWN" : "Unknown" + "UNKNOWN" : "Unknown", + "DETAILS" : "Details", + "STATE_SINCE" : "State last change" }, "GITLAB": { "TITLE": "GitLab Konfiguration", diff --git a/src/test/shell/data/i18n/en.json b/src/test/shell/data/i18n/en.json index a6aee3818aac82cfb7c9a488dc7d06774c578dcb..e2aea66b117a82b6da5343dadcc94354ebcae2c1 100644 --- a/src/test/shell/data/i18n/en.json +++ b/src/test/shell/data/i18n/en.json @@ -191,7 +191,9 @@ "STATE" : "State", "UP" : "Up", "DOWN" : "Down", - "UNKNOWN" : "Unknown" + "UNKNOWN" : "Unknown", + "DETAILS" : "Details", + "STATE_SINCE" : "State last change" }, "GITLAB": { "TITLE": "GitLab configuration", diff --git a/src/test/shell/data/i18n/fr.json b/src/test/shell/data/i18n/fr.json index 147d5f3e958f54c54832190cddf91214dcbac223..5051d97030f59fd51f7f4e1848de3bb1f27c4801 100644 --- a/src/test/shell/data/i18n/fr.json +++ b/src/test/shell/data/i18n/fr.json @@ -192,7 +192,9 @@ "STATE" : "State", "UP" : "Up", "DOWN" : "Down", - "UNKNOWN" : "Unknown" + "UNKNOWN" : "Unknown", + "DETAILS" : "Details", + "STATE_SINCE" : "State last change" }, "GITLAB": { "TITLE": "Configuration de GitLab", diff --git a/src/test/shell/data/i18n/pl.json b/src/test/shell/data/i18n/pl.json index 7aac0a1066bb582b38a38dab7b5619c57f68e566..f3e91fadca23a09c96a470ee314048e333909297 100644 --- a/src/test/shell/data/i18n/pl.json +++ b/src/test/shell/data/i18n/pl.json @@ -191,7 +191,9 @@ "STATE" : "Stan", "UP" : "Aktywny", "DOWN" : "Nieaktywny", - "UNKNOWN" : "Nieznany" + "UNKNOWN" : "Nieznany", + "DETAILS" : "Detale", + "STATE_SINCE" : "Stan od" }, "GITLAB": { "TITLE": "Konfiguracja GitLab", diff --git a/src/test/shell/data/mails/clusterSupportEmail.json b/src/test/shell/data/mails/clusterSupportEmail.json new file mode 100644 index 0000000000000000000000000000000000000000..192f7daa3413adb05771036fc34018fec0516938 --- /dev/null +++ b/src/test/shell/data/mails/clusterSupportEmail.json @@ -0,0 +1,55 @@ +{ + "mailType": "REMOTE_CLUSTER_WELCOME_SUPPORT", + "globalInformation": { + "LOGO_ALT": "GÉANT logo", + "PORTAL_LOGO_ALT": "nmaas logo", + "SENDER_INFO": "Ⓒ GÉANT Association Hoekenrode 3 1102 BR - Amsterdam – Zuidoost- The Netherlands" + }, + "templates": [ + { + "language": "en", + "subject": "nmaas: Cluster support", + "template": { + "HEADER": "Dear ${username}", + "CONTENT": "<p>Your email address is added to support remote cluster <b>${clusterCodename}</b>. </p> <p>If you do not agree or do not proceed this action please contact administration for further information.</p>", + "SENDER": "Best regards,<br />nmaas Team", + "NOREPLY": "This is an automatically generated message, please do not reply.", + "SENDER_POLICY": "" + } + }, + { + "language": "fr", + "subject": "nmaas: Cluster is unavailable", + "template": { + "HEADER": "Dear ${username}", + "CONTENT": "<p>Your email address is added to support remote cluster <b>${clusterCodename}</b>. </p> <p>If you do not agree or do not proceed this action please contact administration for further information.</p>", + + "SENDER": "Best regards,<br />nmaas Team", + "NOREPLY": "This is an automatically generated message, please do not reply.", + "SENDER_POLICY": "" + } + }, + { + "language": "de", + "subject": "nmaas: Cluster is unavailable", + "template": { + "HEADER": "Dear ${username}", + "CONTENT": "<p>Your email address is added to support remote cluster <b>${clusterCodename}</b>. </p> <p>If you do not agree or do not proceed this action please contact administration for further information.</p>", + "SENDER": "Best regards,<br />nmaas Team", + "NOREPLY": "This is an automatically generated message, please do not reply.", + "SENDER_POLICY": "" + } + }, + { + "language": "pl", + "subject": "nmaas: Cluster jest niedostępny", + "template": { + "HEADER": "Drogi ${username}", + "CONTENT": "<p>Your email address is added to support remote cluster <b>${clusterCodename}</b>. </p> <p>If you do not agree or do not proceed this action please contact administration for further information.</p>", + "SENDER": "Z pozdrowieniami,<br />Zespół nmaas", + "NOREPLY": "Ta wiadomość została wygenerowana automatycznie.", + "SENDER_POLICY": "" + } + } + ] +} \ No newline at end of file diff --git a/src/test/shell/data/mails/clusterUnavailable.json b/src/test/shell/data/mails/clusterUnavailable.json new file mode 100644 index 0000000000000000000000000000000000000000..b126a6d6fbd55f29d996e7dd69e5ef8f7757cbf3 --- /dev/null +++ b/src/test/shell/data/mails/clusterUnavailable.json @@ -0,0 +1,55 @@ +{ + "mailType": "REMOTE_CLUSTER_UNAVAILABLE", + "globalInformation": { + "LOGO_ALT": "GÉANT logo", + "PORTAL_LOGO_ALT": "nmaas logo", + "SENDER_INFO": "Ⓒ GÉANT Association Hoekenrode 3 1102 BR - Amsterdam – Zuidoost- The Netherlands" + }, + "templates": [ + { + "language": "en", + "subject": "nmaas: Cluster is unavailable", + "template": { + "HEADER": "Dear ${username}", + "CONTENT": "<p> Cluster <b>${clusterCodename}</b> is unavailable.</p> <p><b> Cluster ID:<b> ${clusterId}</p> <p><b> Cluster CodeName:<b> ${clusterCodename}</p> <p>Please contact the administration for further information.</p>", + "SENDER": "Best regards,<br />nmaas Team", + "NOREPLY": "This is an automatically generated message, please do not reply.", + "SENDER_POLICY": "" + } + }, + { + "language": "fr", + "subject": "nmaas: Cluster is unavailable", + "template": { + "HEADER": "Dear ${username}", + "CONTENT": "<p> Cluster <b>${clusterCodename}</b> is unavailable.</p> <p><b> Cluster ID:<b> ${clusterId}</p> <p><b> Cluster CodeName:<b> ${clusterCodename}</p> <p>Please contact the administration for further information.</p>", + + "SENDER": "Best regards,<br />nmaas Team", + "NOREPLY": "This is an automatically generated message, please do not reply.", + "SENDER_POLICY": "" + } + }, + { + "language": "de", + "subject": "nmaas: Cluster is unavailable", + "template": { + "HEADER": "Dear ${username}", + "CONTENT": "<p> Cluster <b>${clusterCodename}</b> is unavailable.</p> <p><b> Cluster ID:<b> ${clusterId}</p> <p><b> Cluster CodeName:<b> ${clusterCodename}</p> <p>Please contact the administration for further information.</p>", + "SENDER": "Best regards,<br />nmaas Team", + "NOREPLY": "This is an automatically generated message, please do not reply.", + "SENDER_POLICY": "" + } + }, + { + "language": "pl", + "subject": "nmaas: Cluster jest niedostępny", + "template": { + "HEADER": "Drogi ${username}", + "CONTENT": "<p> Cluster <b>${clusterCodename}</b> is unavailable.</p> <p><b> Cluster ID:</b> ${clusterId}</p> <p><b> Cluster CodeName:</b> ${clusterCodename}</p> <p> <i>Please contact the administration for further information. </i></p>", + "SENDER": "Z pozdrowieniami,<br />Zespół nmaas", + "NOREPLY": "Ta wiadomość została wygenerowana automatycznie.", + "SENDER_POLICY": "" + } + } + ] +} \ No newline at end of file