Skip to content
Snippets Groups Projects
Commit 04404f8a authored by Lukasz Lopatowski's avatar Lukasz Lopatowski
Browse files

Merge remote-tracking branch 'origin/develop' into...

Merge remote-tracking branch 'origin/develop' into 292-feduc-email-notification-in-case-of-a-cluster-becoming-unavailable
parents 611f5ba1 98239f8c
No related branches found
No related tags found
2 merge requests!273Release 1.8.0 update,!202get user from database if email exist
Pipeline #94317 failed
Showing
with 174 additions and 129 deletions
Copyright 2025 GÉANT Association
Contributions to this work were made on behalf of the GÉANT project, a project that has received funding
from the European Union’s Horizon 2020 research and innovation programme under Grant Agreement No. 731122 (GN4-2)
and Grant Agreement No. 856726 (GN4-3).
from the European Union’s Horizon 2020 research and innovation programme under Grant Agreement No. 731122 (GN4-2),
Grant Agreement No. 856726 (GN4-3) and Grant Agreement No. 101100680 (GN5-1).
On behalf of GÉANT project, GÉANT Association is the sole owner of the copyright in all material
which was developed by the members of the GÉANT project:
On behalf of GÉANT project, GÉANT Association is the sole owner of the copyright in all material which was developed
by the members of the GÉANT project:
- Poznan Supercomputing and Networking Center (PSNC), Poland;
- RENATER, France;
- University of Belgrade (UoB), Serbia;
......
......@@ -2,7 +2,7 @@ plugins {
id 'java'
id 'idea'
id 'jacoco'
id 'org.springframework.boot' version '3.4.5'
id 'org.springframework.boot' version '3.4.6'
id 'io.spring.dependency-management' version '1.1.7'
id 'com.gorylenko.gradle-git-properties' version '2.5.0'
id 'org.sonarqube' version '6.2.0.5505'
......@@ -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'
......@@ -112,7 +111,7 @@ dependencies {
runtimeOnly('org.postgresql:postgresql')
// Database audit trail
implementation 'org.hibernate.orm:hibernate-envers:6.6.15.Final'
implementation 'org.hibernate.orm:hibernate-envers:6.6.16.Final'
// Database migrations
implementation('org.flywaydb:flyway-core:11.8.2')
......@@ -126,7 +125,7 @@ dependencies {
implementation('io.micrometer:micrometer-registry-prometheus:1.15.0')
// Kubernetes API client
implementation('io.fabric8:kubernetes-client:7.3.0')
implementation('io.fabric8:kubernetes-client:7.3.1')
// GitLab API client
implementation 'org.gitlab4j:gitlab4j-api:6.0.0-rc.10' // version 6 for jakarta
......@@ -141,7 +140,7 @@ dependencies {
implementation('com.opencsv:opencsv:5.11')
// Tests
testImplementation('org.mockito:mockito-core:5.17.0')
testImplementation('org.mockito:mockito-core:5.18.0')
testImplementation('org.springframework.boot:spring-boot-starter-test')
integrationTestImplementation('org.springframework.boot:spring-boot-starter-test')
......
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
......
......@@ -39,6 +39,7 @@ public class ClusterConfigView {
.append("}");
return sb.toString();
}
@Getter
@Setter
@AllArgsConstructor
......@@ -126,13 +127,13 @@ public class ClusterConfigView {
public static class UserToken {
private String token;
@JsonProperty("client-certificate-data")
private String client_certificate_data;
private String clientCertificateData;
@JsonProperty("client-key-data")
private String client_key_data;
private String clientKeyData;
@Override
public String toString() {
return "{ token: " + token + ", client certificate data: " + client_certificate_data + ", client key data: " +client_key_data + " }";
return "{ token: " + token + ", client certificate data: " + clientCertificateData + ", client key data: " + clientKeyData + " }";
}
}
}
......@@ -2,23 +2,22 @@ package net.geant.nmaas.externalservices.kubernetes;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.stereotype.Service;
import org.quartz.Job;
@Slf4j
@RequiredArgsConstructor
@Service
@RequiredArgsConstructor
@Slf4j
public class ClusterMonitoringJob implements Job {
private final RemoteClusterManager remoteClusterManager;
private final ClusterMonitoringService clusterMonitoringService;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("Triggering cluster health check...");
remoteClusterManager.restoreFileIfMissing();
log.info("File checked, everything looks fine. Next stage: Update clusters state.");
remoteClusterManager.updateAllClusterState();
clusterMonitoringService.updateAllClusterState();
}
}
package net.geant.nmaas.externalservices.kubernetes;
public interface ClusterMonitoringService {
void updateAllClusterState();
}
......@@ -38,8 +38,8 @@ 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.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
......@@ -49,9 +49,7 @@ import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class RemoteClusterManager {
private final static ModelMapper modelMapper = new ModelMapper();
public class RemoteClusterManager implements ClusterMonitoringService {
private final KClusterRepository clusterRepository;
private final KubernetesClusterIngressManager kClusterIngressManager;
......@@ -59,7 +57,7 @@ public class RemoteClusterManager {
private final DomainService domainService;
private final ApplicationEventPublisher eventPublisher;
private final UserService userService;
private final ModelMapper modelMapper;
public RemoteClusterView getClusterView(Long id) {
Optional<KCluster> cluster = clusterRepository.findById(id);
......@@ -72,7 +70,7 @@ public class RemoteClusterManager {
public List<RemoteClusterView> getAllClusterView() {
List<KCluster> clusters = clusterRepository.findAll();
return clusters.stream().map(RemoteClusterManager::toView).collect(Collectors.toList());
return clusters.stream().map(this::toView).collect(Collectors.toList());
}
public KCluster getCluster(Long id) {
......@@ -141,11 +139,11 @@ public class RemoteClusterManager {
.state(KClusterState.UNKNOWN)
.contactEmail(view.getContactEmail())
.currentStateSince(OffsetDateTime.now())
.domains(view.getDomainNames().stream().map(d -> {
.domains(!view.getDomainNames().isEmpty() ? view.getDomainNames().stream().map(d -> {
Optional<Domain> dom = domainService.findDomain(d);
return dom.orElse(null);
}
).toList())
).toList() : Collections.emptyList())
.build(),
file);
......@@ -153,9 +151,7 @@ public class RemoteClusterManager {
log.error("More than 1 cluster provided, not implemented yet");
}
} catch (IOException e) {
throw new RuntimeException(e);
} catch (NoSuchAlgorithmException e) {
} catch (IOException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
......@@ -189,7 +185,6 @@ public class RemoteClusterManager {
updated = clusterRepository.save(updated);
//TODO : implement file update logic
return toView(updated);
}
}
......@@ -239,25 +234,27 @@ public class RemoteClusterManager {
if (view.getCodename() == null) {
throw new IllegalArgumentException("Codename of the cluster is null");
}
}
public static RemoteClusterView toView(KCluster KCluster) {
private RemoteClusterView toView(KCluster KCluster) {
RemoteClusterView view = modelMapper.map(KCluster, RemoteClusterView.class);
view.setDomainNames(KCluster.getDomains().stream().map(Domain::getName).toList());
return view;
}
@Override
public void updateAllClusterState() {
restoreFileIfMissing();
List<KCluster> kClusters = clusterRepository.findAll();
kClusters.forEach(cluster -> {
Config config = null;
try {
config = Config.fromKubeconfig(Files.readString(Path.of(cluster.getPathConfigFile())));
} catch (IOException e) {
log.error("IO error with accesing the file {}", e.getMessage());
} catch (IOException e) {
log.error("IO error with accessing 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());
......@@ -268,12 +265,9 @@ public class RemoteClusterManager {
log.error("Can not connect to cluster {}", cluster.getCodename());
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);
} catch (Exception ex) {
log.error("Caught unexpected exception: {}", ex.getMessage(), ex);
}
......@@ -282,14 +276,13 @@ public class RemoteClusterManager {
clusterRepository.saveAll(kClusters);
}
private void updateStateIfNeeded(KCluster cluster, KClusterState newState) {
if (!cluster.getState().equals(newState)) {
cluster.setState(newState);
cluster.setCurrentStateSince(OffsetDateTime.now());
if(cluster.getState().equals(KClusterState.DOWN) || cluster.getState().equals(KClusterState.UNKNOWN)) {
if (cluster.getState().equals(KClusterState.DOWN) || cluster.getState().equals(KClusterState.UNKNOWN)) {
sendMail(cluster, MailType.REMOTE_CLUSTER_UNAVAILABLE);
};
}
}
}
......@@ -312,7 +305,6 @@ public class RemoteClusterManager {
.build();
this.eventPublisher.publishEvent(new NotificationEvent(this, mailAttributes));
}
public void restoreFileIfMissing() {
......@@ -334,9 +326,9 @@ public class RemoteClusterManager {
});
}
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
package net.geant.nmaas.monitor;
import lombok.extern.slf4j.Slf4j;
import net.geant.nmaas.monitor.model.MonitorEntryView;
import net.geant.nmaas.scheduling.ScheduleManager;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@Configuration
@Slf4j
public class MonitorConfig {
@Bean
public InitializingBean insertDefaultMonitoringJobs() {
return new InitializingBean() {
@Autowired
private List<MonitorService> monitorServices;
@Autowired
private ScheduleManager scheduleManager;
@Autowired
private MonitorManager monitorManager;
@Override
@Transactional
public void afterPropertiesSet() {
Arrays.stream(ServiceType.values())
.filter(serviceType -> !scheduleManager.jobExists(serviceType.toString())) // if job does not exist
.forEach(serviceType -> {
MonitorEntryView monitorEntry;
if (monitorManager.existsByServiceName(serviceType)) { // if entry exists
monitorEntry = monitorManager.getMonitorEntries(serviceType.toString()); // read it from database
} else {
monitorEntry = serviceType.getDefaultMonitorEntry(); // if entry does not exist
monitorManager.createMonitorEntry(monitorEntry); // create new default entry
}
Optional<MonitorService> service = monitorServices.stream()
.filter(s -> s.getServiceType().equals(serviceType))
.filter(MonitorService::schedulable)
.findFirst();
if (service.isPresent()) {
scheduleManager.createJob(service.get(), monitorEntry);
} else {
log.warn("Monitor service for {} not found or is not schedulable", serviceType);
}
});
}
};
}
}
......@@ -23,7 +23,7 @@ public abstract class MonitorService implements Job {
this.monitorManager.updateMonitorEntry(new Date(), this.getServiceType(), status);
}
protected boolean schedulable() {
public boolean schedulable() {
return true;
}
......
......@@ -9,11 +9,11 @@ import net.geant.nmaas.nmservice.deployment.entities.NmServiceInfo;
import net.geant.nmaas.nmservice.deployment.repository.NmServiceInfoRepository;
import net.geant.nmaas.orchestration.Identifier;
import net.geant.nmaas.orchestration.exceptions.InvalidDeploymentIdException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
......@@ -21,12 +21,14 @@ import java.util.Optional;
@Slf4j
public abstract class NmServiceRepositoryManager<T extends NmServiceInfo> {
@Autowired
protected GitLabProjectRepository gitLabProjectRepository;
@Autowired
protected NmServiceInfoRepository<T> repository;
public NmServiceRepositoryManager(GitLabProjectRepository gitLabProjectRepository, NmServiceInfoRepository<T> repository) {
this.gitLabProjectRepository = gitLabProjectRepository;
this.repository = repository;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void storeService(T serviceInfo) {
if (repository.findByDeploymentId(serviceInfo.getDeploymentId()).isEmpty()) {
......@@ -69,8 +71,7 @@ public abstract class NmServiceRepositoryManager<T extends NmServiceInfo> {
try {
updateServiceState(event.getDeploymentId(), event.getState());
} catch (Exception ex) {
long timestamp = System.currentTimeMillis();
log.error("Error reported at {}", timestamp, ex);
log.error("Error reported at {}", LocalDateTime.now(), ex);
}
}
......
package net.geant.nmaas.nmservice.deployment.containerorchestrators.kubernetes;
import net.geant.nmaas.nmservice.configuration.repositories.GitLabProjectRepository;
import net.geant.nmaas.nmservice.deployment.NmServiceRepositoryManager;
import net.geant.nmaas.nmservice.deployment.containerorchestrators.kubernetes.entities.KubernetesNmServiceInfo;
import net.geant.nmaas.nmservice.deployment.containerorchestrators.kubernetes.entities.ServiceAccessMethod;
import net.geant.nmaas.nmservice.deployment.containerorchestrators.kubernetes.entities.ServiceStorageVolume;
import net.geant.nmaas.nmservice.deployment.containerorchestrators.kubernetes.repositories.ServiceAccessMethodRepository;
import net.geant.nmaas.nmservice.deployment.containerorchestrators.kubernetes.repositories.ServiceStorageVolumeRepository;
import net.geant.nmaas.nmservice.deployment.repository.NmServiceInfoRepository;
import net.geant.nmaas.orchestration.Identifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
......@@ -25,11 +27,15 @@ import static net.geant.nmaas.nmservice.deployment.containerorchestrators.kubern
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
public class KubernetesRepositoryManager extends NmServiceRepositoryManager<KubernetesNmServiceInfo> {
@Autowired
private ServiceStorageVolumeRepository storageVolumeRepository;
private final ServiceStorageVolumeRepository storageVolumeRepository;
private final ServiceAccessMethodRepository accessMethodRepository;
@Autowired
private ServiceAccessMethodRepository accessMethodRepository;
public KubernetesRepositoryManager(GitLabProjectRepository gitLabProjectRepository, NmServiceInfoRepository<KubernetesNmServiceInfo> repository, ServiceStorageVolumeRepository storageVolumeRepository, ServiceAccessMethodRepository accessMethodRepository) {
super(gitLabProjectRepository, repository);
this.storageVolumeRepository = storageVolumeRepository;
this.accessMethodRepository = accessMethodRepository;
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
......
package net.geant.nmaas.nmservice.deployment.containerorchestrators.kubernetes.components.helm;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.geant.nmaas.nmservice.deployment.containerorchestrators.kubernetes.components.helm.commands.HelmDeleteCommand;
import net.geant.nmaas.nmservice.deployment.containerorchestrators.kubernetes.components.helm.commands.HelmInstallCommand;
......@@ -23,23 +24,28 @@ import java.util.Map;
import static net.geant.nmaas.nmservice.deployment.containerorchestrators.kubernetes.components.helm.HelmCommand.HELM_VERSION_2;
@NoArgsConstructor
@Component
public class HelmCommandExecutor {
@Autowired
private CommandExecutor commandExecutor;
@Setter
@Value("${helm.version:v3}")
private String helmVersion;
@Setter
@Value("${helm.repositoryName}")
private String helmRepositoryName;
@Value("${helm.enableTls:false}")
private Boolean enableTls;
@Autowired
public HelmCommandExecutor(CommandExecutor commandExecutor,
@Value("${helm.version:v3}") String helmVersion,
@Value("${helm.repositoryName}") String helmRepositoryName,
@Value("${helm.enableTls:false}") Boolean enableTls) {
this.commandExecutor = commandExecutor;
this.helmVersion = helmVersion;
this.helmRepositoryName = helmRepositoryName;
this.enableTls = enableTls;
}
void executeHelmInstallCommand(String namespace, String releaseName, KubernetesTemplate template, Map<String, String> arguments) {
executeInstall(namespace, releaseName, template, arguments);
}
......
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");
}
}
}
......@@ -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;
......
......@@ -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;
......
......@@ -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;
......
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;
}
......@@ -25,7 +25,7 @@ public class WebhookEventDto {
@Pattern(regexp = "^(Authorization|X-.*)?$", message = "Authorization header must be either 'Authorization' or start with 'X-'")
private String authorizationHeader;
public WebhookEventDto (Long id, String name, String targetUrl, WebhookEventType eventType){
public WebhookEventDto(Long id, String name, String targetUrl, WebhookEventType eventType) {
this.id = id;
this.name = name;
this.targetUrl = targetUrl;
......
......@@ -5,11 +5,15 @@ import lombok.extern.slf4j.Slf4j;
import net.geant.nmaas.portal.api.domain.ContentView;
import net.geant.nmaas.portal.persistent.entity.Content;
import net.geant.nmaas.portal.service.DashboardService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.OffsetDateTime;
@RestController
@AllArgsConstructor
@RequestMapping("/api/dashboard")
......@@ -19,8 +23,11 @@ public class DashboardController {
private DashboardService dashboardService;
@GetMapping("/admin")
public DashboardView getDashboardAdmin() {
DashboardView view = dashboardService.getSystemDashboard();
@PreAuthorize("hasRole('ROLE_SYSTEM_ADMIN')")
public DashboardView getDashboardAdmin( @RequestParam("startDate") OffsetDateTime startDate,
@RequestParam("end") OffsetDateTime endDate) {
checkDate(startDate,endDate);
DashboardView view = dashboardService.getSystemDashboard(startDate,endDate);
log.error("View : {}", view.toString());
return view;
......@@ -32,4 +39,14 @@ public class DashboardController {
log.error("View : {}", view.toString());
return view;
}
private void checkDate(OffsetDateTime startDate, OffsetDateTime endDate) {
if(startDate == null || endDate == null) {
throw new IllegalArgumentException("Start date can not be null");
}
if(startDate.isAfter(endDate)) {
throw new IllegalArgumentException("Start date is after end date.");
}
}
}
......@@ -57,6 +57,7 @@ import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.lang.reflect.Field;
......@@ -252,7 +253,9 @@ public class AppInstanceController extends AppBaseController {
@Transactional
public Id createAppInstance(@RequestBody AppInstanceRequest appInstanceRequest,
@NotNull Principal principal,
@PathVariable Long domainId) {
@PathVariable Long domainId,
@RequestParam(name = "clusterId",required = false) Long clusterId) {
log.error("Cluster = {}", clusterId);
Application app = getApp(appInstanceRequest.getApplicationId());
Domain domain = domainService.findDomain(domainId)
.orElseThrow(() -> new MissingElementException("Domain not found"));
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment