diff --git a/build.gradle b/build.gradle index 50fa4f247683f3442bb8d8f1aca9e6c6f00c0b74..a20cb0792087167795f0e9cc3816c03eb44214ab 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/orchestration/jobs/DomainGroupJob.java b/src/main/java/net/geant/nmaas/orchestration/jobs/DomainGroupJob.java new file mode 100644 index 0000000000000000000000000000000000000000..7fe42451ebba65a1061e0981e789e9c0d823826a --- /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 7ed46a20911b349308d0eebcdb3bbd0b93c36dcd..ccce6b28f866030a0897efc855ff62458a58f250 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 9bfd26a5b81a42a51a4c4dfe7c763ec4214a4e22..cf77585ec84652b6db6cbc25f83f9b9a8fe00981 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 2e40d9005a04d0013f0e5867c3fffcd1ac2a7fed..a5855816dd63f59af5c3088119d4c7230cbe539a 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 0000000000000000000000000000000000000000..75685180793fe87af636eabd3623888851ca0aeb --- /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/persistent/entity/DomainGroup.java b/src/main/java/net/geant/nmaas/portal/persistent/entity/DomainGroup.java index 186f96dcf0397919d061d6c389ae6f42e4bfb751..74d554474081b8ed4233bba8c9d4b2ae03ac7669 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/main/java/net/geant/nmaas/portal/service/impl/DomainGroupServiceImpl.java b/src/main/java/net/geant/nmaas/portal/service/impl/DomainGroupServiceImpl.java index 3dbc1baf929b1624f7abf0b246166ea6966ce1e9..2823c728ce858495ce6f708d8968d072848e1503 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/DomainGroupServiceTest.java b/src/test/java/net/geant/nmaas/portal/service/impl/DomainGroupServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2b1c864275d91a098134dccf3cf1d81e22f73751 --- /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 6f9276d7ff38e95d21e9293a6cd9cad9f5308adf..5fb8cb4c800f2519d943b89133014c20531a2147 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, 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