From 31bdf309120acb8c9832baa9f1b744af0816a228 Mon Sep 17 00:00:00 2001 From: cgeorgilakis-grnet <cgeorgilakis@admin.grnet.gr> Date: Fri, 16 May 2025 12:59:02 +0300 Subject: [PATCH 1/2] Webhook communication for Domain group creation/deletion #277 --- build.gradle | 2 +- .../orchestration/jobs/DomainGroupJob.java | 55 +++++++++++++++++++ .../domain/ApplicationStatePerDomainView.java | 4 +- .../nmaas/portal/api/domain/DomainBase.java | 4 +- .../portal/api/domain/DomainGroupView.java | 3 +- .../api/domain/DomainGroupWebhookDto.java | 14 +++++ .../service/impl/DomainGroupServiceImpl.java | 22 +++++++- .../service/impl/DomainServiceTest.java | 2 +- 8 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 src/main/java/net/geant/nmaas/orchestration/jobs/DomainGroupJob.java create mode 100644 src/main/java/net/geant/nmaas/portal/api/domain/DomainGroupWebhookDto.java diff --git a/build.gradle b/build.gradle index a64fd32ca..892bc6465 100644 --- a/build.gradle +++ b/build.gradle @@ -88,7 +88,7 @@ dependencies { implementation('org.springframework.boot:spring-boot-starter-mail') implementation('org.springframework.boot:spring-boot-starter-data-jpa') implementation('org.springframework.boot:spring-boot-starter-actuator') - implementation('org.springframework.boot:spring-boot-devtools') +// implementation('org.springframework.boot:spring-boot-devtools') implementation('org.springframework.boot:spring-boot-starter-quartz') implementation('org.springframework.boot:spring-boot-starter-validation') implementation 'org.springframework.boot:spring-boot-starter-cache' diff --git a/src/main/java/net/geant/nmaas/orchestration/jobs/DomainGroupJob.java b/src/main/java/net/geant/nmaas/orchestration/jobs/DomainGroupJob.java new file mode 100644 index 000000000..7fe42451e --- /dev/null +++ b/src/main/java/net/geant/nmaas/orchestration/jobs/DomainGroupJob.java @@ -0,0 +1,55 @@ +package net.geant.nmaas.orchestration.jobs; + +import lombok.extern.slf4j.Slf4j; +import net.geant.nmaas.orchestration.exceptions.WebServiceCommunicationException; +import net.geant.nmaas.portal.api.domain.DomainGroupView; +import net.geant.nmaas.portal.api.domain.DomainGroupWebhookDto; +import net.geant.nmaas.portal.api.domain.WebhookEventDto; +import net.geant.nmaas.portal.api.exception.MissingElementException; +import net.geant.nmaas.portal.persistent.entity.WebhookEventType; +import net.geant.nmaas.portal.service.WebhookEventService; +import org.modelmapper.ModelMapper; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import java.security.GeneralSecurityException; + +@Slf4j +@Component +public class DomainGroupJob extends WebhookJob { + + @Autowired + public DomainGroupJob(RestClient restClient, WebhookEventService webhookEventService, ModelMapper modelMapper) { + super(restClient, webhookEventService, modelMapper); + } + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + JobDataMap dataMap = context.getJobDetail().getJobDataMap(); + Long webhookId = dataMap.getLong("webhookId"); + String action = dataMap.getString("action"); + DomainGroupView domainGroup = (DomainGroupView) dataMap.get("domainGroup"); + + try { + WebhookEventDto webhook = webhookEventService.getById(webhookId); + if (!WebhookEventType.DOMAIN_GROUP_CHANGE.equals(webhook.getEventType())) { + log.warn("Webhook's event type with id {} has been updated. DomainGroupJob is abandoned", webhookId); + return; + } + DomainGroupWebhookDto view = new DomainGroupWebhookDto(domainGroup, action); + callWebhook(webhook, view); + } catch (GeneralSecurityException e) { + log.error("Failed to decrypt webhook with id {}", webhookId); + throw new JobExecutionException("Failed webhook decryption"); + } catch (MissingElementException e) { + log.warn("Webhook does not exist. DomainGroupJob is abandoned"); + } catch (WebServiceCommunicationException e) { + log.error("Failed to communicate with external system for the webhoook of domain group with id {}", domainGroup.getId()); + throw new JobExecutionException("Failed communication with external system"); + } + } +} diff --git a/src/main/java/net/geant/nmaas/portal/api/domain/ApplicationStatePerDomainView.java b/src/main/java/net/geant/nmaas/portal/api/domain/ApplicationStatePerDomainView.java index 7ed46a209..ccce6b28f 100644 --- a/src/main/java/net/geant/nmaas/portal/api/domain/ApplicationStatePerDomainView.java +++ b/src/main/java/net/geant/nmaas/portal/api/domain/ApplicationStatePerDomainView.java @@ -3,9 +3,11 @@ package net.geant.nmaas.portal.api.domain; import lombok.Getter; import lombok.Setter; +import java.io.Serializable; + @Getter @Setter -public class ApplicationStatePerDomainView { +public class ApplicationStatePerDomainView implements Serializable { Long applicationBaseId; String applicationBaseName; boolean enabled; diff --git a/src/main/java/net/geant/nmaas/portal/api/domain/DomainBase.java b/src/main/java/net/geant/nmaas/portal/api/domain/DomainBase.java index 9bfd26a5b..cf77585ec 100644 --- a/src/main/java/net/geant/nmaas/portal/api/domain/DomainBase.java +++ b/src/main/java/net/geant/nmaas/portal/api/domain/DomainBase.java @@ -4,10 +4,12 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.io.Serializable; + @Getter @Setter @NoArgsConstructor -public class DomainBase { +public class DomainBase implements Serializable { Long id; String name; diff --git a/src/main/java/net/geant/nmaas/portal/api/domain/DomainGroupView.java b/src/main/java/net/geant/nmaas/portal/api/domain/DomainGroupView.java index 2e40d9005..a5855816d 100644 --- a/src/main/java/net/geant/nmaas/portal/api/domain/DomainGroupView.java +++ b/src/main/java/net/geant/nmaas/portal/api/domain/DomainGroupView.java @@ -6,6 +6,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; @@ -13,7 +14,7 @@ import java.util.List; @NoArgsConstructor @Getter @Setter -public class DomainGroupView { +public class DomainGroupView implements Serializable { @NotNull private Long id; diff --git a/src/main/java/net/geant/nmaas/portal/api/domain/DomainGroupWebhookDto.java b/src/main/java/net/geant/nmaas/portal/api/domain/DomainGroupWebhookDto.java new file mode 100644 index 000000000..756851807 --- /dev/null +++ b/src/main/java/net/geant/nmaas/portal/api/domain/DomainGroupWebhookDto.java @@ -0,0 +1,14 @@ +package net.geant.nmaas.portal.api.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@AllArgsConstructor +@Getter +@Setter +public class DomainGroupWebhookDto { + + private DomainGroupView domainGroup; + private String action; +} diff --git a/src/main/java/net/geant/nmaas/portal/service/impl/DomainGroupServiceImpl.java b/src/main/java/net/geant/nmaas/portal/service/impl/DomainGroupServiceImpl.java index 3dbc1baf9..2823c728c 100644 --- a/src/main/java/net/geant/nmaas/portal/service/impl/DomainGroupServiceImpl.java +++ b/src/main/java/net/geant/nmaas/portal/service/impl/DomainGroupServiceImpl.java @@ -2,6 +2,8 @@ package net.geant.nmaas.portal.service.impl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import net.geant.nmaas.orchestration.jobs.DomainCreationJob; +import net.geant.nmaas.orchestration.jobs.DomainGroupJob; import net.geant.nmaas.portal.api.domain.ApplicationStatePerDomainView; import net.geant.nmaas.portal.api.domain.DomainGroupView; import net.geant.nmaas.portal.api.exception.MissingElementException; @@ -11,16 +13,21 @@ import net.geant.nmaas.portal.persistent.entity.ApplicationStatePerDomain; import net.geant.nmaas.portal.persistent.entity.Domain; import net.geant.nmaas.portal.persistent.entity.DomainGroup; import net.geant.nmaas.portal.persistent.entity.User; +import net.geant.nmaas.portal.persistent.entity.WebhookEventType; import net.geant.nmaas.portal.persistent.repositories.DomainGroupRepository; +import net.geant.nmaas.portal.persistent.repositories.WebhookEventRepository; import net.geant.nmaas.portal.service.ApplicationStatePerDomainService; import net.geant.nmaas.portal.service.DomainGroupService; +import net.geant.nmaas.scheduling.ScheduleManager; import org.apache.commons.lang3.StringUtils; import org.modelmapper.ModelMapper; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -31,6 +38,8 @@ public class DomainGroupServiceImpl implements DomainGroupService { private final DomainGroupRepository domainGroupRepository; private final ApplicationStatePerDomainService applicationStatePerDomainService; + private final WebhookEventRepository webhookEventRepository; + private final ScheduleManager scheduleManager; private final ModelMapper modelMapper; @@ -55,6 +64,10 @@ public class DomainGroupServiceImpl implements DomainGroupService { DomainGroup domainGroupEntity = modelMapper.map(domainGroup, DomainGroup.class); domainGroupEntity.setApplicationStatePerDomain(applicationStatePerDomainList); domainGroupEntity = domainGroupRepository.save(domainGroupEntity); + + //call existing webhooks + DomainGroupView domainGroupView = modelMapper.map(domainGroupEntity, DomainGroupView.class); + webhookEventRepository.findIdByEventType(WebhookEventType.DOMAIN_GROUP_CHANGE).forEach(id -> scheduleManager.createOneTimeJob(DomainGroupJob.class, "DomainGroup_" + id + "_" + domainGroupView.getId()+ "_" + LocalDateTime.now(), Map.of("webhookId", id, "action", "create","domainGroup",domainGroupView))); return modelMapper.map(domainGroupEntity, DomainGroupView.class); } @@ -81,6 +94,7 @@ public class DomainGroupServiceImpl implements DomainGroupService { @Override public void deleteDomainGroup(Long domainGroupId) { DomainGroup domainGroup = domainGroupRepository.findById(domainGroupId).orElseThrow(); + DomainGroupView domainGroupView = modelMapper.map(domainGroup, DomainGroupView.class); List<Domain> toRemove = new ArrayList<>(domainGroup.getDomains()); Iterator<Domain> iterator = toRemove.iterator(); while (iterator.hasNext()) { @@ -90,6 +104,8 @@ public class DomainGroupServiceImpl implements DomainGroupService { iterator.remove(); } domainGroupRepository.deleteById(domainGroupId); + //call existing webhooks + webhookEventRepository.findIdByEventType(WebhookEventType.DOMAIN_GROUP_CHANGE).forEach(id -> scheduleManager.createOneTimeJob(DomainGroupJob.class, "DomainGroup_" + id + "_" + domainGroup.getId()+ "_" + LocalDateTime.now(), Map.of("webhookId", id, "action", "delete","domainGroup",domainGroupView))); } @Override @@ -128,7 +144,11 @@ public class DomainGroupServiceImpl implements DomainGroupService { } domainGroupRepository.save(domainGroup); - return modelMapper.map(domainGroup, DomainGroupView.class); + + //call existing webhooks + DomainGroupView domainGroupView = modelMapper.map(domainGroup, DomainGroupView.class); + webhookEventRepository.findIdByEventType(WebhookEventType.DOMAIN_GROUP_CHANGE).forEach(id -> scheduleManager.createOneTimeJob(DomainGroupJob.class, "DomainGroup_" + id + "_" + domainGroupView.getId()+ "_" + LocalDateTime.now(), Map.of("webhookId", id, "action", "update","domainGroup",domainGroupView))); + return domainGroupView; } protected void checkParam(DomainGroupView domainGroup) { diff --git a/src/test/java/net/geant/nmaas/portal/service/impl/DomainServiceTest.java b/src/test/java/net/geant/nmaas/portal/service/impl/DomainServiceTest.java index 6f9276d7f..ccbc33590 100644 --- a/src/test/java/net/geant/nmaas/portal/service/impl/DomainServiceTest.java +++ b/src/test/java/net/geant/nmaas/portal/service/impl/DomainServiceTest.java @@ -103,7 +103,7 @@ class DomainServiceTest { void setup() { validator = new DefaultCodenameValidator("[a-z-]{2,12}"); namespaceValidator = new DefaultCodenameValidator("[a-z-]{0,64}"); - domainGroupService = new DomainGroupServiceImpl(domainGroupRepository, applicationStatePerDomainService, modelMapper); + domainGroupService = new DomainGroupServiceImpl(domainGroupRepository, applicationStatePerDomainService, webhookEventRepository, scheduleManager, modelMapper); scheduleManager = new ScheduleManager( scheduler); domainService = new DomainServiceImpl(validator, namespaceValidator, domainRepository, -- GitLab From f47b942432ee972b6bb76da77194e56214752ad5 Mon Sep 17 00:00:00 2001 From: cgeorgilakis-grnet <cgeorgilakis@admin.grnet.gr> Date: Thu, 22 May 2025 14:41:57 +0300 Subject: [PATCH 2/2] fixes related to DomainGroup, add tests for DomainGroup --- build.gradle | 1 - .../portal/persistent/entity/DomainGroup.java | 2 +- .../service/impl/DomainGroupServiceTest.java | 144 ++++++++++++++++++ .../service/impl/DomainServiceTest.java | 5 +- 4 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 src/test/java/net/geant/nmaas/portal/service/impl/DomainGroupServiceTest.java diff --git a/build.gradle b/build.gradle index 796507dc6..574373708 100644 --- a/build.gradle +++ b/build.gradle @@ -88,7 +88,6 @@ dependencies { implementation('org.springframework.boot:spring-boot-starter-mail') implementation('org.springframework.boot:spring-boot-starter-data-jpa') implementation('org.springframework.boot:spring-boot-starter-actuator') -// implementation('org.springframework.boot:spring-boot-devtools') implementation('org.springframework.boot:spring-boot-starter-quartz') implementation('org.springframework.boot:spring-boot-starter-validation') implementation 'org.springframework.boot:spring-boot-starter-cache' diff --git a/src/main/java/net/geant/nmaas/portal/persistent/entity/DomainGroup.java b/src/main/java/net/geant/nmaas/portal/persistent/entity/DomainGroup.java index 186f96dcf..74d554474 100644 --- a/src/main/java/net/geant/nmaas/portal/persistent/entity/DomainGroup.java +++ b/src/main/java/net/geant/nmaas/portal/persistent/entity/DomainGroup.java @@ -67,7 +67,7 @@ public class DomainGroup implements Serializable { public DomainGroup(String name, String codename) { super(); this.name = name; - this.codename = name; + this.codename = codename; } public DomainGroup(Long id, String name, String codename) { diff --git a/src/test/java/net/geant/nmaas/portal/service/impl/DomainGroupServiceTest.java b/src/test/java/net/geant/nmaas/portal/service/impl/DomainGroupServiceTest.java new file mode 100644 index 000000000..2b1c86427 --- /dev/null +++ b/src/test/java/net/geant/nmaas/portal/service/impl/DomainGroupServiceTest.java @@ -0,0 +1,144 @@ +package net.geant.nmaas.portal.service.impl; + +import net.geant.nmaas.orchestration.jobs.DomainGroupJob; +import net.geant.nmaas.portal.api.domain.DomainGroupView; +import net.geant.nmaas.portal.persistent.entity.DomainGroup; +import net.geant.nmaas.portal.persistent.entity.WebhookEvent; +import net.geant.nmaas.portal.persistent.entity.WebhookEventType; +import net.geant.nmaas.portal.persistent.repositories.DomainGroupRepository; +import net.geant.nmaas.portal.persistent.repositories.WebhookEventRepository; +import net.geant.nmaas.portal.service.ApplicationStatePerDomainService; +import net.geant.nmaas.portal.service.DomainGroupService; +import net.geant.nmaas.scheduling.ScheduleManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.modelmapper.ModelMapper; +import org.quartz.JobListener; +import org.quartz.ListenerManager; +import org.quartz.Matcher; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; + +import java.util.Optional; +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class DomainGroupServiceTest { + + DomainGroupRepository domainGroupRepository = mock(DomainGroupRepository.class); + ApplicationStatePerDomainService applicationStatePerDomainService = mock(ApplicationStatePerDomainService.class); + WebhookEventRepository webhookEventRepository = mock(WebhookEventRepository.class); + Scheduler scheduler = mock(Scheduler.class); + ListenerManager listenerManager = mock(ListenerManager.class); + ScheduleManager scheduleManager; + ModelMapper modelMapper = new ModelMapper(); + DomainGroupService domainGroupService; + + @BeforeEach + void setup() { + scheduleManager = new ScheduleManager(scheduler); + domainGroupService = new DomainGroupServiceImpl(domainGroupRepository, applicationStatePerDomainService, webhookEventRepository, scheduleManager, modelMapper); + } + + @Test + void shouldCreateDomainGroup() throws SchedulerException { + // Setup webhook event + WebhookEvent webhookEvent = new WebhookEvent(1L, "webhook", "https://example.com/webhook", WebhookEventType.DOMAIN_GROUP_CHANGE, null, null); + when(webhookEventRepository.findIdByEventType(WebhookEventType.DOMAIN_GROUP_CHANGE)) + .thenReturn(Stream.of(1L)); + when(webhookEventRepository.findById(1L)) + .thenReturn(Optional.of(webhookEvent)); + + // Setup domain group + String name = "testgroup"; + String codename = "testgrp"; + DomainGroup domainGroup = new DomainGroup(name, codename); + domainGroup.setId(10L); + when(domainGroupRepository.save(any(DomainGroup.class))).thenReturn(domainGroup); + when(domainGroupRepository.findById(10L)).thenReturn(Optional.of(domainGroup)); + when(scheduler.getListenerManager()).thenReturn(listenerManager); + doNothing().when(listenerManager).addJobListener(any(JobListener.class), any(Matcher.class)); + + // Create domain group + DomainGroupView domainGroupView = new DomainGroupView(); + domainGroupView.setName(name); + domainGroupView.setCodename(codename); + DomainGroupView result = this.domainGroupService.createDomainGroup(domainGroupView); + + // Verify webhook job was scheduled with correct parameters for creation + verify(scheduler, times(1)).scheduleJob( + argThat(jobDetail -> + jobDetail.getKey().getName().startsWith("DomainGroup_1_10_") && + jobDetail.getJobClass().equals(DomainGroupJob.class) + ), + argThat(trigger -> + trigger.getKey().getName().startsWith("DomainGroup_1_10_") + ) + ); + + // Verify domain group was created correctly + assertThat("Codenames are not the same", result.getCodename().equals(codename)); + assertThat("Names are not the same", result.getName().equals(name)); + + // Update domain group + when(webhookEventRepository.findIdByEventType(WebhookEventType.DOMAIN_GROUP_CHANGE)) + .thenReturn(Stream.of(1L)); + when(webhookEventRepository.findById(1L)) + .thenReturn(Optional.of(webhookEvent)); + domainGroupView.setCodename(codename + "2"); + domainGroupView.setId(10L); + result = this.domainGroupService.updateDomainGroup(10L, domainGroupView); + + // Verify webhook job was scheduled with correct parameters for update + verify(scheduler, times(2)).scheduleJob( + argThat(jobDetail -> + jobDetail.getKey().getName().startsWith("DomainGroup_1_10_") && + jobDetail.getJobClass().equals(DomainGroupJob.class) + ), + argThat(trigger -> + trigger.getKey().getName().startsWith("DomainGroup_1_10_") + ) + ); + + // Verify domain group was updated correctly + assertThat("Updated codenames are not the same", result.getCodename().equals(codename + "2")); + assertThat("Names are not the same after update", result.getName().equals(name)); + } + + @Test + void shouldDeleteDomainGroup() throws SchedulerException { + // Setup webhook event + WebhookEvent webhookEvent = new WebhookEvent(1L, "webhook", "https://example.com/webhook", WebhookEventType.DOMAIN_GROUP_CHANGE, null, null); + when(webhookEventRepository.findIdByEventType(WebhookEventType.DOMAIN_GROUP_CHANGE)) + .thenReturn(Stream.of(1L)); + when(webhookEventRepository.findById(1L)) + .thenReturn(Optional.of(webhookEvent)); + + DomainGroup domainGroup = new DomainGroup("testgroup", "testgrp"); + domainGroup.setId(10L); + when(domainGroupRepository.findById(10L)).thenReturn(Optional.of(domainGroup)); + when(scheduler.getListenerManager()).thenReturn(listenerManager); + doNothing().when(listenerManager).addJobListener(any(JobListener.class), any(Matcher.class)); + this.domainGroupService.deleteDomainGroup(10L); + verify(domainGroupRepository, times(1)).deleteById(10L); + + verify(scheduler, times(1)).scheduleJob( + argThat(jobDetail -> + jobDetail.getKey().getName().startsWith("DomainGroup_1_10_") && + jobDetail.getJobClass().equals(DomainGroupJob.class) + ), + argThat(trigger -> + trigger.getKey().getName().startsWith("DomainGroup_1_10_") + ) + ); + } +} diff --git a/src/test/java/net/geant/nmaas/portal/service/impl/DomainServiceTest.java b/src/test/java/net/geant/nmaas/portal/service/impl/DomainServiceTest.java index ccbc33590..5fb8cb4c8 100644 --- a/src/test/java/net/geant/nmaas/portal/service/impl/DomainServiceTest.java +++ b/src/test/java/net/geant/nmaas/portal/service/impl/DomainServiceTest.java @@ -94,8 +94,6 @@ class DomainServiceTest { Scheduler scheduler = mock(Scheduler.class); ListenerManager listenerManager = mock(ListenerManager.class); ScheduleManager scheduleManager; - EncryptionService encryptionService = mock(EncryptionService.class); - WebhookEventService webhookEventService; DomainService domainService; @@ -103,15 +101,14 @@ class DomainServiceTest { void setup() { validator = new DefaultCodenameValidator("[a-z-]{2,12}"); namespaceValidator = new DefaultCodenameValidator("[a-z-]{0,64}"); - domainGroupService = new DomainGroupServiceImpl(domainGroupRepository, applicationStatePerDomainService, webhookEventRepository, scheduleManager, modelMapper); scheduleManager = new ScheduleManager( scheduler); + domainGroupService = new DomainGroupServiceImpl(domainGroupRepository, applicationStatePerDomainService, webhookEventRepository, scheduleManager, modelMapper); domainService = new DomainServiceImpl(validator, namespaceValidator, domainRepository, domainDcnDetailsRepository, domainTechDetailsRepository, userService, userRoleRepo, dcnRepositoryManager, modelMapper, applicationStatePerDomainService, domainGroupService, eventPublisher, domainAnnotationsRepository, webhookEventRepository, scheduleManager); ((DomainServiceImpl) domainService).globalDomain = "GLOBAL"; - webhookEventService = new WebhookEventService(webhookEventRepository, encryptionService, modelMapper); } @Test -- GitLab