diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java index 6efb7e7b3d..5fd1ebb8c5 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java @@ -158,6 +158,9 @@ public final class Messages { public static final String FLOWABLE_JOB_EXECUTOR_CORE_THREADS = "Flowable job executor core threads: {0}"; public static final String FLOWABLE_JOB_EXECUTOR_MAX_THREADS = "Flowable job executor max threads: {0}"; public static final String FLOWABLE_JOB_EXECUTOR_QUEUE_CAPACITY = "Flowable job executor queue capacity: {0}"; + public static final String CLOUD_LOGGING_SERVICE_EXECUTOR_CORE_THREADS = "Cloud logging service executor core threads: {0}"; + public static final String CLOUD_LOGGING_SERVICE_EXECUTOR_MAX_THREADS = "Cloud logging service executor max threads: {0}"; + public static final String CLOUD_LOGGING_SERVICE_EXECUTOR_QUEUE_CAPACITY = "Cloud logging service executor queue capacity: {0}"; public static final String GLOBAL_AUDITOR_ORIGIN = "Global auditor user origin: {0}"; public static final String AUDIT_LOG_ABOUT_TO_PERFORM_ACTION = "About to perform action \"{0}\""; @@ -287,6 +290,17 @@ public final class Messages { public static final String ENTRY_CREATE_AUDIT_LOG_CONFIG = "Configuration entry create"; public static final String ENTRY_UPDATE_AUDIT_LOG_CONFIG = "Configuration entry update"; + public static final String LOGGING_CONFIGURATION_CREATE = "Create cloud-logging-configuration in space with id: {0}"; + public static final String LOGGING_CONFIGURATION_UPDATE = "Update cloud-logging-configuration in space with id: {0}"; + public static final String LOGGING_CONFIGURATION_DELETE = "Delete cloud-logging-configuration in space with id: {0}"; + public static final String LOGGING_CONFIGURATION_GET = "Get cloud-logging-configuration in space with id: {0}"; + public static final String LOGGING_CONFIGURATION_LIST = "List cloud-logging-configurations in space with id: {0}"; + public static final String LOGGING_CONFIGURATION_CREATE_AUDIT_LOG_CONFIG = "Cloud logging configuration create"; + public static final String LOGGING_CONFIGURATION_UPDATE_AUDIT_LOG_CONFIG = "Cloud logging configuration update"; + public static final String LOGGING_CONFIGURATION_DELETE_AUDIT_LOG_CONFIG = "Cloud logging configuration delete"; + public static final String LOGGING_CONFIGURATION_GET_AUDIT_LOG_CONFIG = "Cloud logging configuration get"; + public static final String LOGGING_CONFIGURATION_LIST_AUDIT_LOG_CONFIG = "Cloud logging configuration list"; + public static final String API_INFO_AUDIT_LOG_CONFIG = "Api info"; public static final String IGNORING_NAMESPACE_PARAMETERS = "Ignoring parameter \"{0}\" , as the MTA is not deployed with namespace!"; public static final String NAMESPACE_PARSING_ERROR_MESSAGE = "Cannot parse \"{0}\" flag - expected a boolean format."; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/AuditLogBean.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/AuditLogBean.java index 755f5a13f4..830d3212eb 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/AuditLogBean.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/AuditLogBean.java @@ -58,4 +58,9 @@ public ConfigurationSubscriptionServiceAuditLog buildAConfigurationSubscriptionS public ConfigurationEntryServiceAuditLog buildAConfigurationEntryServiceAuditLog(AuditLoggingFacade auditLoggingFacade) { return new ConfigurationEntryServiceAuditLog(auditLoggingFacade); } + + @Bean + public CloudLoggingServiceConfigurationAuditLog buildCloudLoggingServiceConfigurationAuditLog(AuditLoggingFacade auditLoggingFacade) { + return new CloudLoggingServiceConfigurationAuditLog(auditLoggingFacade); + } } diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLog.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLog.java new file mode 100644 index 0000000000..9d7c9a28eb --- /dev/null +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLog.java @@ -0,0 +1,89 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.cloudfoundry.multiapps.controller.core.Messages; +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; + +public class CloudLoggingServiceConfigurationAuditLog { + + private static final String ID_PROPERTY_NAME = "id"; + private static final String MTA_ID_PROPERTY_NAME = "mtaId"; + private static final String MTA_SPACE_PROPERTY_NAME = "mtaSpace"; + private static final String MTA_SPACE_ID_PROPERTY_NAME = "mtaSpaceId"; + private static final String MTA_ORG_PROPERTY_NAME = "mtaOrg"; + private static final String NAMESPACE_PROPERTY_NAME = "namespace"; + private static final String TARGET_SPACE_PROPERTY_NAME = "targetSpace"; + private static final String TARGET_ORG_PROPERTY_NAME = "targetOrg"; + private static final String SERVICE_INSTANCE_NAME_PROPERTY_NAME = "serviceInstanceName"; + private static final String SERVICE_KEY_NAME_PROPERTY_NAME = "serviceKeyName"; + private static final String LOG_LEVEL_PROPERTY_NAME = "logLevel"; + private static final String IS_FAILSAFE_PROPERTY_NAME = "isFailSafe"; + + private final AuditLoggingFacade auditLoggingFacade; + + public CloudLoggingServiceConfigurationAuditLog(AuditLoggingFacade auditLoggingFacade) { + this.auditLoggingFacade = auditLoggingFacade; + } + + public void logCreateLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration) { + String performedAction = MessageFormat.format(Messages.LOGGING_CONFIGURATION_CREATE, spaceId); + auditLoggingFacade.logConfigurationChangeAuditLog(new AuditLogConfiguration(username, + spaceId, + performedAction, + Messages.LOGGING_CONFIGURATION_CREATE_AUDIT_LOG_CONFIG, + buildIdentifiers(loggingConfiguration)), + ConfigurationChangeActions.CONFIGURATION_CREATE); + } + + public void logUpdateLoggingConfiguration(String username, String spaceId, LoggingConfiguration newConfiguration) { + String performedAction = MessageFormat.format(Messages.LOGGING_CONFIGURATION_UPDATE, spaceId); + auditLoggingFacade.logConfigurationChangeAuditLog(new AuditLogConfiguration(username, + spaceId, + performedAction, + Messages.LOGGING_CONFIGURATION_UPDATE_AUDIT_LOG_CONFIG, + buildIdentifiers(newConfiguration)), + ConfigurationChangeActions.CONFIGURATION_UPDATE); + } + + public void logDeleteLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration) { + String performedAction = MessageFormat.format(Messages.LOGGING_CONFIGURATION_DELETE, spaceId); + auditLoggingFacade.logConfigurationChangeAuditLog(new AuditLogConfiguration(username, + spaceId, + performedAction, + Messages.LOGGING_CONFIGURATION_DELETE_AUDIT_LOG_CONFIG, + buildIdentifiers(loggingConfiguration)), + ConfigurationChangeActions.CONFIGURATION_DELETE); + } + + public void logGetLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration) { + String performedAction = MessageFormat.format(Messages.LOGGING_CONFIGURATION_GET, spaceId); + auditLoggingFacade.logDataAccessAuditLog(new AuditLogConfiguration(username, + spaceId, + performedAction, + Messages.LOGGING_CONFIGURATION_GET_AUDIT_LOG_CONFIG, + buildIdentifiers(loggingConfiguration))); + } + + private Map buildIdentifiers(LoggingConfiguration loggingConfiguration) { + Map identifiers = new HashMap<>(); + identifiers.put(ID_PROPERTY_NAME, loggingConfiguration.getId()); + identifiers.put(MTA_ID_PROPERTY_NAME, loggingConfiguration.getMtaId()); + identifiers.put(MTA_SPACE_PROPERTY_NAME, loggingConfiguration.getMtaSpace()); + identifiers.put(MTA_SPACE_ID_PROPERTY_NAME, loggingConfiguration.getMtaSpaceId()); + identifiers.put(MTA_ORG_PROPERTY_NAME, loggingConfiguration.getMtaOrg()); + identifiers.put(NAMESPACE_PROPERTY_NAME, loggingConfiguration.getNamespace()); + identifiers.put(TARGET_SPACE_PROPERTY_NAME, loggingConfiguration.getTargetSpace()); + identifiers.put(TARGET_ORG_PROPERTY_NAME, loggingConfiguration.getTargetOrg()); + identifiers.put(SERVICE_INSTANCE_NAME_PROPERTY_NAME, loggingConfiguration.getServiceInstanceName()); + identifiers.put(SERVICE_KEY_NAME_PROPERTY_NAME, loggingConfiguration.getServiceKeyName()); + identifiers.put(LOG_LEVEL_PROPERTY_NAME, Objects.toString(loggingConfiguration.getLogLevel())); + identifiers.put(IS_FAILSAFE_PROPERTY_NAME, Objects.toString(loggingConfiguration.isFailSafe())); + return identifiers; + } +} diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ResourceType.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ResourceType.java index 0846c93b82..00ad4686df 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ResourceType.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ResourceType.java @@ -9,7 +9,8 @@ public enum ResourceType { MANAGED_SERVICE("managed-service", SupportedParameters.SERVICE, SupportedParameters.SERVICE_PLAN), USER_PROVIDED_SERVICE( - "user-provided-service"), EXISTING_SERVICE("existing-service"), EXISTING_SERVICE_KEY("existing-service-key"); + "user-provided-service"), EXISTING_SERVICE("existing-service"), EXISTING_SERVICE_KEY("existing-service-key"), + CLOUD_LOGGING_SERVICE("cloud-logging-service"); private final String name; private final Set requiredParameters = new HashSet<>(); @@ -33,7 +34,7 @@ public static ResourceType get(String value) { } public static Set getServiceTypes() { - return EnumSet.of(MANAGED_SERVICE, USER_PROVIDED_SERVICE, EXISTING_SERVICE); + return EnumSet.of(MANAGED_SERVICE, USER_PROVIDED_SERVICE, EXISTING_SERVICE, CLOUD_LOGGING_SERVICE); } public Set getRequiredParameters() { diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurger.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurger.java index 060abf9fd4..89512f688b 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurger.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurger.java @@ -12,6 +12,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudSpaceClient; import org.cloudfoundry.multiapps.controller.core.Messages; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; import org.cloudfoundry.multiapps.controller.core.auditlogging.MtaConfigurationPurgerAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadata; import org.cloudfoundry.multiapps.controller.core.cf.metadata.processor.MtaMetadataParser; @@ -20,6 +21,8 @@ import org.cloudfoundry.multiapps.controller.persistence.model.CloudTarget; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationSubscription; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationEntryService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationSubscriptionService; import org.slf4j.Logger; @@ -37,25 +40,32 @@ public class MtaConfigurationPurger { private final ConfigurationEntryService configurationEntryService; private final ConfigurationSubscriptionService configurationSubscriptionService; private MtaMetadataParser mtaMetadataParser; + private CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + private CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; public MtaConfigurationPurger(CloudControllerClient client, CloudSpaceClient spaceClient, ConfigurationEntryService configurationEntryService, ConfigurationSubscriptionService configurationSubscriptionService, MtaMetadataParser mtaMetadataParser, - MtaConfigurationPurgerAuditLog mtaConfigurationPurgerAuditLog) { + MtaConfigurationPurgerAuditLog mtaConfigurationPurgerAuditLog, + CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService, + CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog) { this.client = client; this.spaceClient = spaceClient; this.configurationEntryService = configurationEntryService; this.configurationSubscriptionService = configurationSubscriptionService; this.mtaMetadataParser = mtaMetadataParser; this.mtaConfigurationPurgerAuditLog = mtaConfigurationPurgerAuditLog; + this.cloudLoggingServiceConfigurationService = cloudLoggingServiceConfigurationService; + this.cloudLoggingServiceConfigurationAuditLog = cloudLoggingServiceConfigurationAuditLog; } - public void purge(String org, String space) { + public void purge(String org, String space, String userName) { CloudTarget targetSpace = new CloudTarget(org, space); String targetId = new ClientHelper(spaceClient).computeSpaceId(org, space); List existingApps = getExistingApps(); purgeConfigurationSubscriptions(targetId, existingApps); purgeConfigurationEntries(targetSpace, existingApps, targetId); + purgeCloudLoggingServiceConfigurations(targetId, userName); } private void purgeConfigurationSubscriptions(String spaceId, List existingApps) { @@ -96,6 +106,15 @@ private void purgeConfigurationEntries(CloudTarget targetSpace, List loggingConfigurations = cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace( + spaceId); + for (LoggingConfiguration loggingConfiguration : loggingConfigurations) { + cloudLoggingServiceConfigurationService.deleteCloudLoggingServiceConfiguration(loggingConfiguration.getId()); + cloudLoggingServiceConfigurationAuditLog.logDeleteLoggingConfiguration(userName, spaceId, loggingConfiguration); + } + } + private boolean isStillRelevant(List stillRelevantEntries, ConfigurationEntry entry) { return stillRelevantEntries.stream() .anyMatch(currentEntry -> haveSameProviderIdAndVersion(currentEntry, entry)); diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/ExternalLoggingServiceConfiguration.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/ExternalLoggingServiceConfiguration.java new file mode 100644 index 0000000000..7da95fb789 --- /dev/null +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/ExternalLoggingServiceConfiguration.java @@ -0,0 +1,51 @@ +package org.cloudfoundry.multiapps.controller.core.model; + +import java.util.List; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cloudfoundry.multiapps.common.Nullable; +import org.immutables.value.Value; + +@Value.Immutable +@JsonSerialize(as = ImmutableExternalLoggingServiceConfiguration.class) +@JsonDeserialize(as = ImmutableExternalLoggingServiceConfiguration.class) +public interface ExternalLoggingServiceConfiguration { + + @Nullable + String getServiceInstanceName(); + + @Nullable + String getServiceKeyName(); + + @Nullable + String getTargetOrg(); + + @Nullable + String getTargetSpace(); + + @Nullable + String getOperationId(); + + @Nullable + String getEndpointUrl(); + + @Nullable + String getServerCa(); + + @Nullable + String getClientCert(); + + @Nullable + String getClientKey(); + + @Value.Default + default List getLogLevels() { + return List.of(); + } + + @Value.Default + default Boolean isFailSafe() { + return false; + } +} diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java index 1e84c0acd4..e7efd2c8ba 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java @@ -158,6 +158,10 @@ public class SupportedParameters { public static final String FAIL_ON_SERVICE_UPDATE = "fail-on-service-update"; public static final String SYSLOG_DRAIN_URL = "syslog-drain-url"; public static final String SERVICE_GUID = "service-guid"; + public static final String LOG_LEVEL = "log-level"; + public static final String DESTINATION = "destination"; + public static final String CLS_SPACE_NAME = "space-name"; + public static final String CLS_ORG_NAME = "org-name"; // Configuration reference (new syntax): public static final String PROVIDER_NID = "provider-nid"; @@ -210,7 +214,8 @@ public class SupportedParameters { SERVICE_KEY_NAME, SERVICE_NAME, SERVICE_PLAN, SERVICE_TAGS, SERVICE_BROKER, SKIP_SERVICE_UPDATES, TYPE, PROVIDER_ID, PROVIDER_NID, TARGET, SERVICE_CONFIG_PATH, FILTER, MANAGED, VERSION, PATH, MEMORY, - FAIL_ON_SERVICE_UPDATE, SERVICE_PROVIDER, SERVICE_VERSION); + FAIL_ON_SERVICE_UPDATE, SERVICE_PROVIDER, SERVICE_VERSION, LOG_LEVEL, + DESTINATION, CLS_ORG_NAME, CLS_SPACE_NAME); public static final Set GLOBAL_PARAMETERS = Set.of(KEEP_EXISTING_ROUTES, APPS_UPLOAD_TIMEOUT, APPS_TASK_EXECUTION_TIMEOUT, APPS_START_TIMEOUT, APPS_STAGE_TIMEOUT, APPLY_NAMESPACE, ENABLE_PARALLEL_DEPLOYMENTS, DEPLOY_MODE, BG_DEPENDENCY_AWARE_STOP_ORDER); diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationService.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationService.java index ffda6a00e0..bfa71068f0 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationService.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationService.java @@ -6,12 +6,12 @@ import java.time.format.DateTimeFormatter; import java.util.List; -import jakarta.inject.Inject; import jakarta.inject.Named; import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.controller.api.model.Operation; import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; import org.cloudfoundry.multiapps.controller.core.Messages; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; import org.cloudfoundry.multiapps.controller.core.auditlogging.MtaConfigurationPurgerAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.clients.CFOptimizedEventGetter; import org.cloudfoundry.multiapps.controller.core.cf.clients.WebClientFactory; @@ -21,6 +21,8 @@ import org.cloudfoundry.multiapps.controller.persistence.dto.BackupDescriptor; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationSubscription; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationEntryService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationSubscriptionService; import org.cloudfoundry.multiapps.controller.persistence.services.DescriptorBackupService; @@ -43,22 +45,36 @@ public class DataTerminationService { // https://v3-apidocs.cloudfoundry.org/version/3.128.0/index.html#timestamps private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"); - @Inject - private ConfigurationEntryService configurationEntryService; - @Inject - private ConfigurationSubscriptionService configurationSubscriptionService; - @Inject - private OperationService operationService; - @Inject - private FileService fileService; - @Inject - private ApplicationConfiguration configuration; - @Inject - private WebClientFactory webClientFactory; - @Inject - private MtaConfigurationPurgerAuditLog mtaConfigurationPurgerAuditLog; - @Inject - private DescriptorBackupService descriptorBackupService; + private final ConfigurationEntryService configurationEntryService; + private final ConfigurationSubscriptionService configurationSubscriptionService; + private final OperationService operationService; + private final FileService fileService; + private final ApplicationConfiguration configuration; + private final WebClientFactory webClientFactory; + private final MtaConfigurationPurgerAuditLog mtaConfigurationPurgerAuditLog; + private final DescriptorBackupService descriptorBackupService; + private final CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + private final CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; + + public DataTerminationService(ConfigurationEntryService configurationEntryService, + ConfigurationSubscriptionService configurationSubscriptionService, OperationService operationService, + FileService fileService, ApplicationConfiguration configuration, WebClientFactory webClientFactory, + MtaConfigurationPurgerAuditLog mtaConfigurationPurgerAuditLog, + DescriptorBackupService descriptorBackupService, + CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService, + CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog) { + this.configurationEntryService = configurationEntryService; + this.configurationSubscriptionService = configurationSubscriptionService; + this.operationService = operationService; + this.fileService = fileService; + this.configuration = configuration; + this.webClientFactory = webClientFactory; + this.mtaConfigurationPurgerAuditLog = mtaConfigurationPurgerAuditLog; + this.descriptorBackupService = descriptorBackupService; + this.cloudLoggingServiceConfigurationService = cloudLoggingServiceConfigurationService; + this.cloudLoggingServiceConfigurationAuditLog = cloudLoggingServiceConfigurationAuditLog; + + } private static void log(Exception e) { LOGGER.error(format(Messages.ERROR_DURING_DATA_TERMINATION_0, e.getMessage()), e); @@ -72,6 +88,7 @@ public void deleteOrphanUserData() { SAFE_EXECUTOR.execute(() -> deleteConfigurationEntryOrphanData(spaceId)); SAFE_EXECUTOR.execute(() -> deleteUserOperationsOrphanData(spaceId)); SAFE_EXECUTOR.execute(() -> deletedMtaDescriptorsOrphanData(spaceId)); + SAFE_EXECUTOR.execute(() -> deleteExistingCloudLoggingServiceConfiguration(spaceId)); } if (!spaceEventsToBeDeleted.isEmpty()) { SAFE_EXECUTOR.execute(() -> deleteSpaceIdsLeftovers(spaceEventsToBeDeleted)); @@ -161,4 +178,12 @@ private void deleteSpaceIdsLeftovers(List spaceIds) { } } + private void deleteExistingCloudLoggingServiceConfiguration(String spaceId) { + List loggingConfigurations = cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace( + spaceId); + for (LoggingConfiguration loggingConfiguration : loggingConfigurations) { + cloudLoggingServiceConfigurationService.deleteCloudLoggingServiceConfiguration(loggingConfiguration.getId()); + cloudLoggingServiceConfigurationAuditLog.logDeleteLoggingConfiguration("", spaceId, loggingConfiguration); + } + } } \ No newline at end of file diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/util/ApplicationConfiguration.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/util/ApplicationConfiguration.java index 0af38e4b41..fbdaca6cf8 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/util/ApplicationConfiguration.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/util/ApplicationConfiguration.java @@ -75,6 +75,9 @@ public class ApplicationConfiguration { static final String CFG_FLOWABLE_JOB_EXECUTOR_CORE_THREADS = "FLOWABLE_JOB_EXECUTOR_CORE_THREADS"; static final String CFG_FLOWABLE_JOB_EXECUTOR_MAX_THREADS = "FLOWABLE_JOB_EXECUTOR_MAX_THREADS"; static final String CFG_FLOWABLE_JOB_EXECUTOR_QUEUE_CAPACITY = "FLOWABLE_JOB_EXECUTOR_QUEUE_CAPACITY"; + static final String CFG_CLOUD_LOGGING_SERVICE_EXECUTOR_CORE_THREADS = "CLOUD_LOGGING_SERVICE_EXECUTOR_CORE_THREADS"; + static final String CFG_CLOUD_LOGGING_SERVICE_EXECUTOR_MAX_THREADS = "CLOUD_LOGGING_SERVICE_EXECUTOR_MAX_THREADS"; + static final String CFG_CLOUD_LOGGING_SERVICE_EXECUTOR_QUEUE_CAPACITY = "CLOUD_LOGGING_SERVICE_EXECUTOR_QUEUE_CAPACITY"; static final String CFG_FSS_CACHE_UPDATE_TIMEOUT_MINUTES = "FSS_CACHE_UPDATE_TIMEOUT_MINUTES"; static final String CFG_THREAD_MONITOR_CACHE_UPDATE_IN_SECONDS = "THREAD_MONITOR_CACHE_UPDATE_IN_SECONDS"; static final String CFG_SPACE_DEVELOPER_CACHE_TIME_IN_SECONDS = "SPACE_DEVELOPER_CACHE_TIME_IN_SECONDS"; @@ -130,6 +133,9 @@ public class ApplicationConfiguration { public static final Integer DEFAULT_FLOWABLE_JOB_EXECUTOR_CORE_THREADS = 8; public static final Integer DEFAULT_FLOWABLE_JOB_EXECUTOR_MAX_THREADS = 32; public static final Integer DEFAULT_FLOWABLE_JOB_EXECUTOR_QUEUE_CAPACITY = 16; + public static final Integer DEFAULT_CLOUD_LOGGING_SERVICE_EXECUTOR_CORE_THREADS = 2; + public static final Integer DEFAULT_CLOUD_LOGGING_SERVICE_EXECUTOR_MAX_THREADS = 8; + public static final Integer DEFAULT_CLOUD_LOGGING_SERVICE_EXECUTOR_QUEUE_CAPACITY = 100; public static final Integer DEFAULT_FSS_CACHE_UPDATE_TIMEOUT_MINUTES = 30; public static final Integer DEFAULT_THREAD_MONITOR_CACHE_UPDATE_IN_SECONDS = 1; public static final Integer DEFAULT_SPACE_DEVELOPER_CACHE_TIME_IN_SECONDS = 20; @@ -194,6 +200,9 @@ public class ApplicationConfiguration { private Integer flowableJobExecutorCoreThreads; private Integer flowableJobExecutorMaxThreads; private Integer flowableJobExecutorQueueCapacity; + private Integer cloudLoggingServiceExecutorCoreThreads; + private Integer cloudLoggingServiceExecutorMaxThreads; + private Integer cloudLoggingServiceExecutorQueueCapacity; private Integer fssCacheUpdateTimeoutMinutes; private Integer threadMonitorCacheUpdateInSeconds; private Integer spaceDeveloperCacheTimeInSeconds; @@ -283,7 +292,9 @@ private Set getNotSensitiveConfigVariables() { CFG_STEP_POLLING_INTERVAL_IN_SECONDS, CFG_SKIP_SSL_VALIDATION, CFG_VERSION, CFG_CHANGE_LOG_LOCK_POLL_RATE, CFG_CHANGE_LOG_LOCK_DURATION, CFG_CHANGE_LOG_LOCK_ATTEMPTS, CFG_GLOBAL_CONFIG_SPACE, CFG_FLOWABLE_JOB_EXECUTOR_CORE_THREADS, CFG_FLOWABLE_JOB_EXECUTOR_MAX_THREADS, - CFG_FLOWABLE_JOB_EXECUTOR_QUEUE_CAPACITY, CFG_CONTROLLER_CLIENT_CONNECTION_POOL_SIZE, + CFG_FLOWABLE_JOB_EXECUTOR_QUEUE_CAPACITY, CFG_CLOUD_LOGGING_SERVICE_EXECUTOR_CORE_THREADS, + CFG_CLOUD_LOGGING_SERVICE_EXECUTOR_MAX_THREADS, CFG_CLOUD_LOGGING_SERVICE_EXECUTOR_QUEUE_CAPACITY, + CFG_CONTROLLER_CLIENT_CONNECTION_POOL_SIZE, CFG_CONTROLLER_CLIENT_THREAD_POOL_SIZE, CFG_CONTROLLER_CLIENT_RESPONSE_TIMEOUT, CFG_DB_TRANSACTION_TIMEOUT_IN_SECONDS, CFG_SNAKEYAML_MAX_ALIASES_FOR_COLLECTIONS, CFG_SERVICE_HANDLING_MAX_PARALLEL_THREADS); } @@ -541,6 +552,27 @@ public Integer getFlowableJobExecutorQueueCapacity() { return flowableJobExecutorQueueCapacity; } + public Integer getCloudLoggingServiceExecutorCoreThreads() { + if (cloudLoggingServiceExecutorCoreThreads == null) { + cloudLoggingServiceExecutorCoreThreads = getCloudLoggingServiceExecutorCoreThreadsFromEnvironment(); + } + return cloudLoggingServiceExecutorCoreThreads; + } + + public Integer getCloudLoggingServiceExecutorMaxThreads() { + if (cloudLoggingServiceExecutorMaxThreads == null) { + cloudLoggingServiceExecutorMaxThreads = getCloudLoggingServiceExecutorMaxThreadsFromEnvironment(); + } + return cloudLoggingServiceExecutorMaxThreads; + } + + public Integer getCloudLoggingServiceExecutorQueueCapacity() { + if (cloudLoggingServiceExecutorQueueCapacity == null) { + cloudLoggingServiceExecutorQueueCapacity = getCloudLoggingServiceExecutorQueueCapacityFromEnvironment(); + } + return cloudLoggingServiceExecutorQueueCapacity; + } + public Integer getFssCacheUpdateTimeoutMinutes() { if (fssCacheUpdateTimeoutMinutes == null) { fssCacheUpdateTimeoutMinutes = getFssCacheUpdateTimeoutMinutesFromEnvironment(); @@ -969,6 +1001,28 @@ private Integer getFlowableJobExecutorQueueCapacityFromEnvironment() { return value; } + private Integer getCloudLoggingServiceExecutorCoreThreadsFromEnvironment() { + Integer value = environment.getPositiveInteger(CFG_CLOUD_LOGGING_SERVICE_EXECUTOR_CORE_THREADS, + DEFAULT_CLOUD_LOGGING_SERVICE_EXECUTOR_CORE_THREADS); + logEnvironmentVariable(CFG_CLOUD_LOGGING_SERVICE_EXECUTOR_CORE_THREADS, Messages.CLOUD_LOGGING_SERVICE_EXECUTOR_CORE_THREADS, value); + return value; + } + + private Integer getCloudLoggingServiceExecutorMaxThreadsFromEnvironment() { + Integer value = environment.getPositiveInteger(CFG_CLOUD_LOGGING_SERVICE_EXECUTOR_MAX_THREADS, + DEFAULT_CLOUD_LOGGING_SERVICE_EXECUTOR_MAX_THREADS); + logEnvironmentVariable(CFG_CLOUD_LOGGING_SERVICE_EXECUTOR_MAX_THREADS, Messages.CLOUD_LOGGING_SERVICE_EXECUTOR_MAX_THREADS, value); + return value; + } + + private Integer getCloudLoggingServiceExecutorQueueCapacityFromEnvironment() { + Integer value = environment.getPositiveInteger(CFG_CLOUD_LOGGING_SERVICE_EXECUTOR_QUEUE_CAPACITY, + DEFAULT_CLOUD_LOGGING_SERVICE_EXECUTOR_QUEUE_CAPACITY); + logEnvironmentVariable(CFG_CLOUD_LOGGING_SERVICE_EXECUTOR_QUEUE_CAPACITY, Messages.CLOUD_LOGGING_SERVICE_EXECUTOR_QUEUE_CAPACITY, + value); + return value; + } + private String getCronExpression(String name, String defaultValue) { String value = environment.getString(name); if (CronExpression.isValidExpression(value)) { diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLogTest.java new file mode 100644 index 0000000000..e577f99305 --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLogTest.java @@ -0,0 +1,300 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.mta.model.ConfigurationIdentifier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +class CloudLoggingServiceConfigurationAuditLogTest { + + private static final String USERNAME = "test-user"; + private static final String SPACE_ID = "space-guid-1"; + private static final String LOGGING_CONFIG_ID = "logging-config-1"; + private static final String MTA_ID = "my-mta"; + private static final String MTA_SPACE = "my-space"; + private static final String MTA_SPACE_ID = "mta-space-guid-1"; + private static final String MTA_ORG = "my-org"; + private static final String NAMESPACE = "dev"; + private static final String TARGET_SPACE = "target-space"; + private static final String TARGET_ORG = "target-org"; + private static final String SERVICE_INSTANCE_NAME = "my-cls-instance"; + private static final String SERVICE_KEY_NAME = "my-cls-key"; + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private CloudLoggingServiceConfigurationAuditLog auditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + auditLog = new CloudLoggingServiceConfigurationAuditLog(auditLoggingFacade); + } + + // --- logCreateLoggingConfiguration --- + + @Test + void testLogCreateLoggingConfiguration_invokesFacadeWithCreateAction() { + auditLog.logCreateLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + verify(auditLoggingFacade).logConfigurationChangeAuditLog(any(AuditLogConfiguration.class), + eqAction(ConfigurationChangeActions.CONFIGURATION_CREATE)); + } + + @Test + void testLogCreateLoggingConfiguration_setsUserAndSpace() { + auditLog.logCreateLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + AuditLogConfiguration captured = captureCreate(); + assertEquals(USERNAME, captured.getUserId()); + assertEquals(SPACE_ID, captured.getSpaceId()); + } + + @Test + void testLogCreateLoggingConfiguration_setsPerformedActionContainingSpaceId() { + auditLog.logCreateLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + AuditLogConfiguration captured = captureCreate(); + assertTrue(captured.getPerformedAction() + .contains(SPACE_ID)); + } + + @Test + void testLogCreateLoggingConfiguration_includesAllIdentifiers() { + auditLog.logCreateLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + Map identifiers = identifiersFromCreate(); + assertEquals(LOGGING_CONFIG_ID, identifiers.get("id")); + assertEquals(MTA_ID, identifiers.get("mtaId")); + assertEquals(MTA_SPACE, identifiers.get("mtaSpace")); + assertEquals(MTA_SPACE_ID, identifiers.get("mtaSpaceId")); + assertEquals(MTA_ORG, identifiers.get("mtaOrg")); + assertEquals(NAMESPACE, identifiers.get("namespace")); + assertEquals(TARGET_SPACE, identifiers.get("targetSpace")); + assertEquals(TARGET_ORG, identifiers.get("targetOrg")); + assertEquals(SERVICE_INSTANCE_NAME, identifiers.get("serviceInstanceName")); + assertEquals(SERVICE_KEY_NAME, identifiers.get("serviceKeyName")); + assertEquals("INFO", identifiers.get("logLevel")); + assertEquals("true", identifiers.get("isFailSafe")); + } + + @Test + void testLogCreateLoggingConfiguration_logLevelIsNullStringWhenLogLevelIsNull() { + LoggingConfiguration config = ImmutableLoggingConfiguration.builder() + .from(buildLoggingConfiguration()) + .logLevel(null) + .build(); + + auditLog.logCreateLoggingConfiguration(USERNAME, SPACE_ID, config); + + assertEquals("null", identifiersFromCreate().get("logLevel")); + } + + // --- logUpdateLoggingConfiguration --- + + @Test + void testLogUpdateLoggingConfiguration_invokesFacadeWithUpdateAction() { + auditLog.logUpdateLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + verify(auditLoggingFacade).logConfigurationChangeAuditLog(any(AuditLogConfiguration.class), + eqAction(ConfigurationChangeActions.CONFIGURATION_UPDATE)); + } + + @Test + void testLogUpdateLoggingConfiguration_setsUserAndSpace() { + auditLog.logUpdateLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + AuditLogConfiguration captured = captureUpdate(); + assertEquals(USERNAME, captured.getUserId()); + assertEquals(SPACE_ID, captured.getSpaceId()); + } + + @Test + void testLogUpdateLoggingConfiguration_includesAllIdentifiers() { + auditLog.logUpdateLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + Map identifiers = identifiersFromUpdate(); + assertEquals(LOGGING_CONFIG_ID, identifiers.get("id")); + assertEquals(MTA_ID, identifiers.get("mtaId")); + assertEquals(NAMESPACE, identifiers.get("namespace")); + } + + // --- logDeleteLoggingConfiguration --- + + @Test + void testLogDeleteLoggingConfiguration_invokesFacadeWithDeleteAction() { + auditLog.logDeleteLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + verify(auditLoggingFacade).logConfigurationChangeAuditLog(any(AuditLogConfiguration.class), + eqAction(ConfigurationChangeActions.CONFIGURATION_DELETE)); + } + + @Test + void testLogDeleteLoggingConfiguration_setsUserAndSpace() { + auditLog.logDeleteLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + AuditLogConfiguration captured = captureDelete(); + assertEquals(USERNAME, captured.getUserId()); + assertEquals(SPACE_ID, captured.getSpaceId()); + } + + @Test + void testLogDeleteLoggingConfiguration_includesAllIdentifiers() { + auditLog.logDeleteLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + Map identifiers = identifiersFromDelete(); + assertEquals(LOGGING_CONFIG_ID, identifiers.get("id")); + assertEquals(MTA_ID, identifiers.get("mtaId")); + assertEquals(NAMESPACE, identifiers.get("namespace")); + } + + @Test + void testLogDeleteLoggingConfiguration_omitsNullValuesFromConfigurationIdentifiers() { + LoggingConfiguration sparseConfig = ImmutableLoggingConfiguration.builder() + .id(LOGGING_CONFIG_ID) + .build(); + + auditLog.logDeleteLoggingConfiguration(USERNAME, SPACE_ID, sparseConfig); + + AuditLogConfiguration captured = captureDelete(); + Map identifiers = identifiersFromConfig(captured); + assertEquals(LOGGING_CONFIG_ID, identifiers.get("id")); + // null fields should not be exposed in getConfigurationIdentifiers + for (ConfigurationIdentifier identifier : captured.getConfigurationIdentifiers()) { + assertNotNull(identifier.getIdentifierValue(), "Configuration identifier value should not be null"); + } + } + + // --- logGetLoggingConfiguration --- + + @Test + void testLogGetLoggingConfiguration_invokesDataAccessFacade() { + auditLog.logGetLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + verify(auditLoggingFacade).logDataAccessAuditLog(any(AuditLogConfiguration.class)); + } + + @Test + void testLogGetLoggingConfiguration_setsUserAndSpace() { + auditLog.logGetLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + AuditLogConfiguration captured = captureDataAccess(); + assertEquals(USERNAME, captured.getUserId()); + assertEquals(SPACE_ID, captured.getSpaceId()); + } + + @Test + void testLogGetLoggingConfiguration_includesAllConfigurationIdentifiers() { + auditLog.logGetLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + Map identifiers = identifiersFromDataAccess(); + assertEquals(MTA_ID, identifiers.get("mtaId")); + assertEquals(NAMESPACE, identifiers.get("namespace")); + // The "get" variant logs the full set of configuration identifiers + assertEquals(12, countNonReservedIdentifiers(captureDataAccess())); + } + + // --- Helpers --- + + private AuditLogConfiguration captureCreate() { + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + verify(auditLoggingFacade).logConfigurationChangeAuditLog(captor.capture(), + eqAction(ConfigurationChangeActions.CONFIGURATION_CREATE)); + return captor.getValue(); + } + + private AuditLogConfiguration captureUpdate() { + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + verify(auditLoggingFacade).logConfigurationChangeAuditLog(captor.capture(), + eqAction(ConfigurationChangeActions.CONFIGURATION_UPDATE)); + return captor.getValue(); + } + + private AuditLogConfiguration captureDelete() { + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + verify(auditLoggingFacade).logConfigurationChangeAuditLog(captor.capture(), + eqAction(ConfigurationChangeActions.CONFIGURATION_DELETE)); + return captor.getValue(); + } + + private AuditLogConfiguration captureDataAccess() { + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + verify(auditLoggingFacade).logDataAccessAuditLog(captor.capture()); + return captor.getValue(); + } + + private Map identifiersFromCreate() { + return identifiersFromConfig(captureCreate()); + } + + private Map identifiersFromUpdate() { + return identifiersFromConfig(captureUpdate()); + } + + private Map identifiersFromDelete() { + return identifiersFromConfig(captureDelete()); + } + + private Map identifiersFromDataAccess() { + return identifiersFromConfig(captureDataAccess()); + } + + private Map identifiersFromConfig(AuditLogConfiguration config) { + Map result = new HashMap<>(); + List configurationIdentifiers = config.getConfigurationIdentifiers(); + for (ConfigurationIdentifier identifier : configurationIdentifiers) { + result.put(identifier.getIdentifierName(), identifier.getIdentifierValue()); + } + return result; + } + + private int countNonReservedIdentifiers(AuditLogConfiguration config) { + int count = 0; + for (ConfigurationIdentifier identifier : config.getConfigurationIdentifiers()) { + String name = identifier.getIdentifierName(); + if (!"performed_action".equals(name) && !"time".equals(name) && !"spaceId".equals(name)) { + count++; + } + } + return count; + } + + private static LoggingConfiguration buildLoggingConfiguration() { + return ImmutableLoggingConfiguration.builder() + .id(LOGGING_CONFIG_ID) + .mtaId(MTA_ID) + .mtaSpace(MTA_SPACE) + .mtaSpaceId(MTA_SPACE_ID) + .mtaOrg(MTA_ORG) + .namespace(NAMESPACE) + .targetSpace(TARGET_SPACE) + .targetOrg(TARGET_ORG) + .serviceInstanceName(SERVICE_INSTANCE_NAME) + .serviceKeyName(SERVICE_KEY_NAME) + .logLevel(LogLevel.INFO) + .isFailSafe(true) + .build(); + } + + private static ConfigurationChangeActions eqAction(ConfigurationChangeActions action) { + return org.mockito.ArgumentMatchers.eq(action); + } +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurgerTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurgerTest.java index bbbedb936c..c78246d0a0 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurgerTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurgerTest.java @@ -2,14 +2,19 @@ import java.util.ArrayList; import java.util.List; +import java.util.UUID; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudMetadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudApplication; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableLifecycle; import org.cloudfoundry.multiapps.controller.client.facade.domain.LifecycleType; import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudSpaceClient; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; import org.cloudfoundry.multiapps.controller.core.auditlogging.MtaConfigurationPurgerAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.metadata.processor.MtaMetadataParser; import org.cloudfoundry.multiapps.controller.core.cf.metadata.processor.MtaMetadataValidator; @@ -17,9 +22,12 @@ import org.cloudfoundry.multiapps.controller.persistence.model.CloudTarget; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationSubscription; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; import org.cloudfoundry.multiapps.controller.persistence.query.ConfigurationEntryQuery; import org.cloudfoundry.multiapps.controller.persistence.query.ConfigurationSubscriptionQuery; import org.cloudfoundry.multiapps.controller.persistence.query.Query; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationEntryService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationSubscriptionService; import org.cloudfoundry.multiapps.mta.model.Version; @@ -31,6 +39,8 @@ import org.mockito.MockitoAnnotations; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class MtaConfigurationPurgerTest { @@ -63,6 +73,10 @@ class MtaConfigurationPurgerTest { ConfigurationSubscriptionQuery configurationSubscriptionQuery; @Mock MtaConfigurationPurgerAuditLog mtaConfigurationPurgerAuditLog; + @Mock + CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + @Mock + CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; @BeforeEach void setUp() throws Exception { @@ -78,8 +92,9 @@ void testPurge() { MtaConfigurationPurger purger = new MtaConfigurationPurger(client, spaceClient, configurationEntryService, configurationSubscriptionService, new MtaMetadataParser(new MtaMetadataValidator()), - mtaConfigurationPurgerAuditLog); - purger.purge("org", "space"); + mtaConfigurationPurgerAuditLog, cloudLoggingServiceConfigurationService, + cloudLoggingServiceConfigurationAuditLog); + purger.purge("org", "space", "test-user"); verifyConfigurationEntriesDeleted(); verifyConfigurationEntriesNotDeleted(); } @@ -147,4 +162,73 @@ private ConfigurationEntry createEntry(int id, String providerId) { new CloudTarget(TARGET_ORG, TARGET_SPACE), null, null, null, null); } + @Test + void testPurgeCloudLoggingServiceConfigurations_deletesAllConfigurationsInSpace() { + String spaceId = "00000000-0000-0000-0000-000000000001"; + LoggingConfiguration config1 = createLoggingConfiguration("id-1", spaceId, "mta-1"); + LoggingConfiguration config2 = createLoggingConfiguration("id-2", spaceId, "mta-2"); + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId)).thenReturn(List.of(config1, + config2)); + when(spaceClient.getSpace(TARGET_ORG, TARGET_SPACE)).thenReturn(createCloudSpace(spaceId)); + + MtaConfigurationPurger purger = createPurger(); + purger.purge(TARGET_ORG, TARGET_SPACE, "test-user"); + + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-1"); + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-2"); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("test-user", spaceId, config1); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("test-user", spaceId, config2); + } + + @Test + void testPurgeCloudLoggingServiceConfigurations_doesNothingWhenNoConfigurationsExist() { + String spaceId = "00000000-0000-0000-0000-000000000002"; + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId)).thenReturn(List.of()); + when(spaceClient.getSpace(TARGET_ORG, TARGET_SPACE)).thenReturn(createCloudSpace(spaceId)); + + MtaConfigurationPurger purger = createPurger(); + purger.purge(TARGET_ORG, TARGET_SPACE, "test-user"); + + verify(cloudLoggingServiceConfigurationService, never()).deleteCloudLoggingServiceConfiguration(Mockito.anyString()); + verify(cloudLoggingServiceConfigurationAuditLog, never()).logDeleteLoggingConfiguration(Mockito.anyString(), Mockito.anyString(), + Mockito.any()); + } + + @Test + void testPurgeCloudLoggingServiceConfigurations_deletesSingleConfiguration() { + String spaceId = "00000000-0000-0000-0000-000000000003"; + LoggingConfiguration config = createLoggingConfiguration("id-1", spaceId, "mta-1"); + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId)).thenReturn(List.of(config)); + when(spaceClient.getSpace(TARGET_ORG, TARGET_SPACE)).thenReturn(createCloudSpace(spaceId)); + + MtaConfigurationPurger purger = createPurger(); + purger.purge(TARGET_ORG, TARGET_SPACE, "test-user"); + + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-1"); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("test-user", spaceId, config); + } + + private MtaConfigurationPurger createPurger() { + return new MtaConfigurationPurger(client, spaceClient, configurationEntryService, configurationSubscriptionService, + new MtaMetadataParser(new MtaMetadataValidator()), mtaConfigurationPurgerAuditLog, + cloudLoggingServiceConfigurationService, cloudLoggingServiceConfigurationAuditLog); + } + + private LoggingConfiguration createLoggingConfiguration(String id, String spaceId, String mtaId) { + return ImmutableLoggingConfiguration.builder() + .id(id) + .mtaSpaceId(spaceId) + .mtaId(mtaId) + .build(); + } + + private CloudSpace createCloudSpace(String spaceId) { + return ImmutableCloudSpace.builder() + .metadata(ImmutableCloudMetadata.builder() + .guid(UUID.fromString(spaceId)) + .build()) + .name(TARGET_SPACE) + .build(); + } + } diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationServiceTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationServiceTest.java index 2e9c0656e5..60cbc2d96a 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationServiceTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationServiceTest.java @@ -1,16 +1,5 @@ package org.cloudfoundry.multiapps.controller.core.security.data.termination; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import java.text.MessageFormat; import java.util.Collections; import java.util.List; @@ -19,18 +8,24 @@ import java.util.stream.Stream; import org.cloudfoundry.multiapps.controller.core.Messages; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; import org.cloudfoundry.multiapps.controller.core.auditlogging.MtaConfigurationPurgerAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.clients.CFOptimizedEventGetter; +import org.cloudfoundry.multiapps.controller.core.cf.clients.WebClientFactory; import org.cloudfoundry.multiapps.controller.core.test.MockBuilder; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.model.CloudTarget; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationSubscription; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; import org.cloudfoundry.multiapps.controller.persistence.query.ConfigurationEntryQuery; import org.cloudfoundry.multiapps.controller.persistence.query.ConfigurationSubscriptionQuery; import org.cloudfoundry.multiapps.controller.persistence.query.OperationQuery; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationEntryService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationSubscriptionService; +import org.cloudfoundry.multiapps.controller.persistence.services.DescriptorBackupService; import org.cloudfoundry.multiapps.controller.persistence.services.FileService; import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; import org.cloudfoundry.multiapps.controller.persistence.services.OperationService; @@ -46,6 +41,18 @@ import org.mockito.MockitoAnnotations; import org.mockito.verification.VerificationMode; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + class DataTerminationServiceTest { private static final String GLOBAL_AUDITOR_USERNAME = "test"; @@ -71,17 +78,28 @@ class DataTerminationServiceTest { private CFOptimizedEventGetter cfOptimizedEventsGetter; @Mock private MtaConfigurationPurgerAuditLog mtaConfigurationPurgerAuditLog; + @Mock + private DescriptorBackupService descriptorBackupService; + @Mock + private WebClientFactory webClientFactory; + @Mock + private CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + @Mock + private CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; @InjectMocks - private final DataTerminationService dataTerminationService = createDataTerminationService(); + private DataTerminationService dataTerminationService; @BeforeEach void setUp() throws Exception { MockitoAnnotations.openMocks(this) .close(); + dataTerminationService = createDataTerminationService(); } private DataTerminationService createDataTerminationService() { - return new DataTerminationService() { + return new DataTerminationService(configurationEntryService, configurationSubscriptionService, operationService, fileService, + configuration, webClientFactory, mtaConfigurationPurgerAuditLog, descriptorBackupService, + cloudLoggingServiceConfigurationService, cloudLoggingServiceConfigurationAuditLog) { @Override protected CFOptimizedEventGetter getCfOptimizedEventGetter() { @@ -155,12 +173,14 @@ private void initializeServiceMocks(List subscription when(configurationSubscriptionService.createQuery()).thenReturn(configurationSubscriptionQuery); when(configurationEntryService.createQuery()).thenReturn(configurationEntryQuery); - ConfigurationSubscriptionQuery configurationSubscriptionQueryMock = new MockBuilder<>(configurationSubscriptionQuery).on(query -> query.spaceId(deletedSpace)) + ConfigurationSubscriptionQuery configurationSubscriptionQueryMock = new MockBuilder<>(configurationSubscriptionQuery).on( + query -> query.spaceId(deletedSpace)) .build(); doReturn(subscriptions).when(configurationSubscriptionQueryMock) .list(); - ConfigurationEntryQuery configurationEntryQueryMock = new MockBuilder<>(configurationEntryQuery).on(query -> query.spaceId(deletedSpace)) + ConfigurationEntryQuery configurationEntryQueryMock = new MockBuilder<>(configurationEntryQuery).on( + query -> query.spaceId(deletedSpace)) .build(); doReturn(configurationEntries).when(configurationEntryQueryMock) .list(); @@ -221,4 +241,91 @@ void testDoesNotThrowExceptionOnFailToDeleteSpace() throws FileStorageException assertDoesNotThrow(() -> dataTerminationService.deleteOrphanUserData()); } + @Test + void testDeleteExistingCloudLoggingServiceConfiguration_deletesAllConfigurationsInSpace() { + String spaceId = "space-1"; + LoggingConfiguration config1 = createLoggingConfiguration("id-1", spaceId, "mta-1"); + LoggingConfiguration config2 = createLoggingConfiguration("id-2", spaceId, "mta-2"); + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId)).thenReturn( + List.of(config1, config2)); + prepareGlobalAuditorCredentials(); + prepareCfOptimizedEventsGetter(List.of(spaceId)); + when(configurationSubscriptionService.createQuery()).thenReturn(configurationSubscriptionQuery); + when(configurationEntryService.createQuery()).thenReturn(configurationEntryQuery); + when(operationService.createQuery()).thenReturn(operationQuery); + + dataTerminationService.deleteOrphanUserData(); + + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-1"); + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-2"); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("", spaceId, config1); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("", spaceId, config2); + } + + @Test + void testDeleteExistingCloudLoggingServiceConfiguration_doesNothingWhenNoConfigurationsExist() { + String spaceId = "space-2"; + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId)).thenReturn(List.of()); + prepareGlobalAuditorCredentials(); + prepareCfOptimizedEventsGetter(List.of(spaceId)); + when(configurationSubscriptionService.createQuery()).thenReturn(configurationSubscriptionQuery); + when(configurationEntryService.createQuery()).thenReturn(configurationEntryQuery); + when(operationService.createQuery()).thenReturn(operationQuery); + + dataTerminationService.deleteOrphanUserData(); + + verify(cloudLoggingServiceConfigurationService, never()).deleteCloudLoggingServiceConfiguration(anyString()); + verify(cloudLoggingServiceConfigurationAuditLog, never()).logDeleteLoggingConfiguration(anyString(), anyString(), any()); + } + + @Test + void testDeleteExistingCloudLoggingServiceConfiguration_deletesSingleConfiguration() { + String spaceId = "space-3"; + LoggingConfiguration config = createLoggingConfiguration("id-1", spaceId, "mta-1"); + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId)).thenReturn(List.of(config)); + prepareGlobalAuditorCredentials(); + prepareCfOptimizedEventsGetter(List.of(spaceId)); + when(configurationSubscriptionService.createQuery()).thenReturn(configurationSubscriptionQuery); + when(configurationEntryService.createQuery()).thenReturn(configurationEntryQuery); + when(operationService.createQuery()).thenReturn(operationQuery); + + dataTerminationService.deleteOrphanUserData(); + + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-1"); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("", spaceId, config); + } + + @Test + void testDeleteExistingCloudLoggingServiceConfiguration_deletesAcrossMultipleSpaces() { + String spaceId1 = "space-4"; + String spaceId2 = "space-5"; + LoggingConfiguration config1 = createLoggingConfiguration("id-1", spaceId1, "mta-1"); + LoggingConfiguration config2 = createLoggingConfiguration("id-2", spaceId2, "mta-2"); + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId1)).thenReturn( + List.of(config1)); + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId2)).thenReturn( + List.of(config2)); + prepareGlobalAuditorCredentials(); + prepareCfOptimizedEventsGetter(List.of(spaceId1, spaceId2)); + when(configurationSubscriptionService.createQuery()).thenReturn(configurationSubscriptionQuery); + when(configurationEntryService.createQuery()).thenReturn(configurationEntryQuery); + when(operationService.createQuery()).thenReturn(operationQuery); + + dataTerminationService.deleteOrphanUserData(); + + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-1"); + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-2"); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("", spaceId1, config1); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("", spaceId2, config2); + } + + private LoggingConfiguration createLoggingConfiguration(String id, String spaceId, String mtaId) { + return ImmutableLoggingConfiguration.builder() + .id(id) + .mtaSpaceId(spaceId) + .mtaId(mtaId) + .build(); + } + } + diff --git a/multiapps-controller-persistence/pom.xml b/multiapps-controller-persistence/pom.xml index eade772907..080f5a6181 100644 --- a/multiapps-controller-persistence/pom.xml +++ b/multiapps-controller-persistence/pom.xml @@ -231,5 +231,13 @@ javax.xml.bind jaxb-api + + org.springframework + spring-webflux + + + io.projectreactor.netty + reactor-netty-http + diff --git a/multiapps-controller-persistence/src/main/java/module-info.java b/multiapps-controller-persistence/src/main/java/module-info.java index 8c3c8226e0..caa5c52821 100644 --- a/multiapps-controller-persistence/src/main/java/module-info.java +++ b/multiapps-controller-persistence/src/main/java/module-info.java @@ -43,6 +43,7 @@ requires google.cloud.core; requires google.cloud.nio; requires google.cloud.storage; + requires io.netty.handler; requires jakarta.xml.bind; requires jakarta.annotation; requires jakarta.inject; @@ -57,9 +58,13 @@ requires org.cloudfoundry.multiapps.common; requires org.eclipse.persistence.core; requires org.slf4j; + requires reactor.core; + requires reactor.netty.core; + requires reactor.netty.http; requires spring.beans; requires spring.context; requires spring.core; + requires spring.webflux; requires static java.compiler; requires static org.immutables.value; diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/Messages.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/Messages.java index 4a2daa7c71..67e0c0756d 100644 --- a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/Messages.java +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/Messages.java @@ -14,6 +14,7 @@ public final class Messages { public static final String ERROR_GETTING_FILES_WITH_SPACE_AND_NAMESPACE = "Error getting files with space {0} and namespace {1}"; public static final String ERROR_GETTING_LOGS_WITH_SPACE_AND_OPERATION_ID = "Error getting logs with space {0} and operation id {1}"; public static final String ERROR_GETTING_LOGS_WITH_SPACE_OPERATION_ID_AND_NAME = "Error getting logs with space {0} operation id {1} and file name {2}"; + public static final String ERROR_UPDATING_LOGS_WITH_SPACE_OPERATION_ID_AND_NAME = "Error updating log with space {0} operation id {1} and file name {2}"; public static final String ERROR_GETTING_ALL_FILES = "Error getting all files"; public static final String ERROR_LOG_FILE_NOT_FOUND = "Log file with name \"{0}\" for operation \"{1}\" in space \"{2}\" was not found"; public static final String ERROR_CORRELATION_ID_OR_ACTIVITY_ID_NULL = "Unable to retrieve correlation id or activity id for process \"{0}\" at activity \"{1}\" and space \"{2}\""; @@ -48,15 +49,18 @@ public final class Messages { public static final String APPLICATION_SHUTDOWN_WITH_APPLICATION_INSTANCE_ID_ALREADY_EXIST = "Application shutdown application instance ID \"{0}\" already exist"; public static final String SECRET_TOKEN_WITH_ID_NOT_EXIST = "Secret token with ID \"{0}\" does not exist"; public static final String DATABASE_HEALTH_CHECK_FAILED = "Database health check failed"; + public static final String FAILED_TO_SEND_LOG_MESSAGE_TO_CLS = "Failed to send log message to Cloud Logging service"; // ERROR log messages: public static final String UPLOAD_STREAM_FAILED_TO_CLOSE = "Cannot close file upload stream"; + public static final String INVALID_LOG_FILE = "Invalid log file"; // WARN log messages: public static final String COULD_NOT_CLOSE_RESULT_SET = "Could not close result set."; public static final String COULD_NOT_CLOSE_STATEMENT = "Could not close statement."; public static final String COULD_NOT_CLOSE_CONNECTION = "Could not close connection."; public static final String ATTEMPT_TO_UPLOAD_BLOB_FAILED = "Attempt [{0}/{1}] to upload blob to ObjectStore failed with \"{2}\""; + public static final String RETRYING_SEND_LOGS_TO_CLS = "Retrying send of log batch to Cloud Logging service after transient failure: {0}"; public static final String ATTEMPT_TO_DOWNLOAD_MISSING_BLOB = "Attempt [{0}/{1}] to download missing blob {2} from ObjectStore"; public static final String USER_METADATA_OF_BLOB_0_EMPTY_AND_WILL_BE_DELETED = "User metadata of blob \"{0}\" is empty and will be deleted"; public static final String DATE_METADATA_OF_BLOB_0_IS_NOT_IN_PROPER_FORMAT_AND_WILL_BE_DELETED = "Date metadata of blob \"{0}\" is not in a proper format and will be deleted"; @@ -77,6 +81,7 @@ public final class Messages { public static final String RETRIEVED_SECRET_TOKEN_WITH_ID_0_FOR_PROCESS_WITH_ID_1 = "Retrieved secret token with id \"{0}\" for process with id \"{1}\""; public static final String DELETED_0_SECRET_TOKENS_FOR_PROCESS_WITH_ID_1 = "Deleted \"{0}\" secret tokens for process with id \"{1}\""; public static final String DELETED_0_SECRET_TOKENS_WITH_EXPIRATION_DATE_1 = "Deleted secret tokens \"{0}\" with an expiration date \"{1}\""; + public static final String CREATING_WEBCLIENT_WITH_MTLS_CONFIGURATION_FOR_ENDPOINT_1 = "Creating WebClient with mTLS configuration for endpoint: {0}"; protected Messages() { } diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/dto/LoggingConfigurationDto.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/dto/LoggingConfigurationDto.java new file mode 100644 index 0000000000..a8e0271b31 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/dto/LoggingConfigurationDto.java @@ -0,0 +1,181 @@ +package org.cloudfoundry.multiapps.controller.persistence.dto; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.Table; + +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.PersistenceMetadata.TableColumnNames; +import org.cloudfoundry.multiapps.controller.persistence.model.PersistenceMetadata.TableNames; + +@Entity +@Table(name = TableNames.CLOUD_LOGGING_SERVICE_CONFIGURATION_TABLE) +@NamedQuery(name = LoggingConfigurationDto.NamedQueries.FIND_BY_MTA_SPACE_ID_NAMESPACE, query = "SELECT c FROM LoggingConfigurationDto c WHERE c.mtaSpace = :mtaSpace AND c.mtaId = :mtaId AND c.namespace = :namespace") +@NamedQuery(name = LoggingConfigurationDto.NamedQueries.FIND_BY_MTA_SPACE_ID_NULL_NAMESPACE, query = "SELECT c FROM LoggingConfigurationDto c WHERE c.mtaSpace = :mtaSpace AND c.mtaId = :mtaId AND c.namespace IS NULL") +@NamedQuery(name = LoggingConfigurationDto.NamedQueries.FIND_ALL_BY_MTA_SPACE_ID, query = "SELECT c FROM LoggingConfigurationDto c WHERE c.mtaSpaceId = :mtaSpaceId") +public class LoggingConfigurationDto implements DtoWithPrimaryKey { + + public static class NamedQueries { + + private NamedQueries() { + } + + public static final String FIND_BY_MTA_SPACE_ID_NAMESPACE = "LoggingConfigurationDto.findByMtaSpaceIdNamespace"; + public static final String FIND_BY_MTA_SPACE_ID_NULL_NAMESPACE = "LoggingConfigurationDto.findByMtaSpaceIdNullNamespace"; + public static final String FIND_ALL_BY_MTA_SPACE_ID = "LoggingConfigurationDto.findAllByMtaSpaceId"; + } + + @Id + @Column(name = TableColumnNames.CLOUD_LOGGING_ID, nullable = false, unique = true) + private String id; + + @Column(name = TableColumnNames.CLOUD_LOGGING_TARGET_SPACE, nullable = false) + private String targetSpace; + + @Column(name = TableColumnNames.CLOUD_LOGGING_TARGET_ORG, nullable = false) + private String targetOrg; + + @Column(name = TableColumnNames.CLOUD_LOGGING_MTA_ID, nullable = false) + private String mtaId; + + @Column(name = TableColumnNames.CLOUD_LOGGING_MTA_ORG, nullable = false) + private String mtaOrg; + + @Column(name = TableColumnNames.CLOUD_LOGGING_MTA_SPACE, nullable = false) + private String mtaSpace; + + @Column(name = TableColumnNames.CLOUD_LOGGING_MTA_SPACE_ID, nullable = false) + private String mtaSpaceId; + + @Column(name = TableColumnNames.CLOUD_LOGGING_NAMESPACE) + private String namespace; + + @Column(name = TableColumnNames.CLOUD_LOGGING_SERVICE_INSTANCE_NAME, nullable = false, length = 50) + private String serviceInstanceName; + + @Column(name = TableColumnNames.CLOUD_LOGGING_SERVICE_KEY_NAME, nullable = false, length = 63) + private String serviceKeyName; + + @Column(name = TableColumnNames.CLOUD_LOGGING_LOG_LEVEL, nullable = false, length = 5) + private String logLevel; + + @Column(name = TableColumnNames.CLOUD_LOGGING_IS_FAILSAFE, nullable = false) + private boolean failSafe; + + @Column(name = TableColumnNames.CLOUD_LOGGING_ADDED_AT, nullable = false) + private LocalDateTime addedAt; + + public LoggingConfigurationDto() { + // Required by JPA + } + + @Override + public String getPrimaryKey() { + return id; + } + + @Override + public void setPrimaryKey(String id) { + this.id = id; + } + + public String getTargetSpace() { + return targetSpace; + } + + public void setTargetSpace(String targetSpace) { + this.targetSpace = targetSpace; + } + + public String getTargetOrg() { + return targetOrg; + } + + public void setTargetOrg(String targetOrg) { + this.targetOrg = targetOrg; + } + + public String getMtaId() { + return mtaId; + } + + public void setMtaId(String mtaId) { + this.mtaId = mtaId; + } + + public String getMtaOrg() { + return mtaOrg; + } + + public void setMtaOrg(String mtaOrg) { + this.mtaOrg = mtaOrg; + } + + public String getMtaSpace() { + return mtaSpace; + } + + public void setMtaSpace(String mtaSpace) { + this.mtaSpace = mtaSpace; + } + + public String getMtaSpaceId() { + return mtaSpaceId; + } + + public void setMtaSpaceId(String mtaSpaceId) { + this.mtaSpaceId = mtaSpaceId; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getServiceInstanceName() { + return serviceInstanceName; + } + + public void setServiceInstanceName(String serviceInstanceName) { + this.serviceInstanceName = serviceInstanceName; + } + + public String getServiceKeyName() { + return serviceKeyName; + } + + public void setServiceKeyName(String serviceKeyName) { + this.serviceKeyName = serviceKeyName; + } + + public LogLevel getLogLevel() { + return logLevel == null ? null : LogLevel.get(logLevel); + } + + public void setLogLevel(LogLevel logLevel) { + this.logLevel = logLevel == null ? null : logLevel.name(); + } + + public boolean isFailSafe() { + return failSafe; + } + + public void setFailSafe(boolean failSafe) { + this.failSafe = failSafe; + } + + public LocalDateTime getAddedAt() { + return addedAt; + } + + public void setAddedAt(LocalDateTime addedAt) { + this.addedAt = addedAt; + } +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/ExternalOperationLogEntry.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/ExternalOperationLogEntry.java new file mode 100644 index 0000000000..f729c5fe63 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/ExternalOperationLogEntry.java @@ -0,0 +1,32 @@ +package org.cloudfoundry.multiapps.controller.persistence.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cloudfoundry.multiapps.common.Nullable; +import org.immutables.value.Value; + +@Value.Immutable +@JsonSerialize(as = ImmutableExternalOperationLogEntry.class) +@JsonDeserialize(as = ImmutableExternalOperationLogEntry.class) +public interface ExternalOperationLogEntry { + + @JsonProperty("msg") + String getMessage(); + + @JsonProperty("date") + String getTimestamp(); + + @JsonProperty("correlation_id") + @Nullable + String getCorrelationId(); + + @JsonProperty("operation_log_name") + @Nullable + String getOperationLogName(); + + @Nullable + String getLevel(); + + String getId(); +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/LogLevel.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/LogLevel.java new file mode 100644 index 0000000000..927c8e3ac8 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/LogLevel.java @@ -0,0 +1,43 @@ +package org.cloudfoundry.multiapps.controller.persistence.model; + +import java.util.Arrays; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public enum LogLevel { + + INFO, WARN, DEBUG, ERROR, TRACE; + + private static final Map> logLevelLoggingType = setupLogLevels(); + + public static LogLevel get(String value) { + for (LogLevel logLevel : values()) { + if (logLevel.name() + .equals(value)) { + return logLevel; + } + } + return null; + } + + public static Map> getLogLevelLoggingType() { + return logLevelLoggingType; + } + + private static Map> setupLogLevels() { + Map> logLevels = new EnumMap<>(LogLevel.class); + logLevels.put(TRACE, List.of(TRACE, DEBUG, INFO, WARN, ERROR)); + logLevels.put(DEBUG, List.of(DEBUG, INFO, WARN, ERROR)); + logLevels.put(INFO, List.of(INFO, WARN, ERROR)); + logLevels.put(WARN, List.of(WARN, ERROR)); + logLevels.put(ERROR, List.of(ERROR)); + return logLevels; + } + + public static boolean isValid(String logLevel) { + return Arrays.stream(values()) + .anyMatch(value -> value.name() + .equals(logLevel)); + } +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/LoggingConfiguration.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/LoggingConfiguration.java new file mode 100644 index 0000000000..f873fc8d95 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/LoggingConfiguration.java @@ -0,0 +1,71 @@ +package org.cloudfoundry.multiapps.controller.persistence.model; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cloudfoundry.multiapps.common.Nullable; +import org.immutables.value.Value; + +@Value.Immutable +@JsonSerialize(as = ImmutableLoggingConfiguration.class) +@JsonDeserialize(as = ImmutableLoggingConfiguration.class) +public interface LoggingConfiguration { + + @Nullable + String getId(); + + @Nullable + String getTargetOrg(); + + @Nullable + String getTargetSpace(); + + @Nullable + String getTargetSpaceGuid(); + + @Nullable + String getServiceInstanceGuid(); + + @Nullable + String getMtaOrg(); + + @Nullable + String getMtaSpace(); + + @Nullable + String getMtaSpaceId(); + + @Nullable + String getMtaId(); + + @Nullable + String getOperationId(); + + @Nullable + String getEndpointUrl(); + + @Nullable + String getServerCa(); + + @Nullable + String getClientCert(); + + @Nullable + String getClientKey(); + + @Nullable + LogLevel getLogLevel(); + + @Value.Default + default Boolean isFailSafe() { + return false; + } + + @Nullable + String getServiceInstanceName(); + + @Nullable + String getServiceKeyName(); + + @Nullable + String getNamespace(); +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/PersistenceMetadata.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/PersistenceMetadata.java index 453d8cfc07..f8a6d11a0e 100644 --- a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/PersistenceMetadata.java +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/PersistenceMetadata.java @@ -22,6 +22,7 @@ private TableNames() { public static final String BACKUP_DESCRIPTOR_TABLE = "backup_descriptor"; public static final String APPLICATION_SHUTDOWN_TABLE = "application_shutdown"; public static final String SECRET_TOKEN = "secret_token"; + public static final String CLOUD_LOGGING_SERVICE_CONFIGURATION_TABLE = "cloud_logging_service_configuration"; } @@ -128,6 +129,20 @@ private TableColumnNames() { public static final String SECRET_TOKEN_VARIABLE_NAME = "variable_name"; public static final String SECRET_TOKEN_CONTENT = "content"; public static final String SECRET_TOKEN_TIMESTAMP = "timestamp"; + + public static final String CLOUD_LOGGING_ID = "id"; + public static final String CLOUD_LOGGING_TARGET_SPACE = "target_space"; + public static final String CLOUD_LOGGING_TARGET_ORG = "target_org"; + public static final String CLOUD_LOGGING_MTA_ID = "mta_id"; + public static final String CLOUD_LOGGING_MTA_ORG = "mta_org"; + public static final String CLOUD_LOGGING_MTA_SPACE = "mta_space"; + public static final String CLOUD_LOGGING_MTA_SPACE_ID = "mta_space_id"; + public static final String CLOUD_LOGGING_NAMESPACE = "namespace"; + public static final String CLOUD_LOGGING_SERVICE_INSTANCE_NAME = "service_instance_name"; + public static final String CLOUD_LOGGING_SERVICE_KEY_NAME = "service_key_name"; + public static final String CLOUD_LOGGING_LOG_LEVEL = "log_level"; + public static final String CLOUD_LOGGING_IS_FAILSAFE = "is_failsafe"; + public static final String CLOUD_LOGGING_ADDED_AT = "added_at"; } } diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/SqlOperationLogQueryProvider.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/SqlOperationLogQueryProvider.java index a438a7e574..594bb2aa9e 100644 --- a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/SqlOperationLogQueryProvider.java +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/SqlOperationLogQueryProvider.java @@ -20,10 +20,12 @@ public class SqlOperationLogQueryProvider { private static final String ID_COLUMN_LABEL = "id"; private static final String OPERATION_LOG_COLUMN_LABEL = "operation_log"; private static final String OPERATION_LOG_NAME_COLUMN_LABEL = "operation_log_name"; - private static final String SELECT_LOGS_BY_SPACE_ID_OPERATION_ID_AND_OPERATION_LOG_NAME = "SELECT ID, OPERATION_LOG, OPERATION_LOG_NAME FROM %s WHERE SPACE=? AND OPERATION_ID=? AND OPERATION_LOG_NAME=? ORDER BY MODIFIED ASC"; + private static final String OPERATION_LOG_MODIFIED_COLUMN_LABEL = "modified"; + private static final String SELECT_LOGS_BY_SPACE_ID_OPERATION_ID_AND_OPERATION_LOG_NAME = "SELECT ID, OPERATION_LOG, OPERATION_LOG_NAME, MODIFIED FROM %s WHERE SPACE=? AND OPERATION_ID=? AND OPERATION_LOG_NAME=? ORDER BY MODIFIED ASC"; private static final String SELECT_LOGS_BY_SPACE_ID_AND_NAME = "SELECT DISTINCT ID, OPERATION_LOG, OPERATION_LOG_NAME, MODIFIED FROM %s WHERE SPACE=? AND OPERATION_ID=? ORDER BY MODIFIED ASC"; - private final String tableName; + public static final String UPDATE_IS_SEND_TO_CLOUD_LOGGING_SERVICE = "UPDATE %s SET IS_SEND_TO_CLOUD_LOGGING_SERVICE = ? where ID = ?"; + private final String tableName; public SqlOperationLogQueryProvider(String tableName) { this.tableName = tableName; @@ -115,6 +117,8 @@ private OperationLogEntry getOperationLogEntry(ResultSet resultSet) throws SQLEx .id(resultSet.getString(ID_COLUMN_LABEL)) .operationLog(resultSet.getString(OPERATION_LOG_COLUMN_LABEL)) .operationLogName(resultSet.getString(OPERATION_LOG_NAME_COLUMN_LABEL)) + .modified(resultSet.getTimestamp(OPERATION_LOG_MODIFIED_COLUMN_LABEL) + .toLocalDateTime()) .build(); } } diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationService.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationService.java new file mode 100644 index 0000000000..5effa4d9e9 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationService.java @@ -0,0 +1,152 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.function.Function; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.NoResultException; +import jakarta.persistence.TypedQuery; +import org.cloudfoundry.multiapps.controller.persistence.TransactionalExecutor; +import org.cloudfoundry.multiapps.controller.persistence.dto.LoggingConfigurationDto; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; + +@Named("cloudLoggingServiceConfigurationService") +public class CloudLoggingServiceConfigurationService { + + private final EntityManagerFactory entityManagerFactory; + + @Inject + public CloudLoggingServiceConfigurationService(EntityManagerFactory entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + } + + public void storeCloudLoggingServiceConfiguration(LoggingConfiguration loggingConfiguration) { + executeInTransaction(manager -> { + LoggingConfigurationDto dto = toDto(loggingConfiguration); + dto.setAddedAt(LocalDateTime.now()); + manager.persist(dto); + return null; + }); + } + + public LoggingConfiguration getCloudLoggingServiceConfiguration(String mtaSpace, String mtaId, String namespace) { + return executeInEntityManager(manager -> { + TypedQuery query; + if (namespace == null) { + query = manager.createNamedQuery(LoggingConfigurationDto.NamedQueries.FIND_BY_MTA_SPACE_ID_NULL_NAMESPACE, + LoggingConfigurationDto.class); + } else { + query = manager.createNamedQuery(LoggingConfigurationDto.NamedQueries.FIND_BY_MTA_SPACE_ID_NAMESPACE, + LoggingConfigurationDto.class); + query.setParameter("namespace", namespace); + } + query.setParameter("mtaSpace", mtaSpace); + query.setParameter("mtaId", mtaId); + try { + return fromDto(query.getSingleResult()); + } catch (NoResultException e) { + return null; + } + }); + } + + public void deleteCloudLoggingServiceConfiguration(String id) { + executeInTransaction(manager -> { + LoggingConfigurationDto dto = manager.find(LoggingConfigurationDto.class, id); + if (dto != null) { + manager.remove(dto); + } + return null; + }); + } + + public void updateCloudLoggingServiceConfiguration(LoggingConfiguration loggingConfiguration) { + executeInTransaction(manager -> { + TypedQuery query; + String namespace = loggingConfiguration.getNamespace(); + if (namespace == null) { + query = manager.createNamedQuery(LoggingConfigurationDto.NamedQueries.FIND_BY_MTA_SPACE_ID_NULL_NAMESPACE, + LoggingConfigurationDto.class); + } else { + query = manager.createNamedQuery(LoggingConfigurationDto.NamedQueries.FIND_BY_MTA_SPACE_ID_NAMESPACE, + LoggingConfigurationDto.class); + query.setParameter("namespace", namespace); + } + query.setParameter("mtaSpace", loggingConfiguration.getMtaSpace()); + query.setParameter("mtaId", loggingConfiguration.getMtaId()); + LoggingConfigurationDto existing; + try { + existing = query.getSingleResult(); + } catch (NoResultException e) { + return null; + } + existing.setTargetSpace(loggingConfiguration.getTargetSpace()); + existing.setTargetOrg(loggingConfiguration.getTargetOrg()); + existing.setServiceInstanceName(loggingConfiguration.getServiceInstanceName()); + existing.setServiceKeyName(loggingConfiguration.getServiceKeyName()); + existing.setLogLevel(loggingConfiguration.getLogLevel()); + existing.setFailSafe(Boolean.TRUE.equals(loggingConfiguration.isFailSafe())); + existing.setAddedAt(LocalDateTime.now()); + return null; + }); + } + + public List getAllCloudLoggingServiceConfigurationsFromSpace(String spaceId) { + return executeInEntityManager(manager -> manager.createNamedQuery(LoggingConfigurationDto.NamedQueries.FIND_ALL_BY_MTA_SPACE_ID, + LoggingConfigurationDto.class) + .setParameter("mtaSpaceId", spaceId) + .getResultList() + .stream() + .map(CloudLoggingServiceConfigurationService::fromDto) + .toList()); + } + + private R executeInTransaction(Function function) { + return new TransactionalExecutor(entityManagerFactory.createEntityManager()).execute(function); + } + + private R executeInEntityManager(Function function) { + try (EntityManager manager = entityManagerFactory.createEntityManager()) { + return function.apply(manager); + } + } + + private static LoggingConfigurationDto toDto(LoggingConfiguration config) { + LoggingConfigurationDto dto = new LoggingConfigurationDto(); + dto.setPrimaryKey(config.getId()); + dto.setTargetSpace(config.getTargetSpace()); + dto.setTargetOrg(config.getTargetOrg()); + dto.setMtaId(config.getMtaId()); + dto.setMtaOrg(config.getMtaOrg()); + dto.setMtaSpace(config.getMtaSpace()); + dto.setMtaSpaceId(config.getMtaSpaceId()); + dto.setNamespace(config.getNamespace()); + dto.setServiceInstanceName(config.getServiceInstanceName()); + dto.setServiceKeyName(config.getServiceKeyName()); + dto.setLogLevel(config.getLogLevel()); + dto.setFailSafe(Boolean.TRUE.equals(config.isFailSafe())); + return dto; + } + + private static LoggingConfiguration fromDto(LoggingConfigurationDto dto) { + return ImmutableLoggingConfiguration.builder() + .id(dto.getPrimaryKey()) + .targetSpace(dto.getTargetSpace()) + .targetOrg(dto.getTargetOrg()) + .mtaId(dto.getMtaId()) + .mtaOrg(dto.getMtaOrg()) + .mtaSpace(dto.getMtaSpace()) + .mtaSpaceId(dto.getMtaSpaceId()) + .namespace(dto.getNamespace()) + .serviceInstanceName(dto.getServiceInstanceName()) + .serviceKeyName(dto.getServiceKeyName()) + .logLevel(dto.getLogLevel()) + .isFailSafe(dto.isFailSafe()) + .build(); + } +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceHttpClient.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceHttpClient.java new file mode 100644 index 0000000000..879df129ea --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceHttpClient.java @@ -0,0 +1,148 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.time.Duration; +import java.util.List; +import java.util.Set; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.common.util.JsonUtil; +import org.cloudfoundry.multiapps.controller.persistence.Messages; +import org.cloudfoundry.multiapps.controller.persistence.model.ExternalOperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.util.CloudLoggingServiceUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.PrematureCloseException; +import reactor.util.retry.Retry; + +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@Named("cloudLoggingServiceHttpClient") +public class CloudLoggingServiceHttpClient { + + private static final Logger LOGGER = LoggerFactory.getLogger(CloudLoggingServiceHttpClient.class); + private static final int MAX_RETRY_ATTEMPTS = 4; + private static final Duration INITIAL_RETRY_BACKOFF = Duration.ofMillis(500); + private static final Duration MAX_RETRY_BACKOFF = Duration.ofSeconds(10); + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(30); + private static final Set RETRYABLE_STATUS_CODES = Set.of(408, 425, 429, 500, 502, 503, 504); + + public void sendLogsToCloudLoggingService(LoggingConfiguration loggingConfiguration, WebClient webClient, + List logEntryBatch) { + try { + ResponseEntity response = executeSendLogHttpRequest(webClient, logEntryBatch); + if (hasRequestFailed(response)) { + CloudLoggingServiceUtil.logErrorOrThrowExceptionBasedOnFailSafe(loggingConfiguration, LOGGER, + Messages.FAILED_TO_SEND_LOG_MESSAGE_TO_CLS); + } + } catch (RuntimeException e) { + CloudLoggingServiceUtil.logErrorOrThrowExceptionBasedOnFailSafe(loggingConfiguration, LOGGER, + Messages.FAILED_TO_SEND_LOG_MESSAGE_TO_CLS + ": " + + describeFailure(e)); + } + } + + public WebClient createWebClientWithMtls(LoggingConfiguration loggingConfiguration) { + SslContext sslContext = getSslContext(loggingConfiguration); + if (sslContext == null) { + return null; + } + HttpClient httpClient = HttpClient.create() + .secure(sslSpec -> sslSpec.sslContext(sslContext)); + + return WebClient.builder() + .baseUrl(loggingConfiguration.getEndpointUrl()) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); + } + + private SslContext getSslContext(LoggingConfiguration loggingConfiguration) { + try (InputStream serverCaStream = getCredentialInputStream(loggingConfiguration.getServerCa()); + InputStream clientCertStream = getCredentialInputStream(loggingConfiguration.getClientCert()); + InputStream clientKeyStream = getCredentialInputStream(loggingConfiguration.getClientKey())) { + return SslContextBuilder.forClient() + .keyManager(clientCertStream, clientKeyStream) + .trustManager(serverCaStream) + .build(); + } catch (IOException | IllegalArgumentException e) { + // Netty's SslContextBuilder throws IllegalArgumentException for malformed PEM material + // (e.g. "Input stream not contain valid certificates."). Catch it alongside IOException + // so cert-format errors honor failSafe instead of bubbling up as an unchecked exception. + CloudLoggingServiceUtil.logErrorOrThrowExceptionBasedOnFailSafe(loggingConfiguration, LOGGER, e.getMessage()); + return null; + } + } + + private InputStream getCredentialInputStream(String credential) { + return new ByteArrayInputStream((credential.getBytes(StandardCharsets.UTF_8))); + } + + private ResponseEntity executeSendLogHttpRequest(WebClient webClient, List logEntryBatch) { + return webClient.post() + .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) + .bodyValue(JsonUtil.toJson(logEntryBatch)) + .retrieve() + .toBodilessEntity() + .timeout(REQUEST_TIMEOUT) + .retryWhen(buildRetrySpec()) + .block(); + } + + private Retry buildRetrySpec() { + return Retry.backoff(MAX_RETRY_ATTEMPTS, INITIAL_RETRY_BACKOFF) + .maxBackoff(MAX_RETRY_BACKOFF) + .jitter(0.5d) + .filter(this::isRetryableError) + .doBeforeRetry(retrySignal -> LOGGER.warn(MessageFormat.format(Messages.RETRYING_SEND_LOGS_TO_CLS, + describeFailure(retrySignal.failure())))) + .onRetryExhaustedThrow((spec, retrySignal) -> retrySignal.failure()); + } + + private boolean isRetryableError(Throwable throwable) { + if (throwable instanceof WebClientResponseException responseException) { + return RETRYABLE_STATUS_CODES.contains(responseException.getStatusCode() + .value()); + } + if (throwable instanceof WebClientRequestException) { + return true; + } + return throwable instanceof PrematureCloseException || throwable instanceof IOException; + } + + private String describeFailure(Throwable throwable) { + if (throwable instanceof WebClientResponseException responseException) { + String retryAfter = responseException.getHeaders() + .getFirst(HttpHeaders.RETRY_AFTER); + return MessageFormat.format("HTTP {0} {1}{2}", responseException.getStatusCode() + .value(), + responseException.getStatusText(), + retryAfter != null ? " (Retry-After=" + retryAfter + ")" : ""); + } + return throwable.getClass() + .getSimpleName() + ": " + throwable.getMessage(); + } + + private boolean hasRequestFailed(ResponseEntity response) { + if (response == null) { + return true; + } + int statusCode = response.getStatusCode() + .value(); + return statusCode < 200 || statusCode > 299; + } +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceMessageConverter.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceMessageConverter.java new file mode 100644 index 0000000000..fba63fe732 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceMessageConverter.java @@ -0,0 +1,121 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.persistence.Messages; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.util.CloudLoggingServiceUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Named("cloudLoggingServiceMessageConverter") +public class CloudLoggingServiceMessageConverter { + private static final Logger LOGGER = LoggerFactory.getLogger(CloudLoggingServiceMessageConverter.class); + // SAP Cloud Logging ingest endpoint accepts payloads up to ~4 MB; 3.5 MB leaves headroom for JSON envelope and HTTP framing. + private static final Pattern MESSAGE_LOG_DATE_PATTERN = Pattern.compile("^#([^#\\r\\n]*)#", Pattern.MULTILINE); + private static final Pattern MESSAGE_LOG_LEVEL_PATTERN = Pattern.compile("^#[^#\\r\\n]*#[^#\\r\\n]*#([^#\\r\\n]*)#", Pattern.MULTILINE); + private static final Pattern MESSAGE_LOG_NAME = Pattern.compile("^#[^#\\r\\n]*#[^#\\r\\n]*#[^#\\r\\n]*#([^#\\r\\n]*)#", + Pattern.MULTILINE); + + private static final String MESSAGE_SPLITTING_REGEX = "(?m)^#[^#\\r\\n]*#[^#\\r\\n]*#[^#\\r\\n]*#[^#\\r\\n]*#[^#\\r\\n]*#(?:\\r?\\n)?"; + + public Optional extractLogName(String message) { + Matcher matcher = MESSAGE_LOG_NAME.matcher(message); + if (!matcher.find()) { + return Optional.empty(); + } + String raw = matcher.group(1); + return Optional.of(raw.substring(raw.indexOf(".") + 1)); + } + + public Map> getLogsFromOperationLogEntry( + LoggingConfiguration loggingConfiguration, String log) { + + List logLevels = getLogLevels(log); + List logDates = getLogDate(log); + if (logLevels.isEmpty()) { + return new EnumMap<>(LogLevel.class); + } + + List messages = splitNonBlankMessages(log); + if (!areParallelListsConsistent(messages, logLevels, logDates)) { + CloudLoggingServiceUtil.logErrorOrThrowExceptionBasedOnFailSafe(loggingConfiguration, LOGGER, Messages.INVALID_LOG_FILE); + return Map.of(); + } + + return groupByLogLevel(messages, logLevels, logDates); + } + + private List splitNonBlankMessages(String log) { + return Arrays.stream(log.split(MESSAGE_SPLITTING_REGEX)) + .filter(m -> !m.isBlank()) + .toList(); + } + + private boolean areParallelListsConsistent(List messages, List logLevels, List logDates) { + return messages.size() <= logLevels.size() && messages.size() <= logDates.size(); + } + + private Map> groupByLogLevel(List messages, + List logLevels, + List logDates) { + Map> result = new EnumMap<>(LogLevel.class); + for (int i = 0; i < messages.size(); i++) { + LogLevel level = LogLevel.get(logLevels.get(i)); + OperationLogsExporter.OperationLog entry = buildOperationLog(messages.get(i), logDates.get(i)); + result.computeIfAbsent(level, k -> new ArrayList<>()) + .add(entry); + } + return result; + } + + private OperationLogsExporter.OperationLog buildOperationLog(String rawMessage, LocalDateTime date) { + return new OperationLogsExporter.OperationLog(extractMessage(rawMessage), date); + } + + private List getLogLevels(String log) { + Matcher matcher = MESSAGE_LOG_LEVEL_PATTERN.matcher(log); + List logLevels = new ArrayList<>(); + + while (matcher.find()) { + logLevels.add(matcher.group(1)); + } + + return logLevels; + } + + private List getLogDate(String log) { + Matcher matcher = MESSAGE_LOG_DATE_PATTERN.matcher(log); + List logLevels = new ArrayList<>(); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd HH:mm:ss.SSS"); + + while (matcher.find()) { + LocalDateTime dateTime = LocalDateTime.parse(matcher.group(1), formatter); + logLevels.add(dateTime); + } + + return logLevels; + } + + private String extractMessage(String message) { + String trimmed = message.substring(message.indexOf("]") + 1) + .trim(); + if (trimmed.isEmpty()) { + return message; + } else { + return trimmed.substring(0, trimmed.length() - 1); + } + } +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporter.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporter.java new file mode 100644 index 0000000000..5668a82236 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporter.java @@ -0,0 +1,281 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.common.util.JsonUtil; +import org.cloudfoundry.multiapps.controller.persistence.Messages; +import org.cloudfoundry.multiapps.controller.persistence.model.ExternalOperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableExternalOperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.OperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.util.CloudLoggingServiceUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.reactive.function.client.WebClient; + +@Named("operationLogsExporter") +public class OperationLogsExporter { + + private static final Logger LOGGER = LoggerFactory.getLogger(OperationLogsExporter.class); + + private static final int CLIENT_CACHE_MAX_SIZE = 256; + private static final long MAX_LIMIT_REQUEST_SIZE_BYTES = 3 * 1024 * 1024 + 512 * 1024; + + private static final Map clientCache = Collections.synchronizedMap( + new LinkedHashMap<>(CLIENT_CACHE_MAX_SIZE, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > CLIENT_CACHE_MAX_SIZE; + } + }); + + private final ProcessLogsPersistenceService processLogsPersistenceService; + private final CloudLoggingServiceHttpClient cloudLoggingServiceHttpClient; + private final CloudLoggingServiceMessageConverter cloudLoggingServiceMessageConverter; + + public OperationLogsExporter(ProcessLogsPersistenceService processLogsPersistenceService, + CloudLoggingServiceHttpClient cloudLoggingServiceHttpClient, + CloudLoggingServiceMessageConverter cloudLoggingServiceMessageConverter) { + this.processLogsPersistenceService = processLogsPersistenceService; + this.cloudLoggingServiceHttpClient = cloudLoggingServiceHttpClient; + this.cloudLoggingServiceMessageConverter = cloudLoggingServiceMessageConverter; + } + + public void sendLogsToCloudLoggingService(LoggingConfiguration loggingConfiguration, String message) { + List> externalOperationLogEntryBatches = getExternalOperationLogEntryBatches(loggingConfiguration, + message); + + WebClient cloudLoggingServiceWebClient = getCloudLoggingServiceWebClient(loggingConfiguration); + if (cloudLoggingServiceWebClient == null) { + return; + } + + sendLogsToCloudLoggingService(externalOperationLogEntryBatches, cloudLoggingServiceWebClient, loggingConfiguration); + } + + public void info(LoggingConfiguration loggingConfiguration, String message) { + sendLogsToCloudLoggingService(loggingConfiguration, message, LogLevel.INFO); + } + + public void warn(LoggingConfiguration loggingConfiguration, String message) { + sendLogsToCloudLoggingService(loggingConfiguration, message, LogLevel.WARN); + } + + public void error(LoggingConfiguration loggingConfiguration, String message) { + sendLogsToCloudLoggingService(loggingConfiguration, message, LogLevel.ERROR); + } + + public void debug(LoggingConfiguration loggingConfiguration, String message) { + sendLogsToCloudLoggingService(loggingConfiguration, message, LogLevel.DEBUG); + } + + public void trace(LoggingConfiguration loggingConfiguration, String message) { + sendLogsToCloudLoggingService(loggingConfiguration, message, LogLevel.TRACE); + } + + public void sendLogsToCloudLoggingService(LoggingConfiguration loggingConfiguration, String message, LogLevel level) { + if (loggingConfiguration == null) { + return; + } + List allowedLevels = LogLevel.getLogLevelLoggingType() + .get(loggingConfiguration.getLogLevel()); + if (allowedLevels == null || !allowedLevels.contains(level)) { + return; + } + WebClient cloudLoggingServiceWebClient = getCloudLoggingServiceWebClient(loggingConfiguration); + if (cloudLoggingServiceWebClient == null) { + return; + } + ExternalOperationLogEntry entry = ImmutableExternalOperationLogEntry.builder() + .timestamp(String.valueOf(LocalDateTime.now() + .atOffset( + ZoneOffset.UTC))) + .message(message) + .id(UUID.randomUUID() + .toString()) + .operationLogName( + cloudLoggingServiceMessageConverter.extractLogName(message) + .orElse("")) + .correlationId(loggingConfiguration.getOperationId()) + .level(level.name()) + .build(); + sendLogsToCloudLoggingService(getLogEntryBatches(List.of(entry)), cloudLoggingServiceWebClient, + loggingConfiguration); + } + + public void sendLogsToCloudLoggingService(LoggingConfiguration loggingConfiguration, OperationLogEntry operationLogEntry) { + if (loggingConfiguration == null) { + return; + } + List> externalOperationLogEntryBatches = getExternalOperationLogEntryBatches(loggingConfiguration, + operationLogEntry); + + WebClient cloudLoggingServiceWebClient = getCloudLoggingServiceWebClient(loggingConfiguration); + if (cloudLoggingServiceWebClient == null) { + return; + } + + sendLogsToCloudLoggingService(externalOperationLogEntryBatches, cloudLoggingServiceWebClient, loggingConfiguration); + } + + public List getUnsendProcessLogs(LoggingConfiguration loggingConfiguration) { + try { + return processLogsPersistenceService.listOperationLogsBySpaceAndOperationId(loggingConfiguration.getMtaSpaceId(), + loggingConfiguration.getOperationId()); + } catch (FileStorageException e) { + CloudLoggingServiceUtil.logErrorOrThrowExceptionBasedOnFailSafe(loggingConfiguration, LOGGER, e.getMessage()); + return List.of(); + } + } + + public void removeClientFromCache(String operationId) { + clientCache.remove(operationId); + } + + private List> getExternalOperationLogEntryBatches(LoggingConfiguration loggingConfiguration, + String message) { + Map> operationLogs = cloudLoggingServiceMessageConverter.getLogsFromOperationLogEntry( + loggingConfiguration, message); + Map> filteredOperationLogs = removeLogsWithUnwantedLogLevel(loggingConfiguration, operationLogs); + List externalOperationLogEntries = getExternalOperationLogEntries(loggingConfiguration, + filteredOperationLogs, message); + return getLogEntryBatches(externalOperationLogEntries); + } + + private List getExternalOperationLogEntries(LoggingConfiguration loggingConfiguration, + Map> filteredOperationLogs, + String message) { + List externalOperationLogEntries = new ArrayList<>(); + String logName = cloudLoggingServiceMessageConverter.extractLogName(message) + .orElse(""); + + for (Map.Entry> operationLog : filteredOperationLogs.entrySet()) { + for (OperationLog log : operationLog.getValue()) { + externalOperationLogEntries.add(convertToExternalLogEntry(loggingConfiguration, log, operationLog.getKey(), logName)); + } + } + + return externalOperationLogEntries; + } + + private List> getExternalOperationLogEntryBatches(LoggingConfiguration loggingConfiguration, + OperationLogEntry operationLogEntry) { + Map> operationLogs = cloudLoggingServiceMessageConverter.getLogsFromOperationLogEntry( + loggingConfiguration, + operationLogEntry.getOperationLog()); + Map> filteredOperationLogs = removeLogsWithUnwantedLogLevel(loggingConfiguration, operationLogs); + List externalOperationLogEntries = new ArrayList<>(); + + for (Map.Entry> operationLog : filteredOperationLogs.entrySet()) { + for (OperationLog log : operationLog.getValue()) { + externalOperationLogEntries.add( + convertToExternalLogEntry(operationLogEntry, log, operationLog.getKey(), loggingConfiguration.getOperationId())); + } + } + return getLogEntryBatches(externalOperationLogEntries); + } + + private Map> removeLogsWithUnwantedLogLevel(LoggingConfiguration loggingConfiguration, + Map> operationLogs) { + List allowedLevelsToLog = LogLevel.getLogLevelLoggingType() + .get(loggingConfiguration.getLogLevel()); + + return operationLogs.entrySet() + .stream() + .filter(operationLog -> allowedLevelsToLog.contains(operationLog.getKey())) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue + )); + } + + public List> getLogEntryBatches(List externalLogEntries) { + List> batches = new ArrayList<>(); + List currentBatch = new ArrayList<>(); + long currentChunkSize = 0L; + + for (ExternalOperationLogEntry entry : externalLogEntries) { + String entryJson = JsonUtil.toJson(entry); + int entrySize = entryJson.getBytes(StandardCharsets.UTF_8).length; + + if (currentChunkSize + entrySize > MAX_LIMIT_REQUEST_SIZE_BYTES && !currentBatch.isEmpty()) { + batches.add(new ArrayList<>(currentBatch)); + currentBatch.clear(); + currentChunkSize = 0L; + } + + currentBatch.add(entry); + currentChunkSize += entrySize; + } + if (!currentBatch.isEmpty()) { + batches.add(currentBatch); + } + return batches; + } + + private WebClient getCloudLoggingServiceWebClient(LoggingConfiguration loggingConfiguration) { + WebClient webClient = null; + + if (!clientCache.containsKey(loggingConfiguration.getOperationId())) { + webClient = cloudLoggingServiceHttpClient.createWebClientWithMtls(loggingConfiguration); + clientCache.put(loggingConfiguration.getOperationId(), webClient); + LOGGER.debug(MessageFormat.format(Messages.CREATING_WEBCLIENT_WITH_MTLS_CONFIGURATION_FOR_ENDPOINT_1, + loggingConfiguration.getEndpointUrl())); + } else { + webClient = clientCache.get(loggingConfiguration.getOperationId()); + } + + return webClient; + } + + private void sendLogsToCloudLoggingService(List> externalOperationLogEntryBatches, + WebClient webClient, LoggingConfiguration loggingConfiguration) { + for (List logEntryBatch : externalOperationLogEntryBatches) { + cloudLoggingServiceHttpClient.sendLogsToCloudLoggingService(loggingConfiguration, webClient, logEntryBatch); + } + } + + private ExternalOperationLogEntry convertToExternalLogEntry(OperationLogEntry operationLogEntry, OperationLog operationLog, + LogLevel level, String operationId) { + return ImmutableExternalOperationLogEntry.builder() + .timestamp(String.valueOf(operationLog.dateTime() + .atOffset(ZoneOffset.UTC))) + .message(operationLog.log()) + .id(UUID.randomUUID() + .toString()) + .operationLogName(operationLogEntry.getOperationLogName()) + .correlationId(operationId) + .level(level.name()) + .build(); + } + + private ExternalOperationLogEntry convertToExternalLogEntry(LoggingConfiguration loggingConfiguration, OperationLog operationLog, + LogLevel level, String logName) { + return ImmutableExternalOperationLogEntry.builder() + .timestamp(String.valueOf(operationLog.dateTime() + .atOffset(ZoneOffset.UTC))) + .message(operationLog.log()) + .id(UUID.randomUUID() + .toString()) + .operationLogName(logName) + .correlationId(loggingConfiguration.getOperationId()) + .level(level.name()) + .build(); + } + + public record OperationLog(String log, LocalDateTime dateTime) { + + } +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersister.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersister.java index 66474371a5..8ba23b5f91 100644 --- a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersister.java +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersister.java @@ -1,20 +1,22 @@ package org.cloudfoundry.multiapps.controller.persistence.services; -import jakarta.inject.Inject; -import jakarta.inject.Named; -import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableOperationLogEntry; -import org.cloudfoundry.multiapps.controller.persistence.model.OperationLogEntry; -import org.springframework.scheduling.annotation.Async; - import java.time.LocalDateTime; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableOperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.OperationLogEntry; +import org.springframework.scheduling.annotation.Async; + @Named("processLoggerPersister") public class ProcessLoggerPersister { + private static final String OPERATION_LOG_NAME = "OPERATION.log"; private final ProcessLoggerProvider processLoggerProvider; private final ProcessLogsPersistenceService processLogsPersistenceService; @@ -27,13 +29,48 @@ public ProcessLoggerPersister(ProcessLoggerProvider processLoggerProvider, @Async("asyncExecutor") public void persistLogs(String correlationId, String taskId) { + List processLoggers = processLoggerProvider.getExistingLoggers(correlationId, taskId); - Map processLogsMessages = new HashMap<>(); + Map processLogsMessages = getProcessLogsMessages(processLoggers); + if (processLogsMessages.isEmpty()) { + return; + } + + OperationLogEntry operationLogEntryWithExistingData = processLoggers.getFirst() + .getOperationLogEntry(); + + for (var processLogsMessage : processLogsMessages.entrySet()) { + OperationLogEntry operationLogEntry = ImmutableOperationLogEntry.copyOf(operationLogEntryWithExistingData) + .withId(UUID.randomUUID() + .toString()) + .withOperationLogName(processLogsMessage.getKey()) + .withOperationLog(processLogsMessage.getValue() + .toString()) + .withModified(LocalDateTime.now()); + + processLogsPersistenceService.persistLog(operationLogEntry); + } + } + + public List getApplicationProcessLogsMessages(String correlationId, String taskId) { + List processLoggers = processLoggerProvider.getExistingLoggers(correlationId, taskId); + Map processLogsMessages = getProcessLogsMessages(processLoggers); + processLogsMessages.remove(OPERATION_LOG_NAME); + + return processLogsMessages.values() + .stream() + .map(StringBuilder::toString) + .toList(); + } + + public Map getProcessLogsMessages(List processLoggers) { if (processLoggers.isEmpty()) { - return; + return Collections.emptyMap(); } + Map processLogsMessages = new HashMap<>(); + for (ProcessLogger processLogger : processLoggers) { if (processLogsMessages.containsKey(processLogger.getOperationLogEntry() .getOperationLogName())) { @@ -49,19 +86,6 @@ public void persistLogs(String correlationId, String taskId) { processLoggerProvider.removeProcessLoggerFromCache(processLogger); } - - OperationLogEntry operationLogEntryWithExistingData = processLoggers.get(0) - .getOperationLogEntry(); - - for (var processLogsMessage : processLogsMessages.entrySet()) { - OperationLogEntry operationLogEntry = ImmutableOperationLogEntry.copyOf(operationLogEntryWithExistingData) - .withId(UUID.randomUUID() - .toString()) - .withOperationLogName(processLogsMessage.getKey()) - .withOperationLog(processLogsMessage.getValue() - .toString()) - .withModified(LocalDateTime.now()); - processLogsPersistenceService.persistLog(operationLogEntry); - } + return processLogsMessages; } } diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/util/CloudLoggingServiceUtil.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/util/CloudLoggingServiceUtil.java new file mode 100644 index 0000000000..4bc7949205 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/util/CloudLoggingServiceUtil.java @@ -0,0 +1,19 @@ +package org.cloudfoundry.multiapps.controller.persistence.util; + +import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.slf4j.Logger; + +public class CloudLoggingServiceUtil { + + private CloudLoggingServiceUtil() { + } + + public static void logErrorOrThrowExceptionBasedOnFailSafe(LoggingConfiguration loggingConfiguration, Logger logger, String message) { + if (loggingConfiguration.isFailSafe()) { + logger.error(message); + } else { + throw new SLException(message); + } + } +} diff --git a/multiapps-controller-persistence/src/main/resources/META-INF/persistence.xml b/multiapps-controller-persistence/src/main/resources/META-INF/persistence.xml index 8c94332836..e0ac1df4e2 100644 --- a/multiapps-controller-persistence/src/main/resources/META-INF/persistence.xml +++ b/multiapps-controller-persistence/src/main/resources/META-INF/persistence.xml @@ -16,6 +16,7 @@ org.cloudfoundry.multiapps.controller.persistence.dto.BackupDescriptorDto org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdownDto org.cloudfoundry.multiapps.controller.persistence.dto.SecretTokenDto + org.cloudfoundry.multiapps.controller.persistence.dto.LoggingConfigurationDto true diff --git a/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog-2.43.0-persistence.xml b/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog-2.43.0-persistence.xml new file mode 100644 index 0000000000..ba6d631545 --- /dev/null +++ b/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog-2.43.0-persistence.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog.xml b/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog.xml index d4379c650e..0592d75f42 100644 --- a/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog.xml +++ b/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog.xml @@ -44,4 +44,8 @@ + + + diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/LogLevelTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/LogLevelTest.java new file mode 100644 index 0000000000..3ebcf5ce45 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/LogLevelTest.java @@ -0,0 +1,98 @@ +package org.cloudfoundry.multiapps.controller.persistence.model; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LogLevelTest { + + static Stream testGet_withValidInput() { + return Stream.of(Arguments.of("INFO", LogLevel.INFO), Arguments.of("WARN", LogLevel.WARN), Arguments.of("DEBUG", LogLevel.DEBUG), + Arguments.of("ERROR", LogLevel.ERROR), Arguments.of("TRACE", LogLevel.TRACE)); + } + + @ParameterizedTest + @MethodSource + void testGet_withValidInput(String value, LogLevel expected) { + assertEquals(expected, LogLevel.get(value)); + } + + @Test + void testGet_returnsNullForUnknownValue() { + assertNull(LogLevel.get("UNKNOWN")); + } + + @Test + void testGet_returnsNullForNull() { + assertNull(LogLevel.get(null)); + } + + @Test + void testIsValid_ValidInput() { + assertTrue(LogLevel.isValid("INFO")); + } + + @Test + void testIsValid_InvalidInput() { + assertFalse(LogLevel.isValid("test")); + } + + @Test + void testGet_isCaseSensitive() { + assertNull(LogLevel.get("info")); + assertNull(LogLevel.get("Info")); + } + + static Stream testLogLevelLoggingType() { + return Stream.of( + Arguments.of(LogLevel.TRACE, List.of(LogLevel.TRACE, LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR)), + Arguments.of(LogLevel.DEBUG, List.of(LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR)), + Arguments.of(LogLevel.INFO, List.of(LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR)), + Arguments.of(LogLevel.WARN, List.of(LogLevel.WARN, LogLevel.ERROR)), + Arguments.of(LogLevel.ERROR, List.of(LogLevel.ERROR))); + } + + @ParameterizedTest + @MethodSource + void testLogLevelLoggingType(LogLevel level, List expectedAllowedLevels) { + Map> logLevelLoggingType = LogLevel.getLogLevelLoggingType(); + assertEquals(expectedAllowedLevels, logLevelLoggingType.get(level)); + } + + @Test + void testGetLogLevelLoggingTypeThatContainsAllLevels() { + Map> logLevelLoggingType = LogLevel.getLogLevelLoggingType(); + assertEquals(LogLevel.values().length, logLevelLoggingType.size()); + for (LogLevel level : LogLevel.values()) { + assertTrue(logLevelLoggingType.containsKey(level)); + } + } + + @Test + void testLogLevelLoggingTypeThatErrorOnlyIncludesError() { + List allowedForError = LogLevel.getLogLevelLoggingType() + .get(LogLevel.ERROR); + assertEquals(1, allowedForError.size()); + assertTrue(allowedForError.contains(LogLevel.ERROR)); + } + + @Test + void testLogLevelLoggingTypeThatTraceIncludesAll() { + List allowedForTrace = LogLevel.getLogLevelLoggingType() + .get(LogLevel.TRACE); + assertEquals(LogLevel.values().length, allowedForTrace.size()); + for (LogLevel level : LogLevel.values()) { + assertTrue(allowedForTrace.contains(level)); + } + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationServiceTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationServiceTest.java new file mode 100644 index 0000000000..cf42cb4353 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationServiceTest.java @@ -0,0 +1,174 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import java.util.List; + +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Persistence; + +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class CloudLoggingServiceConfigurationServiceTest { + + private static final String SPACE_ID_1 = "space-id-1"; + private static final String SPACE_ID_2 = "space-id-2"; + private static final String MTA_SPACE_1 = "mta-space-1"; + private static final String MTA_ID_1 = "mta-id-1"; + private static final String MTA_ID_2 = "mta-id-2"; + private static final String ID_1 = "id-1"; + private static final String ID_2 = "id-2"; + + private EntityManagerFactory entityManagerFactory; + private CloudLoggingServiceConfigurationService service; + + @BeforeEach + void setUp() { + entityManagerFactory = Persistence.createEntityManagerFactory("TestDefault"); + service = new CloudLoggingServiceConfigurationService(entityManagerFactory); + } + + @AfterEach + void tearDown() { + service.getAllCloudLoggingServiceConfigurationsFromSpace(SPACE_ID_1) + .forEach(config -> service.deleteCloudLoggingServiceConfiguration(config.getId())); + service.getAllCloudLoggingServiceConfigurationsFromSpace(SPACE_ID_2) + .forEach(config -> service.deleteCloudLoggingServiceConfiguration(config.getId())); + entityManagerFactory.close(); + } + + @Test + void testStoreAndGetConfiguration() { + LoggingConfiguration config = buildConfiguration(ID_1, SPACE_ID_1, MTA_SPACE_1, MTA_ID_1, "ns-1"); + service.storeCloudLoggingServiceConfiguration(config); + + LoggingConfiguration result = service.getCloudLoggingServiceConfiguration(MTA_SPACE_1, MTA_ID_1, "ns-1"); + + assertNotNull(result); + assertEquals(ID_1, result.getId()); + assertEquals(MTA_ID_1, result.getMtaId()); + assertEquals(MTA_SPACE_1, result.getMtaSpace()); + assertEquals(SPACE_ID_1, result.getMtaSpaceId()); + assertEquals(LogLevel.INFO, result.getLogLevel()); + } + + @Test + void testGetConfiguration_withNullNamespace() { + LoggingConfiguration config = buildConfiguration(ID_1, SPACE_ID_1, MTA_SPACE_1, MTA_ID_1, null); + service.storeCloudLoggingServiceConfiguration(config); + + LoggingConfiguration result = service.getCloudLoggingServiceConfiguration(MTA_SPACE_1, MTA_ID_1, null); + + assertNotNull(result); + assertEquals(ID_1, result.getId()); + } + + @Test + void testGetConfiguration_returnsNullWhenNotFound() { + LoggingConfiguration result = service.getCloudLoggingServiceConfiguration("nonexistent-space", "nonexistent-mta", "ns"); + + assertNull(result); + } + + @Test + void testDeleteConfiguration() { + LoggingConfiguration config = buildConfiguration(ID_1, SPACE_ID_1, MTA_SPACE_1, MTA_ID_1, "ns-2"); + service.storeCloudLoggingServiceConfiguration(config); + + service.deleteCloudLoggingServiceConfiguration(ID_1); + + assertNull(service.getCloudLoggingServiceConfiguration(MTA_SPACE_1, MTA_ID_1, "ns-2")); + } + + @Test + void testDeleteConfiguration_nonExistentIdDoesNotThrow() { + service.deleteCloudLoggingServiceConfiguration("nonexistent-id"); + } + + @Test + void testUpdateConfiguration() { + LoggingConfiguration config = buildConfiguration(ID_1, SPACE_ID_1, MTA_SPACE_1, MTA_ID_1, "ns-3"); + service.storeCloudLoggingServiceConfiguration(config); + + LoggingConfiguration updated = ImmutableLoggingConfiguration.builder() + .from(config) + .logLevel(LogLevel.ERROR) + .serviceInstanceName("updated-instance") + .build(); + service.updateCloudLoggingServiceConfiguration(updated); + + LoggingConfiguration result = service.getCloudLoggingServiceConfiguration(MTA_SPACE_1, MTA_ID_1, "ns-3"); + assertNotNull(result); + assertEquals(LogLevel.ERROR, result.getLogLevel()); + assertEquals("updated-instance", result.getServiceInstanceName()); + } + + @Test + void testUpdateConfiguration_withNullNamespace() { + LoggingConfiguration config = buildConfiguration(ID_1, SPACE_ID_1, MTA_SPACE_1, MTA_ID_1, null); + service.storeCloudLoggingServiceConfiguration(config); + + LoggingConfiguration updated = ImmutableLoggingConfiguration.builder() + .from(config) + .logLevel(LogLevel.WARN) + .build(); + service.updateCloudLoggingServiceConfiguration(updated); + + LoggingConfiguration result = service.getCloudLoggingServiceConfiguration(MTA_SPACE_1, MTA_ID_1, null); + assertNotNull(result); + assertEquals(LogLevel.WARN, result.getLogLevel()); + } + + @Test + void testGetAllConfigurationsFromSpace_returnsAllForSpace() { + service.storeCloudLoggingServiceConfiguration(buildConfiguration(ID_1, SPACE_ID_1, MTA_SPACE_1, MTA_ID_1, "ns-4")); + service.storeCloudLoggingServiceConfiguration(buildConfiguration(ID_2, SPACE_ID_1, MTA_SPACE_1, MTA_ID_2, "ns-5")); + + List results = service.getAllCloudLoggingServiceConfigurationsFromSpace(SPACE_ID_1); + + assertEquals(2, results.size()); + } + + @Test + void testGetAllConfigurationsFromSpace_returnsEmptyListWhenNoneExist() { + List results = service.getAllCloudLoggingServiceConfigurationsFromSpace("unknown-space"); + + assertEquals(0, results.size()); + } + + @Test + void testGetAllConfigurationsFromSpace_doesNotReturnConfigurationsFromOtherSpaces() { + service.storeCloudLoggingServiceConfiguration(buildConfiguration(ID_1, SPACE_ID_1, MTA_SPACE_1, MTA_ID_1, "ns-6")); + service.storeCloudLoggingServiceConfiguration(buildConfiguration(ID_2, SPACE_ID_2, "mta-space-2", MTA_ID_2, "ns-7")); + + List results = service.getAllCloudLoggingServiceConfigurationsFromSpace(SPACE_ID_1); + + assertEquals(1, results.size()); + assertEquals(ID_1, results.get(0) + .getId()); + } + + private LoggingConfiguration buildConfiguration(String id, String mtaSpaceId, String mtaSpace, String mtaId, String namespace) { + return ImmutableLoggingConfiguration.builder() + .id(id) + .mtaSpaceId(mtaSpaceId) + .mtaSpace(mtaSpace) + .mtaId(mtaId) + .mtaOrg("mta-org") + .targetSpace("target-space") + .targetOrg("target-org") + .serviceInstanceName("my-cls-instance") + .serviceKeyName("my-cls-key") + .logLevel(LogLevel.INFO) + .isFailSafe(true) + .namespace(namespace) + .build(); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceHttpClientTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceHttpClientTest.java new file mode 100644 index 0000000000..60c00b572d --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceHttpClientTest.java @@ -0,0 +1,228 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.controller.persistence.model.ExternalOperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableExternalOperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.PrematureCloseException; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CloudLoggingServiceHttpClientTest { + + private CloudLoggingServiceHttpClient client; + + @BeforeEach + void setUp() { + client = new CloudLoggingServiceHttpClient(); + } + + // --- createWebClientWithMtls: SSL context construction failure paths --- + // + // The success path (valid PEM material) is exercised end-to-end in higher-level + // integration tests that own real test certificates. Unit-testing it here would + // require generating an X.509 cert at runtime, which adds a non-trivial dependency + // for marginal value. We cover the two failure branches that route through + // CloudLoggingServiceUtil. + + @Test + void createWebClientWithMtls_failSafeTrue_returnsNullOnInvalidCredentials() { + LoggingConfiguration config = configBuilder(true).serverCa("not a pem") + .clientCert("not a pem") + .clientKey("not a pem") + .build(); + + WebClient webClient = client.createWebClientWithMtls(config); + + assertNull(webClient); + } + + @Test + void createWebClientWithMtls_failSafeFalse_throwsOnInvalidCredentials() { + LoggingConfiguration config = configBuilder(false).serverCa("not a pem") + .clientCert("not a pem") + .clientKey("not a pem") + .build(); + + assertThrows(SLException.class, () -> client.createWebClientWithMtls(config)); + } + + // --- sendLogsToCloudLoggingService: happy path --- + + @Test + void sendLogs_2xxResponse_doesNotThrow() { + WebClient webClient = stubWebClient(req -> response(HttpStatus.OK)); + + assertDoesNotThrow(() -> client.sendLogsToCloudLoggingService(configBuilder(true).build(), webClient, sampleBatch())); + } + + @Test + void sendLogs_sendsJsonContentTypeHeader() { + AtomicInteger calls = new AtomicInteger(); + WebClient webClient = stubWebClient(req -> { + calls.incrementAndGet(); + assertEquals(MediaType.APPLICATION_JSON_VALUE, req.headers() + .getFirst(HttpHeaders.CONTENT_TYPE)); + return response(HttpStatus.OK); + }); + + client.sendLogsToCloudLoggingService(configBuilder(true).build(), webClient, sampleBatch()); + + assertEquals(1, calls.get()); + } + + // --- sendLogsToCloudLoggingService: failure modes --- + + @Test + void sendLogs_non2xxNonRetryable_failSafeTrue_doesNotThrow() { + // 404 is not in RETRYABLE_STATUS_CODES — the retry filter rejects it, + // and failSafe=true swallows the resulting WebClientResponseException. + WebClient webClient = stubWebClient(req -> response(HttpStatus.NOT_FOUND)); + + assertDoesNotThrow(() -> client.sendLogsToCloudLoggingService(configBuilder(true).build(), webClient, sampleBatch())); + } + + @Test + void sendLogs_non2xxNonRetryable_failSafeFalse_throwsSLException() { + WebClient webClient = stubWebClient(req -> response(HttpStatus.NOT_FOUND)); + + assertThrows(SLException.class, + () -> client.sendLogsToCloudLoggingService(configBuilder(false).build(), webClient, sampleBatch())); + } + + // --- sendLogsToCloudLoggingService: retry behavior --- + + @ParameterizedTest + @ValueSource(ints = { 408, 425, 429, 500, 502, 503, 504 }) + void sendLogs_retryableStatus_isRetriedUntilSuccess(int retryableStatus) { + AtomicInteger attempts = new AtomicInteger(); + WebClient webClient = stubWebClient(req -> { + int n = attempts.incrementAndGet(); + return n == 1 ? response(HttpStatus.valueOf(retryableStatus)) : response(HttpStatus.OK); + }); + + client.sendLogsToCloudLoggingService(configBuilder(true).build(), webClient, sampleBatch()); + + assertEquals(2, attempts.get()); + } + + @Test + void sendLogs_persistentRetryableStatus_failSafeTrue_doesNotThrowAfterExhaustion() { + AtomicInteger attempts = new AtomicInteger(); + WebClient webClient = stubWebClient(req -> { + attempts.incrementAndGet(); + return response(HttpStatus.SERVICE_UNAVAILABLE); + }); + + assertDoesNotThrow(() -> client.sendLogsToCloudLoggingService(configBuilder(true).build(), webClient, sampleBatch())); + // 1 initial + up to 4 retries + assertTrue(attempts.get() >= 2, "expected at least one retry, got " + attempts.get()); + } + + @Test + void sendLogs_ioExceptionFromExchange_isRetried() { + AtomicInteger attempts = new AtomicInteger(); + WebClient webClient = stubWebClient(req -> { + int n = attempts.incrementAndGet(); + return n == 1 ? Mono.error(new IOException("connection reset")) : response(HttpStatus.OK); + }); + + client.sendLogsToCloudLoggingService(configBuilder(true).build(), webClient, sampleBatch()); + + assertEquals(2, attempts.get()); + } + + @Test + void sendLogs_prematureCloseException_isRetried() { + AtomicInteger attempts = new AtomicInteger(); + WebClient webClient = stubWebClient(req -> { + int n = attempts.incrementAndGet(); + return n == 1 ? Mono.error(PrematureCloseException.TEST_EXCEPTION) : response(HttpStatus.OK); + }); + + client.sendLogsToCloudLoggingService(configBuilder(true).build(), webClient, sampleBatch()); + + assertEquals(2, attempts.get()); + } + + @Test + void sendLogs_nonRetryableRuntimeException_failSafeTrue_doesNotThrow_andDoesNotRetry() { + AtomicInteger attempts = new AtomicInteger(); + WebClient webClient = stubWebClient(req -> { + attempts.incrementAndGet(); + return Mono.error(new IllegalStateException("boom")); + }); + + assertDoesNotThrow(() -> client.sendLogsToCloudLoggingService(configBuilder(true).build(), webClient, sampleBatch())); + assertEquals(1, attempts.get()); + } + + @Test + void sendLogs_nonRetryableRuntimeException_failSafeFalse_throwsSLException() { + WebClient webClient = stubWebClient(req -> Mono.error(new IllegalStateException("boom"))); + + assertThrows(SLException.class, + () -> client.sendLogsToCloudLoggingService(configBuilder(false).build(), webClient, sampleBatch())); + } + + // --- helpers --- + + private static WebClient stubWebClient(Function> handler) { + ExchangeFunction exchange = handler::apply; + return WebClient.builder() + .baseUrl("https://cls.example.com") + .exchangeFunction(exchange) + .build(); + } + + private static Mono response(HttpStatus status) { + return Mono.just(ClientResponse.create(status) + .build()); + } + + private static List sampleBatch() { + return List.of(ImmutableExternalOperationLogEntry.builder() + .id("id-1") + .timestamp("2024-01-15T10:30:00Z") + .message("hello") + .operationLogName("svc") + .correlationId("op-1") + .level(LogLevel.INFO.name()) + .build()); + } + + private static ImmutableLoggingConfiguration.Builder configBuilder(boolean failSafe) { + return ImmutableLoggingConfiguration.builder() + .operationId("op-1") + .mtaSpaceId("space-1") + .logLevel(LogLevel.INFO) + .isFailSafe(failSafe) + .endpointUrl("https://cls.example.com") + .serverCa("server-ca") + .clientCert("client-cert") + .clientKey("client-key"); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceMessageConverterTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceMessageConverterTest.java new file mode 100644 index 0000000000..e2b4e01007 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceMessageConverterTest.java @@ -0,0 +1,189 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CloudLoggingServiceMessageConverterTest { + + private static final String DATE = "2024 01 15 10:30:00.000"; + private static final LocalDateTime EXPECTED_DATE = LocalDateTime.of(2024, 1, 15, 10, 30, 0); + + private CloudLoggingServiceMessageConverter converter; + + @BeforeEach + void setUp() { + converter = new CloudLoggingServiceMessageConverter(); + } + + // --- extractLogName --- + + @Test + void extractLogName_extractsSuffixAfterFirstDot() { + Optional result = converter.extractLogName(logLine(DATE, "INFO", "deploy-app.hello-backend", "[main] msg")); + + assertEquals(Optional.of("hello-backend"), result); + } + + @Test + void extractLogName_returnsEverythingAfterFirstDotWhenMultiple() { + Optional result = converter.extractLogName(logLine(DATE, "INFO", "a.b.c", "[main] msg")); + + assertEquals(Optional.of("b.c"), result); + } + + @Test + void extractLogName_returnsEmptyWhenPatternDoesNotMatch() { + Optional result = converter.extractLogName("not a log line\n"); + + assertEquals(Optional.empty(), result); + } + + @Test + void extractLogName_returnsEmptyForEmptyString() { + Optional result = converter.extractLogName(""); + + assertEquals(Optional.empty(), result); + } + + // --- getLogsFromOperationLogEntry: happy path --- + + @Test + void getLogsFromOperationLogEntry_singleInfoLine_groupedByLevel() { + String input = logLine(DATE, "INFO", "deploy-app.svc", "[main] hello"); + + Map> result = converter.getLogsFromOperationLogEntry(buildConfig(true), + input); + + List infos = result.get(LogLevel.INFO); + assertEquals(1, infos.size()); + // NOTE: extractMessage strips one trailing character after .trim() — likely a bug + // (the -1 was probably meant to handle a trailing newline that .trim() already removes). + // The test pins current behavior; flag for follow-up. + assertEquals("hell", infos.get(0) + .log()); + assertEquals(EXPECTED_DATE, infos.get(0) + .dateTime()); + } + + @Test + void getLogsFromOperationLogEntry_multipleLevels_groupedSeparately() { + String input = logLine(DATE, "INFO", "deploy-app.svc", "[t] i") + + logLine(DATE, "WARN", "deploy-app.svc", "[t] w") + + logLine(DATE, "ERROR", "deploy-app.svc", "[t] e"); + + Map> result = converter.getLogsFromOperationLogEntry(buildConfig(true), + input); + + assertEquals(1, result.get(LogLevel.INFO) + .size()); + assertEquals(1, result.get(LogLevel.WARN) + .size()); + assertEquals(1, result.get(LogLevel.ERROR) + .size()); + } + + @Test + void getLogsFromOperationLogEntry_sameLevelMultipleEntries_appendedToList() { + String input = logLine(DATE, "INFO", "deploy-app.svc", "[t] one") + + logLine(DATE, "INFO", "deploy-app.svc", "[t] two"); + + Map> result = converter.getLogsFromOperationLogEntry(buildConfig(true), + input); + + List infos = result.get(LogLevel.INFO); + assertEquals(2, infos.size()); + // See note in singleInfoLine_groupedByLevel about the trailing-character chop. + assertEquals("on", infos.get(0) + .log()); + assertEquals("tw", infos.get(1) + .log()); + } + + // --- getLogsFromOperationLogEntry: edge cases --- + + @Test + void getLogsFromOperationLogEntry_emptyInput_returnsEmptyMap() { + Map> result = converter.getLogsFromOperationLogEntry(buildConfig(true), + ""); + + assertTrue(result.isEmpty()); + } + + @Test + void getLogsFromOperationLogEntry_noHeaderLines_returnsEmptyMap() { + Map> result = converter.getLogsFromOperationLogEntry(buildConfig(true), + "free text without any header\n"); + + assertTrue(result.isEmpty()); + } + + @Test + void getLogsFromOperationLogEntry_unknownLogLevel_throwsNpe() { + // Production bug: LogLevel.get("FATAL") returns null, and the result EnumMap + // rejects null keys, so computeIfAbsent(null, …) throws NPE. The map type should either + // be HashMap or LogLevel.get should fall back to a default; flag for follow-up. + String input = logLine(DATE, "FATAL", "deploy-app.svc", "[t] unknown level"); + + assertThrows(NullPointerException.class, () -> converter.getLogsFromOperationLogEntry(buildConfig(true), input)); + } + + // --- failSafe behavior on malformed input --- + // + // The "more messages than levels" branch fires when the split produces more non-blank + // chunks than the header-line regex matches. Putting body text BEFORE the first header + // line (so the split's leading chunk has no header to pair with) reliably triggers it. + + @Test + void getLogsFromOperationLogEntry_moreMessagesThanLevels_failSafeTrue_returnsEmptyMap() { + String malformed = "orphan body before any header\n" + + "#" + DATE + "#org.example.Logger#INFO#deploy-app.svc#main#\n" + + "[t] body\n"; + + Map> result = converter.getLogsFromOperationLogEntry(buildConfig(true), + malformed); + + // failSafe=true: util logs and returns; converter then returns Map.of() + assertTrue(result.isEmpty()); + } + + @Test + void getLogsFromOperationLogEntry_moreMessagesThanLevels_failSafeFalse_throws() { + String malformed = "orphan body before any header\n" + + "#" + DATE + "#org.example.Logger#INFO#deploy-app.svc#main#\n" + + "[t] body\n"; + + assertThrows(SLException.class, () -> converter.getLogsFromOperationLogEntry(buildConfig(false), malformed)); + } + + // --- helpers --- + + private static String logLine(String date, String level, String logName, String text) { + return "#" + date + "#org.example.Logger#" + level + "#" + logName + "#main#\n" + text + "\n"; + } + + private static LoggingConfiguration buildConfig(boolean failSafe) { + return ImmutableLoggingConfiguration.builder() + .operationId("op") + .mtaSpaceId("space") + .logLevel(LogLevel.INFO) + .isFailSafe(failSafe) + .endpointUrl("https://cls.example.com") + .serverCa("ca") + .clientCert("cert") + .clientKey("key") + .build(); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporterTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporterTest.java new file mode 100644 index 0000000000..ed0cf48e03 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporterTest.java @@ -0,0 +1,377 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.controller.persistence.Messages; +import org.cloudfoundry.multiapps.controller.persistence.model.ExternalOperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableOperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.OperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.util.CloudLoggingServiceUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.slf4j.LoggerFactory; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +class OperationLogsExporterTest { + + private static final String OPERATION_ID = "op-123"; + private static final String SPACE_ID = "space-1"; + private static final String LOG_DATE = "2024 01 15 10:30:00.000"; + private static final String INFO_LOG = logLine(LOG_DATE, "INFO", "deploy-app.hello-backend", + "[main] Starting deployment"); + private static final String WARN_LOG = logLine(LOG_DATE, "WARN", "deploy-app.hello-backend", + "[main] Low memory"); + private static final String ERROR_LOG = logLine(LOG_DATE, "ERROR", "deploy-app.hello-backend", + "[main] Deployment failed"); + private static final String DEBUG_LOG = logLine(LOG_DATE, "DEBUG", "deploy-app.hello-backend", + "[main] Debug info"); + private static final String TRACE_LOG = logLine(LOG_DATE, "TRACE", "deploy-app.hello-backend", + "[main] Trace info"); + + @Mock + private ProcessLogsPersistenceService processLogsPersistenceService; + + private CapturingHttpClient httpClient; + private OperationLogsExporter exporter; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + httpClient = new CapturingHttpClient(); + exporter = new OperationLogsExporter(processLogsPersistenceService, httpClient, + new CloudLoggingServiceMessageConverter()); + exporter.removeClientFromCache(OPERATION_ID); + } + + @Test + void testSendLogs_withNullLoggingConfiguration_doesNothing() { + exporter.sendLogsToCloudLoggingService(null, buildEntry(INFO_LOG)); + + assertTrue(httpClient.capturedEntries() + .isEmpty()); + } + + @Test + void testSendLogs_withOperationLogEntry_sendsExpectedNumberOfEntries() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, buildEntry(INFO_LOG + WARN_LOG)); + + assertEquals(2, httpClient.capturedEntries() + .size()); + } + + @Test + void testSendLogs_withOperationLogEntry_setsLevelOnEntry() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, buildEntry(WARN_LOG)); + + assertEquals("WARN", httpClient.capturedEntries() + .get(0) + .getLevel()); + } + + @Test + void testSendLogs_withOperationLogEntry_setsCorrelationId() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, buildEntry(INFO_LOG)); + + assertEquals(OPERATION_ID, httpClient.capturedEntries() + .get(0) + .getCorrelationId()); + } + + @Test + void testSendLogs_withOperationLogEntry_setsOperationLogName() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + OperationLogEntry entry = ImmutableOperationLogEntry.builder() + .operationId(OPERATION_ID) + .operationLog(INFO_LOG) + .operationLogName("my-log") + .build(); + + exporter.sendLogsToCloudLoggingService(config, entry); + + assertEquals("my-log", httpClient.capturedEntries() + .get(0) + .getOperationLogName()); + } + + @Test + void testSendLogs_withMessageString_extractsLogNameSuffix() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, INFO_LOG); + + assertEquals("hello-backend", httpClient.capturedEntries() + .get(0) + .getOperationLogName()); + } + + @Test + void testSendLogs_withMessageString_setsCorrelationId() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, INFO_LOG); + + assertEquals(OPERATION_ID, httpClient.capturedEntries() + .get(0) + .getCorrelationId()); + } + + @Test + void testSendLogs_withMessageString_setsLevel() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, ERROR_LOG); + + assertEquals("ERROR", httpClient.capturedEntries() + .get(0) + .getLevel()); + } + + @Test + void testSendLogs_withMessageString_producesNoBatchesWhenAllFilteredOut() { + LoggingConfiguration config = buildConfig(LogLevel.ERROR); + + exporter.sendLogsToCloudLoggingService(config, INFO_LOG + DEBUG_LOG); + + assertTrue(httpClient.capturedEntries() + .isEmpty()); + } + + static Stream testLogLevelFiltering() { + String allLevels = INFO_LOG + WARN_LOG + ERROR_LOG + DEBUG_LOG + TRACE_LOG; + return Stream.of( + Arguments.of(LogLevel.ERROR, allLevels, 1), + Arguments.of(LogLevel.WARN, allLevels, 2), + Arguments.of(LogLevel.INFO, allLevels, 3), + Arguments.of(LogLevel.DEBUG, allLevels, 4), + Arguments.of(LogLevel.TRACE, allLevels, 5)); + } + + @ParameterizedTest + @MethodSource + void testLogLevelFiltering(LogLevel configuredLevel, String logMessage, int expectedCount) { + LoggingConfiguration config = buildConfig(configuredLevel); + + exporter.sendLogsToCloudLoggingService(config, buildEntry(logMessage)); + + assertEquals(expectedCount, httpClient.capturedEntries() + .size()); + } + + @Test + void testSendLogs_multipleEntriesAreSentInOneBatch() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, buildEntry(INFO_LOG + WARN_LOG + ERROR_LOG)); + + assertEquals(1, httpClient.capturedBatches.size()); + assertEquals(3, httpClient.capturedEntries() + .size()); + } + + @Test + void testSendLogs_emptyLogProducesNoBatches() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, buildEntry("")); + + assertTrue(httpClient.capturedBatches.isEmpty()); + } + + @Test + void testSendLogs_largeBatchIsSplitWhenOverSizeLimit() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + String largeText = "x".repeat(1024 * 1024); + String log1 = logLine(LOG_DATE, "INFO", "deploy-app.svc", "[t] " + largeText); + String log2 = logLine(LOG_DATE, "INFO", "deploy-app.svc", "[t] " + largeText); + String log3 = logLine(LOG_DATE, "INFO", "deploy-app.svc", "[t] " + largeText); + String log4 = logLine(LOG_DATE, "INFO", "deploy-app.svc", "[t] " + largeText); + + exporter.sendLogsToCloudLoggingService(config, buildEntry(log1 + log2 + log3 + log4)); + + assertTrue(httpClient.capturedBatches.size() > 1); + assertEquals(4, httpClient.capturedEntries() + .size()); + } + + // --- failSafe behavior --- + + @Test + void testSendLogs_failSafeTrue_doesNotThrowOnHttpError() { + LoggingConfiguration config = buildConfig(LogLevel.INFO, true); + httpClient.simulateHttpFailure = true; + + assertDoesNotThrow(() -> exporter.sendLogsToCloudLoggingService(config, INFO_LOG)); + + assertEquals(1, httpClient.capturedEntries() + .size()); + } + + @Test + void testSendLogs_failSafeFalse_throwsOnHttpError() { + LoggingConfiguration config = buildConfig(LogLevel.INFO, false); + httpClient.simulateHttpFailure = true; + + assertThrows(SLException.class, () -> exporter.sendLogsToCloudLoggingService(config, INFO_LOG)); + } + + @Test + void testSendLogs_nullResponseDoesNotThrow() { + LoggingConfiguration config = buildConfig(LogLevel.INFO, true); + httpClient.simulateNullResponse = true; + + assertDoesNotThrow(() -> exporter.sendLogsToCloudLoggingService(config, INFO_LOG)); + + assertEquals(1, httpClient.capturedEntries() + .size()); + } + + @Test + void testGetUnsendProcessLogs_returnsLogsFromService() throws FileStorageException { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + OperationLogEntry entry = buildEntry(INFO_LOG); + org.mockito.Mockito.when( + processLogsPersistenceService.listOperationLogsBySpaceAndOperationId(SPACE_ID, OPERATION_ID)) + .thenReturn(List.of(entry)); + + List result = exporter.getUnsendProcessLogs(config); + + assertEquals(1, result.size()); + assertEquals(entry, result.get(0)); + } + + @Test + void testGetUnsendProcessLogs_failSafeTrue_returnsEmptyListOnStorageException() throws FileStorageException { + LoggingConfiguration config = buildConfig(LogLevel.INFO, true); + org.mockito.Mockito.when( + processLogsPersistenceService.listOperationLogsBySpaceAndOperationId( + org.mockito.ArgumentMatchers.anyString(), + org.mockito.ArgumentMatchers.anyString())) + .thenThrow(new FileStorageException("db error")); + + List result = exporter.getUnsendProcessLogs(config); + + assertTrue(result.isEmpty()); + } + + @Test + void testGetUnsendProcessLogs_failSafeFalse_throwsOnStorageException() throws FileStorageException { + LoggingConfiguration config = buildConfig(LogLevel.INFO, false); + org.mockito.Mockito.when( + processLogsPersistenceService.listOperationLogsBySpaceAndOperationId( + org.mockito.ArgumentMatchers.anyString(), + org.mockito.ArgumentMatchers.anyString())) + .thenThrow(new FileStorageException("db error")); + + assertThrows(SLException.class, () -> exporter.getUnsendProcessLogs(config)); + } + + @Test + void testRemoveClientFromCache_newClientCreatedOnNextSend() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + exporter.sendLogsToCloudLoggingService(config, INFO_LOG); + int clientCreationsAfterFirst = httpClient.clientCreations; + + exporter.removeClientFromCache(OPERATION_ID); + exporter.sendLogsToCloudLoggingService(config, INFO_LOG); + + assertEquals(clientCreationsAfterFirst + 1, httpClient.clientCreations); + } + + @Test + void testSendLogs_cachedClientReusedOnSubsequentCalls() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + exporter.sendLogsToCloudLoggingService(config, INFO_LOG); + int clientCreationsAfterFirst = httpClient.clientCreations; + + exporter.sendLogsToCloudLoggingService(config, INFO_LOG); + + assertEquals(clientCreationsAfterFirst, httpClient.clientCreations); + } + + private static String logLine(String date, String level, String logName, String text) { + return "#" + date + "#org.example.Logger#" + level + "#" + logName + "#main#\n" + text + "\n"; + } + + private static LoggingConfiguration buildConfig(LogLevel logLevel) { + return buildConfig(logLevel, true); + } + + private static LoggingConfiguration buildConfig(LogLevel logLevel, boolean failSafe) { + return ImmutableLoggingConfiguration.builder() + .operationId(OPERATION_ID) + .mtaSpaceId(SPACE_ID) + .logLevel(logLevel) + .isFailSafe(failSafe) + .endpointUrl("https://cls.example.com") + .serverCa("server-ca") + .clientCert("client-cert") + .clientKey("client-key") + .build(); + } + + private static OperationLogEntry buildEntry(String log) { + return ImmutableOperationLogEntry.builder() + .operationId(OPERATION_ID) + .operationLog(log) + .operationLogName("test-log") + .build(); + } + + private static class CapturingHttpClient extends CloudLoggingServiceHttpClient { + + final List> capturedBatches = new ArrayList<>(); + int clientCreations = 0; + boolean simulateHttpFailure = false; + boolean simulateNullResponse = false; + + List capturedEntries() { + return capturedBatches.stream() + .flatMap(List::stream) + .toList(); + } + + @Override + public WebClient createWebClientWithMtls(LoggingConfiguration loggingConfiguration) { + clientCreations++; + return mock(WebClient.class); + } + + @Override + public void sendLogsToCloudLoggingService(LoggingConfiguration loggingConfiguration, WebClient webClient, + List logEntryBatch) { + capturedBatches.add(new ArrayList<>(logEntryBatch)); + if (simulateHttpFailure || simulateNullResponse) { + // The real client treats both an error status and a null response as a failure + // and routes through CloudLoggingServiceUtil — reproduce that here so the + // failSafe semantics under test still apply. + CloudLoggingServiceUtil.logErrorOrThrowExceptionBasedOnFailSafe(loggingConfiguration, + LoggerFactory.getLogger(CapturingHttpClient.class), + Messages.FAILED_TO_SEND_LOG_MESSAGE_TO_CLS); + } + } + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersisterTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersisterTest.java index 72756c5382..8ae97828cf 100644 --- a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersisterTest.java +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersisterTest.java @@ -1,29 +1,25 @@ package org.cloudfoundry.multiapps.controller.persistence.services; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.when; - import org.cloudfoundry.multiapps.controller.persistence.Constants; -import org.cloudfoundry.multiapps.controller.persistence.DataSourceWithDialect; -import org.cloudfoundry.multiapps.controller.persistence.test.TestDataSourceProvider; import org.flowable.engine.delegate.DelegateExecution; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + class ProcessLoggerPersisterTest { - private final static String TEST_CORRELATION_ID = "test-correlation-id"; - private final static String TEST_LOG_NAME = "test-log-name"; - private final static String TEST_TASK_ID = "test-task-id"; - private final static String TEST_SPACE_ID = "test-space-id"; + private static final String TEST_CORRELATION_ID = "test-correlation-id"; + private static final String TEST_LOG_NAME = "test-log-name"; + private static final String TEST_TASK_ID = "test-task-id"; + private static final String TEST_SPACE_ID = "test-space-id"; @Mock private DelegateExecution delegateExecution; @@ -54,12 +50,17 @@ void testPersistLog() { processLoggerPersister.persistLogs(TEST_CORRELATION_ID, TEST_TASK_ID); - Mockito.verify(processLoggerProvider).getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID); - Mockito.verify(processLoggerProvider).removeProcessLoggerFromCache(processLogger); - Mockito.verify(processLoggerProvider).removeProcessLoggerFromCache(processLoggerSecond); - Mockito.verify(processLogsPersistenceService, times(2)).persistLog(any()); - - Assertions.assertEquals(processLoggerProvider.getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID).size(), 0); + Mockito.verify(processLoggerProvider) + .getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID); + Mockito.verify(processLoggerProvider) + .removeProcessLoggerFromCache(processLogger); + Mockito.verify(processLoggerProvider) + .removeProcessLoggerFromCache(processLoggerSecond); + Mockito.verify(processLogsPersistenceService, times(2)) + .persistLog(any()); + + Assertions.assertEquals(0, processLoggerProvider.getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID) + .size()); } @Test @@ -70,20 +71,28 @@ void testPersistLogWithTwoLogsWithTheSameOperationLogName() { processLoggerPersister.persistLogs(TEST_CORRELATION_ID, TEST_TASK_ID); - Mockito.verify(processLoggerProvider).getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID); - Mockito.verify(processLoggerProvider).removeProcessLoggerFromCache(processLogger); - Mockito.verify(processLoggerProvider).removeProcessLoggerFromCache(processLoggerSecond); - Mockito.verify(processLoggerProvider).removeProcessLoggerFromCache(processLoggerThird); - Mockito.verify(processLogsPersistenceService, times(2)).persistLog(any()); - - Assertions.assertEquals(processLoggerProvider.getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID).size(), 0); + Mockito.verify(processLoggerProvider) + .getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID); + Mockito.verify(processLoggerProvider) + .removeProcessLoggerFromCache(processLogger); + Mockito.verify(processLoggerProvider) + .removeProcessLoggerFromCache(processLoggerSecond); + Mockito.verify(processLoggerProvider) + .removeProcessLoggerFromCache(processLoggerThird); + Mockito.verify(processLogsPersistenceService, times(2)) + .persistLog(any()); + + Assertions.assertEquals(0, processLoggerProvider.getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID) + .size()); } @Test void testPersistLogWithoutLogs() { processLoggerPersister.persistLogs(TEST_CORRELATION_ID, TEST_TASK_ID); - Mockito.verify(processLoggerProvider).getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID); - Mockito.verify(processLogsPersistenceService, times(0)).persistLog(any()); + Mockito.verify(processLoggerProvider) + .getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID); + Mockito.verify(processLogsPersistenceService, times(0)) + .persistLog(any()); } } diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/util/CloudLoggingServiceUtilTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/util/CloudLoggingServiceUtilTest.java new file mode 100644 index 0000000000..89ad47c2d8 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/util/CloudLoggingServiceUtilTest.java @@ -0,0 +1,53 @@ +package org.cloudfoundry.multiapps.controller.persistence.util; + +import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +class CloudLoggingServiceUtilTest { + + private static final String MESSAGE = "boom"; + + @Test + void logErrorOrThrow_failSafeTrue_logsAndDoesNotThrow() { + Logger logger = mock(Logger.class); + + CloudLoggingServiceUtil.logErrorOrThrowExceptionBasedOnFailSafe(buildConfig(true), logger, MESSAGE); + + verify(logger).error(MESSAGE); + } + + @Test + void logErrorOrThrow_failSafeFalse_throwsAndDoesNotLog() { + Logger logger = mock(Logger.class); + + SLException thrown = assertThrows(SLException.class, + () -> CloudLoggingServiceUtil.logErrorOrThrowExceptionBasedOnFailSafe(buildConfig(false), logger, + MESSAGE)); + + assertEquals(MESSAGE, thrown.getMessage()); + verify(logger, never()).error(MESSAGE); + } + + private static LoggingConfiguration buildConfig(boolean failSafe) { + return ImmutableLoggingConfiguration.builder() + .operationId("op") + .mtaSpaceId("space") + .logLevel(LogLevel.INFO) + .isFailSafe(failSafe) + .endpointUrl("https://cls.example.com") + .serverCa("ca") + .clientCert("cert") + .clientKey("key") + .build(); + } +} diff --git a/multiapps-controller-persistence/src/test/resources/META-INF/persistence.xml b/multiapps-controller-persistence/src/test/resources/META-INF/persistence.xml index f0d782dc02..c3f94eb778 100644 --- a/multiapps-controller-persistence/src/test/resources/META-INF/persistence.xml +++ b/multiapps-controller-persistence/src/test/resources/META-INF/persistence.xml @@ -14,6 +14,7 @@ org.cloudfoundry.multiapps.controller.persistence.dto.BackupDescriptorDto org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdownDto org.cloudfoundry.multiapps.controller.persistence.dto.SecretTokenDto + org.cloudfoundry.multiapps.controller.persistence.dto.LoggingConfigurationDto true diff --git a/multiapps-controller-process/src/main/java/module-info.java b/multiapps-controller-process/src/main/java/module-info.java index 35a6a74387..91a729c19f 100644 --- a/multiapps-controller-process/src/main/java/module-info.java +++ b/multiapps-controller-process/src/main/java/module-info.java @@ -63,6 +63,10 @@ requires static java.compiler; requires static org.immutables.value; + requires spring.webflux; + requires reactor.netty.core; + requires reactor.netty.http; requires org.cloudfoundry.multiapps.controller.shutdown.client; + requires com.google.errorprone.annotations; } \ No newline at end of file diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java index 1a3fb90642..5c4d8fb108 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java @@ -114,6 +114,7 @@ public class Messages { public static final String ERROR_DETECTING_DEPLOYED_MTA = "Error detecting deployed MTA"; public static final String ERROR_RENAMING_APPLICATIONS = "Error renaming applications"; public static final String ERROR_COLLECTING_SYSTEM_PARAMETERS = "Error collecting system parameters"; + public static final String ERROR_COLLECTING_CLOUD_LOGGING_SERVICE_PARAMETERS = "Error collecting cloud logging service parameters"; public static final String ERROR_RESOLVING_DESCRIPTOR_PROPERTIES = "Error resolving merged descriptor properties and parameters"; public static final String ERROR_CREATING_SUBSCRIPTIONS = "Error creating subscriptions"; public static final String ERROR_BUILDING_CLOUD_MODEL = "Error building cloud model"; @@ -818,7 +819,9 @@ public class Messages { public static final String IGNORING_NOT_FOUND_OPTIONAL_SERVICE = "Service {0} not found but is optional"; public static final String IGNORING_NOT_FOUND_INACTIVE_SERVICE = "Service {0} not found but is inactive"; - + public static final String NO_CLOUD_LOGGING_SERVICE_KEY_FOUND_FOR_OPERATION_0_SKIPPING_LOG_EXPORT = "No cloud logging service key found for operation {0}, skipping log export"; + public static final String INVALID_LOG_LEVEL = "Invalid log level"; + public static final String MISSING_REQUIRED_1_CREDENTIAL_FROM_SCL_EXPORT = "Missing required {1} credential for SAP Cloud Logging export"; // Not log messages public static final String SERVICE_TYPE = "{0}/{1}"; public static final String PARSE_NULL_STRING_ERROR = "Cannot parse null string"; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/flowable/FlowableFacade.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/flowable/FlowableFacade.java index f32f4fbdce..0d7f08b0f4 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/flowable/FlowableFacade.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/flowable/FlowableFacade.java @@ -1,7 +1,5 @@ package org.cloudfoundry.multiapps.controller.process.flowable; -import static java.text.MessageFormat.format; - import java.text.MessageFormat; import java.time.LocalDateTime; import java.time.ZoneId; @@ -12,9 +10,9 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.persistence.Constants; import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.controller.process.variables.VariableHandling; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.flowable.common.engine.api.FlowableObjectNotFoundException; import org.flowable.common.engine.api.FlowableOptimisticLockingException; @@ -32,6 +30,8 @@ import org.slf4j.LoggerFactory; import org.springframework.context.annotation.DependsOn; +import static java.text.MessageFormat.format; + @Named @DependsOn("processEngine") public class FlowableFacade { @@ -299,4 +299,9 @@ public void setVariableInParentProcess(DelegateExecution execution, String varia .setVariable(parentProcessId, variableName, value); } + public void setVariableInParentProcessUsingParentProcessInstanceId(DelegateExecution execution, String variableName, Object value) { + String parentProcessInstanceId = VariableHandling.get(execution, Variables.PARENT_PROCESS_INSTANCE_ID); + processEngine.getRuntimeService() + .setVariable(parentProcessInstanceId, variableName, value); + } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/AbstractProcessExecutionListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/AbstractProcessExecutionListener.java index 846b6f8b5f..f560be9982 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/AbstractProcessExecutionListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/AbstractProcessExecutionListener.java @@ -1,10 +1,10 @@ package org.cloudfoundry.multiapps.controller.process.listeners; import jakarta.inject.Inject; - import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLogger; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; @@ -32,6 +32,7 @@ public abstract class AbstractProcessExecutionListener implements ExecutionListe private final FlowableFacade flowableFacade; protected final ApplicationConfiguration configuration; private final ProcessLoggerPersister processLoggerPersister; + private final OperationLogsExporter operationLogsExporter; private StepLogger stepLogger; @@ -39,7 +40,7 @@ public abstract class AbstractProcessExecutionListener implements ExecutionListe protected AbstractProcessExecutionListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration) { + ApplicationConfiguration configuration, OperationLogsExporter operationLogsExporter) { this.progressMessageService = progressMessageService; this.stepLoggerFactory = stepLoggerFactory; this.processLoggerProvider = processLoggerProvider; @@ -47,6 +48,7 @@ protected AbstractProcessExecutionListener(ProgressMessageService progressMessag this.historicOperationEventService = historicOperationEventService; this.flowableFacade = flowableFacade; this.configuration = configuration; + this.operationLogsExporter = operationLogsExporter; } @Override @@ -56,8 +58,8 @@ public void notify(DelegateExecution execution) { this.stepLogger = createStepLogger(execution); notifyInternal(execution); } catch (Exception e) { - logException(e, Messages.EXECUTION_OF_PROCESS_LISTENER_HAS_FAILED); - throw new SLException(e, Messages.EXECUTION_OF_PROCESS_LISTENER_HAS_FAILED); + logException(e, Messages.EXECUTION_OF_PROCESS_LISTENER_HAS_FAILED + e.getMessage()); + throw new SLException(e, Messages.EXECUTION_OF_PROCESS_LISTENER_HAS_FAILED + e.getMessage()); } finally { finalizeLogs(execution); } @@ -114,7 +116,7 @@ protected HistoricOperationEventService getHistoricOperationEventService() { } private StepLogger createStepLogger(DelegateExecution execution) { - return stepLoggerFactory.create(execution, progressMessageService, processLoggerProvider, getLogger()); + return stepLoggerFactory.create(execution, progressMessageService, processLoggerProvider, getLogger(), operationLogsExporter); } protected boolean isRootProcess(DelegateExecution execution) { @@ -127,6 +129,15 @@ protected void setVariableInParentProcess(DelegateExecution execution, String va flowableFacade.setVariableInParentProcess(execution, variableName, value); } + protected void setVariableInParentProcessUsingParentProcessInstanceId(DelegateExecution execution, String variableName, Object value) { + flowableFacade.setVariableInParentProcessUsingParentProcessInstanceId(execution, variableName, value); + } + + protected boolean hasSuperExecution(DelegateExecution execution) { + return execution.getParentId() != null && flowableFacade.getParentExecution(execution.getParentId()) + .getSuperExecutionId() != null; + } + protected abstract void notifyInternal(DelegateExecution execution) throws Exception; protected Logger getLogger() { diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/CreateUpdateServicesListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/CreateUpdateServicesListener.java index 05a98f8790..44d1d9db39 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/CreateUpdateServicesListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/CreateUpdateServicesListener.java @@ -8,12 +8,12 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.core.helpers.DynamicResolvableParametersHelper; import org.cloudfoundry.multiapps.controller.core.model.DynamicResolvableParameter; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -34,14 +34,15 @@ public class CreateUpdateServicesListener extends AbstractProcessExecutionListen protected CreateUpdateServicesListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration) { + ApplicationConfiguration configuration, OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); } @Override @@ -59,8 +60,9 @@ private void setCloudServiceInstancesInParentProcess(DelegateExecution execution } private void setDynamicResolvableParametersInParentProcess(DelegateExecution execution, List services) { - Set dynamicResolvableParametersFromSubProcesses = getDynamicResolvableParametersFromSubProcesses(execution, - services); + Set dynamicResolvableParametersFromSubProcesses = getDynamicResolvableParametersFromSubProcesses( + execution, + services); Set resolvedParameters = new HashSet<>(VariableHandling.get(execution, Variables.DYNAMIC_RESOLVABLE_PARAMETERS)); @@ -71,7 +73,8 @@ private void setDynamicResolvableParametersInParentProcess(DelegateExecution exe Variables.DYNAMIC_RESOLVABLE_PARAMETERS.getSerializer() .serialize(resolvedParameters)); execution.setVariable(Variables.DYNAMIC_RESOLVABLE_PARAMETERS.getName(), Variables.DYNAMIC_RESOLVABLE_PARAMETERS.getSerializer() - .serialize(resolvedParameters)); + .serialize( + resolvedParameters)); } private Set getDynamicResolvableParametersFromSubProcesses(DelegateExecution execution, @@ -82,7 +85,8 @@ private Set getDynamicResolvableParametersFromSubPro .map(serviceGuidConstant -> StepsUtil.getObject(execution, serviceGuidConstant)) .filter(Objects::nonNull) .map(dynamicResolvableParameterObject -> Variables.DYNAMIC_RESOLVABLE_PARAMETER.getSerializer() - .deserialize(dynamicResolvableParameterObject)) + .deserialize( + dynamicResolvableParameterObject)) .collect(Collectors.toSet()); } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DeployAppSubProcessEndListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DeployAppSubProcessEndListener.java index a5d2259a1f..a8e0234ab8 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DeployAppSubProcessEndListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DeployAppSubProcessEndListener.java @@ -7,6 +7,7 @@ import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -27,9 +28,15 @@ public class DeployAppSubProcessEndListener extends AbstractProcessExecutionList protected DeployAppSubProcessEndListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration) { - super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, - flowableFacade, configuration); + ApplicationConfiguration configuration, OperationLogsExporter operationLogsExporter) { + super(progressMessageService, + stepLoggerFactory, + processLoggerProvider, + processLoggerPersister, + historicOperationEventService, + flowableFacade, + configuration, + operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DetermineServiceCreateUpdateActionsListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DetermineServiceCreateUpdateActionsListener.java index a00ecaa40f..dbab6adde6 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DetermineServiceCreateUpdateActionsListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DetermineServiceCreateUpdateActionsListener.java @@ -4,10 +4,10 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -29,14 +29,16 @@ protected DetermineServiceCreateUpdateActionsListener(ProgressMessageService pro StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, - FlowableFacade flowableFacade, ApplicationConfiguration configuration) { + FlowableFacade flowableFacade, ApplicationConfiguration configuration, + OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); } public static String buildExportedVariableName(String serviceName) { diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DoNotDeleteServicesListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DoNotDeleteServicesListener.java index a2445ef612..3db1bab558 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DoNotDeleteServicesListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DoNotDeleteServicesListener.java @@ -1,9 +1,9 @@ package org.cloudfoundry.multiapps.controller.process.listeners; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -20,14 +20,15 @@ public class DoNotDeleteServicesListener extends AbstractProcessExecutionListene protected DoNotDeleteServicesListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration) { + ApplicationConfiguration configuration, OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListener.java index 4a00b41734..4f6c4d35c1 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListener.java @@ -2,11 +2,11 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.api.model.Operation; import org.cloudfoundry.multiapps.controller.api.model.ProcessType; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -35,14 +35,16 @@ public EndProcessListener(ProgressMessageService progressMessageService, StepLog ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, ApplicationConfiguration configuration, OperationInFinalStateHandler eventHandler, - DynatracePublisher dynatracePublisher, ProcessTypeParser processTypeParser) { + DynatracePublisher dynatracePublisher, ProcessTypeParser processTypeParser, + OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); this.eventHandler = eventHandler; this.dynatracePublisher = dynatracePublisher; this.processTypeParser = processTypeParser; @@ -61,7 +63,8 @@ private void publishDynatraceEvent(DelegateExecution execution, ProcessType proc .processId(VariableHandling.get(execution, Variables.CORRELATION_ID)) .mtaId(VariableHandling.get(execution, Variables.MTA_ID)) - .createdBy(VariableHandling.get(execution, Variables.MTA_ARCHIVE_CREATED_BY)) + .createdBy(VariableHandling.get(execution, + Variables.MTA_ARCHIVE_CREATED_BY)) .spaceId(VariableHandling.get(execution, Variables.SPACE_GUID)) .eventType(DynatraceProcessEvent.EventType.FINISHED) .processType(processType) diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessStatisticsListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessStatisticsListener.java index 17a7fc22b9..cc82d243b7 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessStatisticsListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessStatisticsListener.java @@ -2,9 +2,9 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -21,14 +21,15 @@ public class EndProcessStatisticsListener extends AbstractProcessExecutionListen protected EndProcessStatisticsListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration) { + ApplicationConfiguration configuration, OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListener.java index 061bf009c4..aad0a8237f 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListener.java @@ -2,11 +2,11 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.api.model.ImmutableOperation; import org.cloudfoundry.multiapps.controller.api.model.Operation; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.OperationService; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; @@ -29,14 +29,16 @@ public class EnterTestingPhaseListener extends AbstractProcessExecutionListener protected EnterTestingPhaseListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration, OperationService operationService) { + ApplicationConfiguration configuration, OperationService operationService, + OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); this.operationService = operationService; } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ExportCloudLoggingConfigurationListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ExportCloudLoggingConfigurationListener.java new file mode 100644 index 0000000000..540f4fb29e --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ExportCloudLoggingConfigurationListener.java @@ -0,0 +1,62 @@ +package org.cloudfoundry.multiapps.controller.process.listeners; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; +import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; +import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; +import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; +import org.cloudfoundry.multiapps.controller.process.flowable.FlowableFacade; +import org.cloudfoundry.multiapps.controller.process.util.StepLogger; +import org.cloudfoundry.multiapps.controller.process.variables.Serializer; +import org.cloudfoundry.multiapps.controller.process.variables.Variable; +import org.cloudfoundry.multiapps.controller.process.variables.VariableHandling; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.flowable.engine.delegate.DelegateExecution; + +@Named("exportCloudLoggingConfigurationListener") +public class ExportCloudLoggingConfigurationListener extends AbstractProcessExecutionListener { + + @Inject + protected ExportCloudLoggingConfigurationListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, + ProcessLoggerProvider processLoggerProvider, + ProcessLoggerPersister processLoggerPersister, + HistoricOperationEventService historicOperationEventService, + FlowableFacade flowableFacade, ApplicationConfiguration configuration, + OperationLogsExporter operationLogsExporter) { + super(progressMessageService, + stepLoggerFactory, + processLoggerProvider, + processLoggerPersister, + historicOperationEventService, + flowableFacade, + configuration, + operationLogsExporter); + } + + @Override + protected void notifyInternal(DelegateExecution execution) throws Exception { + LoggingConfiguration loggingConfiguration = VariableHandling.get(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION); + if (loggingConfiguration == null) { + return; + } + Variable loggingConfigurationVariable = Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION; + Serializer loggingConfigurationSerializer = loggingConfigurationVariable.getSerializer(); + String parentProcessInstanceId = VariableHandling.get(execution, Variables.PARENT_PROCESS_INSTANCE_ID); + + if (parentProcessInstanceId != null && !parentProcessInstanceId.isEmpty()) { + setVariableInParentProcessUsingParentProcessInstanceId(execution, loggingConfigurationVariable.getName(), + loggingConfigurationSerializer.serialize(loggingConfiguration)); + return; + } + if (hasSuperExecution(execution)) { + setVariableInParentProcess(execution, loggingConfigurationVariable.getName(), + loggingConfigurationSerializer.serialize(loggingConfiguration)); + return; + } + VariableHandling.set(execution, loggingConfigurationVariable, loggingConfiguration); + } +} diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/HooksEndProcessListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/HooksEndProcessListener.java index 6ec5b3f721..6bc5f3fda0 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/HooksEndProcessListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/HooksEndProcessListener.java @@ -1,7 +1,9 @@ package org.cloudfoundry.multiapps.controller.process.listeners; +import jakarta.inject.Named; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -10,22 +12,21 @@ import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.flowable.engine.delegate.DelegateExecution; -import jakarta.inject.Named; - @Named("hooksEndProcessListener") public class HooksEndProcessListener extends AbstractProcessExecutionListener { protected HooksEndProcessListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration) { + ApplicationConfiguration configuration, OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/LeaveTestingPhaseListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/LeaveTestingPhaseListener.java index 7b808bef0e..357572c064 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/LeaveTestingPhaseListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/LeaveTestingPhaseListener.java @@ -2,9 +2,9 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -22,14 +22,15 @@ public class LeaveTestingPhaseListener extends AbstractProcessExecutionListener protected LeaveTestingPhaseListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration) { + ApplicationConfiguration configuration, OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListener.java index 6b0fdaa5ab..00c65d082a 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListener.java @@ -2,11 +2,11 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.api.model.ProcessType; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -29,14 +29,16 @@ public class ManageAppServiceBindingEndListener extends AbstractProcessExecution protected ManageAppServiceBindingEndListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration, ProcessTypeParser processTypeParser) { + ApplicationConfiguration configuration, ProcessTypeParser processTypeParser, + OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); this.processTypeParser = processTypeParser; } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListener.java index fbca97c731..362c7cfb41 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListener.java @@ -9,7 +9,6 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.common.util.JsonUtil; import org.cloudfoundry.multiapps.controller.api.model.ImmutableOperation; @@ -24,6 +23,7 @@ import org.cloudfoundry.multiapps.controller.persistence.services.FileService; import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.OperationService; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; @@ -63,14 +63,16 @@ public StartProcessListener(ProgressMessageService progressMessageService, StepL HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, ApplicationConfiguration configuration, ProcessTypeParser processTypeParser, OperationService operationService, ProcessTypeToOperationMetadataMapper operationMetadataMapper, - DynatracePublisher dynatracePublisher, FileService fileService) { + DynatracePublisher dynatracePublisher, FileService fileService, + OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); this.processTypeParser = processTypeParser; this.operationService = operationService; this.operationMetadataMapper = operationMetadataMapper; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CollectCloudLoggingServiceParametersStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CollectCloudLoggingServiceParametersStep.java new file mode 100644 index 0000000000..ca4e35a16a --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CollectCloudLoggingServiceParametersStep.java @@ -0,0 +1,177 @@ +package org.cloudfoundry.multiapps.controller.process.steps; + +import java.util.List; + +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.api.model.ProcessType; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; +import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; +import org.cloudfoundry.multiapps.controller.core.cf.v2.ResourceType; +import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.OperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; +import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.controller.process.util.LoggingConfigurationBuilder; +import org.cloudfoundry.multiapps.controller.process.util.ProcessTypeParser; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Resource; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Scope; + +import static org.apache.commons.lang3.StringUtils.EMPTY; + +@Named("collectCloudLoggingServiceParametersStep") +@Scope(BeanDefinition.SCOPE_PROTOTYPE) +public class CollectCloudLoggingServiceParametersStep extends SyncFlowableStep { + + private static final String CLOUDFOUNDRY_RESOURCE_TYPE_PREFIX = "org.cloudfoundry."; + + private final TokenService tokenService; + private final CloudControllerClientFactory clientFactory; + private final CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + private final ProcessTypeParser processTypeParser; + private final CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; + + public CollectCloudLoggingServiceParametersStep(TokenService tokenService, CloudControllerClientFactory clientFactory, + CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService, + ProcessTypeParser processTypeParser, + CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog) { + this.tokenService = tokenService; + this.clientFactory = clientFactory; + this.cloudLoggingServiceConfigurationService = cloudLoggingServiceConfigurationService; + this.processTypeParser = processTypeParser; + this.cloudLoggingServiceConfigurationAuditLog = cloudLoggingServiceConfigurationAuditLog; + } + + @Override + protected StepPhase executeStep(ProcessContext context) throws Exception { + LoggingConfiguration loggingConfiguration = getLoggingConfiguration(context); + if (loggingConfiguration == null) { + return StepPhase.DONE; + } + List operationLogEntries = operationLogsExporter.getUnsendProcessLogs(loggingConfiguration); + + for (OperationLogEntry operationLogEntry : operationLogEntries) { + operationLogsExporter.sendLogsToCloudLoggingService(loggingConfiguration, operationLogEntry); + } + context.setVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, loggingConfiguration); + return StepPhase.DONE; + } + + @Override + protected String getStepErrorMessage(ProcessContext context) { + return Messages.ERROR_COLLECTING_CLOUD_LOGGING_SERVICE_PARAMETERS; + } + + private LoggingConfiguration getLoggingConfiguration(ProcessContext context) { + ProcessType processType = processTypeParser.getProcessType(context.getExecution()); + LoggingConfiguration existingLoggingConfiguration = getExistingLoggingConfiguration(context); + + if (processType.equals(ProcessType.UNDEPLOY)) { + return processUndeployLoggingConfiguration(context, existingLoggingConfiguration); + } + return processDeployLoggingConfiguration(context, existingLoggingConfiguration); + } + + private LoggingConfiguration getExistingLoggingConfiguration(ProcessContext context) { + LoggingConfiguration loggingConfiguration = cloudLoggingServiceConfigurationService.getCloudLoggingServiceConfiguration( + context.getVariable(Variables.SPACE_NAME), context.getVariable(Variables.MTA_ID), context.getVariable(Variables.MTA_NAMESPACE)); + + if (loggingConfiguration != null) { + cloudLoggingServiceConfigurationAuditLog.logGetLoggingConfiguration(context.getVariable(Variables.USER), + context.getVariable(Variables.SPACE_GUID), + loggingConfiguration); + } + return loggingConfiguration; + } + + private LoggingConfiguration processUndeployLoggingConfiguration(ProcessContext context, + LoggingConfiguration existingLoggingConfiguration) { + if (existingLoggingConfiguration == null) { + return null; + } + return setExternalLoggingServiceConfigurationIfRequired(context, existingLoggingConfiguration); + } + + private LoggingConfiguration processDeployLoggingConfiguration(ProcessContext context, + LoggingConfiguration existingLoggingConfiguration) { + DeploymentDescriptor deploymentDescriptor = context.getVariable(Variables.DEPLOYMENT_DESCRIPTOR); + if (!isCloudLoggingEnabled(deploymentDescriptor)) { + deleteExistingLoggingConfigurationIfExists(context, existingLoggingConfiguration); + return null; + } + + LoggingConfiguration newLoggingConfiguration = setExternalLoggingServiceConfigurationIfRequired(context, deploymentDescriptor); + if (newLoggingConfiguration == null) { + return null; + } + if (existingLoggingConfiguration == null) { + persistLoggingConfiguration(context, newLoggingConfiguration); + } else { + updateLoggingConfiguration(context, newLoggingConfiguration); + } + return newLoggingConfiguration; + } + + private void deleteExistingLoggingConfigurationIfExists(ProcessContext context, LoggingConfiguration existingLoggingConfiguration) { + if (existingLoggingConfiguration != null) { + cloudLoggingServiceConfigurationAuditLog.logDeleteLoggingConfiguration(context.getVariable(Variables.USER), + context.getVariable(Variables.SPACE_GUID), + existingLoggingConfiguration); + cloudLoggingServiceConfigurationService.deleteCloudLoggingServiceConfiguration(existingLoggingConfiguration.getId()); + } + } + + private boolean isCloudLoggingEnabled(DeploymentDescriptor deploymentDescriptor) { + return !deploymentDescriptor.getResources() + .isEmpty() + && deploymentDescriptor.getResources() + .stream() + .anyMatch(CollectCloudLoggingServiceParametersStep::isCloudLoggingServiceResource); + } + + protected LoggingConfiguration setExternalLoggingServiceConfigurationIfRequired(ProcessContext context, + DeploymentDescriptor deploymentDescriptor) { + LoggingConfigurationBuilder builder = new LoggingConfigurationBuilder(clientFactory, context, tokenService); + Resource resource = findCloudLoggingServiceResource(deploymentDescriptor.getResources()); + return builder.exportOperationLogsToExternalSystem(resource); + } + + protected LoggingConfiguration setExternalLoggingServiceConfigurationIfRequired(ProcessContext context, + LoggingConfiguration loggingConfiguration) { + LoggingConfigurationBuilder builder = new LoggingConfigurationBuilder(clientFactory, context, tokenService); + return builder.exportOperationLogsToExternalSystem(loggingConfiguration, context); + } + + private void persistLoggingConfiguration(ProcessContext context, LoggingConfiguration newLoggingConfiguration) { + cloudLoggingServiceConfigurationAuditLog.logCreateLoggingConfiguration(context.getVariable(Variables.USER), + context.getVariable(Variables.SPACE_GUID), + newLoggingConfiguration); + cloudLoggingServiceConfigurationService.storeCloudLoggingServiceConfiguration(newLoggingConfiguration); + } + + private void updateLoggingConfiguration(ProcessContext context, LoggingConfiguration newLoggingConfiguration) { + cloudLoggingServiceConfigurationAuditLog.logUpdateLoggingConfiguration(context.getVariable(Variables.USER), + context.getVariable(Variables.SPACE_GUID), + newLoggingConfiguration); + cloudLoggingServiceConfigurationService.updateCloudLoggingServiceConfiguration(newLoggingConfiguration); + } + + private Resource findCloudLoggingServiceResource(List resources) { + return resources.stream() + .filter(CollectCloudLoggingServiceParametersStep::isCloudLoggingServiceResource) + .findFirst() + .get(); + } + + private static boolean isCloudLoggingServiceResource(Resource resource) { + ResourceType resourceType = ResourceType.get(stripCloudfoundryPrefix(resource.getType())); + return ResourceType.CLOUD_LOGGING_SERVICE.equals(resourceType); + } + + private static String stripCloudfoundryPrefix(String resourceType) { + return resourceType.replace(CLOUDFOUNDRY_RESOURCE_TYPE_PREFIX, EMPTY); + } +} diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java index 57f2e88526..e7c468feb8 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java @@ -51,7 +51,7 @@ protected String getStepErrorMessage(ProcessContext context) { @Override protected List getAsyncStepExecutions(ProcessContext context) { - return List.of(new PollExecuteTaskStatusExecution(clientFactory, tokenService)); + return List.of(new PollExecuteTaskStatusExecution(clientFactory, tokenService, operationLogsExporter)); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/IncrementalAppInstancesUpdateStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/IncrementalAppInstancesUpdateStep.java index 7f46f4e753..d3e2bc7357 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/IncrementalAppInstancesUpdateStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/IncrementalAppInstancesUpdateStep.java @@ -174,8 +174,8 @@ private void setExecutionIndexToTriggerNewApplicationRollingUpdate(ProcessContex protected List getAsyncStepExecutions(ProcessContext context) { // The sequence of executions is crucial, as the incremental blue-green deployment alternates between them during the polling // process - return List.of(new PollStartLiveAppExecution(clientFactory, tokenService), - new PollStartAppExecutionWithRollbackExecution(clientFactory, tokenService), + return List.of(new PollStartLiveAppExecution(clientFactory, tokenService, operationLogsExporter), + new PollStartAppExecutionWithRollbackExecution(clientFactory, tokenService, operationLogsExporter), new PollIncrementalAppInstanceUpdateExecution()); } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteAppStatusExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteAppStatusExecution.java index 3ad349d46e..33c8641556 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteAppStatusExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteAppStatusExecution.java @@ -17,6 +17,7 @@ import org.cloudfoundry.multiapps.controller.core.helpers.ApplicationAttributes; import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.variables.Variables; @@ -58,13 +59,16 @@ String getMessage() { private final CloudControllerClientFactory clientFactory; private final TokenService tokenService; + private final OperationLogsExporter operationLogsExporter; private static final String DEFAULT_SUCCESS_MARKER = "STDOUT:SUCCESS"; private static final String DEFAULT_FAILURE_MARKER = "STDERR:FAILURE"; - public PollExecuteAppStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService) { + public PollExecuteAppStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService, + OperationLogsExporter operationLogsExporter) { this.clientFactory = clientFactory; this.tokenService = tokenService; + this.operationLogsExporter = operationLogsExporter; } @Override @@ -90,7 +94,7 @@ public AsyncExecutionState execute(ProcessContext context) { AppExecutionDetailedStatus status = getAppExecutionStatus(context, appAttributes, recentLogs); ProcessLoggerProvider processLoggerProvider = context.getStepLogger() .getProcessLoggerProvider(); - StepsUtil.saveAppLogs(context, logCacheClient, appGuid, app.getName(), LOGGER, processLoggerProvider); + StepsUtil.saveAppLogs(context, logCacheClient, appGuid, app.getName(), LOGGER, processLoggerProvider, operationLogsExporter); return checkAppExecutionStatus(context, app, appAttributes, status); } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteTaskStatusExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteTaskStatusExecution.java index cc214da344..62f82ee27d 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteTaskStatusExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteTaskStatusExecution.java @@ -9,6 +9,7 @@ import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.process.Constants; import org.cloudfoundry.multiapps.controller.process.Messages; @@ -25,10 +26,13 @@ public class PollExecuteTaskStatusExecution implements AsyncExecution { private final CloudControllerClientFactory clientFactory; private final TokenService tokenService; + private final OperationLogsExporter operationLogsExporter; - public PollExecuteTaskStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService) { + public PollExecuteTaskStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService, + OperationLogsExporter operationLogsExporter) { this.clientFactory = clientFactory; this.tokenService = tokenService; + this.operationLogsExporter = operationLogsExporter; } @Override @@ -50,7 +54,7 @@ public AsyncExecutionState execute(ProcessContext context) { var logCacheClient = clientFactory.createLogCacheClient(tokenService.getToken(userGuid), correlationId); UUID appGuid = client.getApplicationGuid(app.getName()); - StepsUtil.saveAppLogs(context, logCacheClient, appGuid, app.getName(), LOGGER, processLoggerProvider); + StepsUtil.saveAppLogs(context, logCacheClient, appGuid, app.getName(), LOGGER, processLoggerProvider, operationLogsExporter); if (currentState == CloudTask.State.SUCCEEDED) { return AsyncExecutionState.FINISHED; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecution.java index e057ed7b89..c550bc8601 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecution.java @@ -8,6 +8,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.PackageState; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.ApplicationStager; @@ -24,12 +25,14 @@ public class PollStageAppStatusExecution implements AsyncExecution { private final ApplicationStager applicationStager; private final CloudControllerClientFactory clientFactory; private final TokenService tokenService; + private final OperationLogsExporter operationLogsExporter; public PollStageAppStatusExecution(ApplicationStager applicationStager, CloudControllerClientFactory clientFactory, - TokenService tokenService) { + TokenService tokenService, OperationLogsExporter operationLogsExporter) { this.applicationStager = applicationStager; this.clientFactory = clientFactory; this.tokenService = tokenService; + this.operationLogsExporter = operationLogsExporter; } @Override @@ -49,7 +52,8 @@ public AsyncExecutionState execute(ProcessContext context) { var logCacheClient = clientFactory.createLogCacheClient(tokenService.getToken(userGuid), correlationId); UUID appGuid = client.getApplicationGuid(application.getName()); - StepsUtil.saveAppLogs(context, logCacheClient, appGuid, application.getName(), LOGGER, processLoggerProvider); + StepsUtil.saveAppLogs(context, logCacheClient, appGuid, application.getName(), LOGGER, processLoggerProvider, + operationLogsExporter); if (state.getState() != PackageState.STAGED) { return checkStagingState(context.getStepLogger(), application.getName(), state); diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecution.java index a1fbc35243..69410eeadd 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecution.java @@ -6,6 +6,7 @@ import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.model.IncrementalAppInstanceUpdateConfiguration; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.variables.Variables; @@ -13,8 +14,9 @@ public class PollStartAppExecutionWithRollbackExecution extends PollStartAppStatusExecution { - public PollStartAppExecutionWithRollbackExecution(CloudControllerClientFactory clientFactory, TokenService tokenService) { - super(clientFactory, tokenService); + public PollStartAppExecutionWithRollbackExecution(CloudControllerClientFactory clientFactory, TokenService tokenService, + OperationLogsExporter operationLogsExporter) { + super(clientFactory, tokenService, operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecution.java index a0012d229e..b6ed2cc870 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecution.java @@ -12,6 +12,7 @@ import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; import org.cloudfoundry.multiapps.controller.core.util.UriUtil; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.ReadinessHealthCheckUtil; @@ -26,10 +27,13 @@ public class PollStartAppStatusExecution implements AsyncExecution { private static final Logger LOGGER = LoggerFactory.getLogger(PollStartAppStatusExecution.class); private final CloudControllerClientFactory clientFactory; private final TokenService tokenService; + private final OperationLogsExporter operationLogsExporter; - public PollStartAppStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService) { + public PollStartAppStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService, + OperationLogsExporter operationLogsExporter) { this.clientFactory = clientFactory; this.tokenService = tokenService; + this.operationLogsExporter = operationLogsExporter; } @Override @@ -51,7 +55,7 @@ public AsyncExecutionState execute(ProcessContext context) { var correlationId = context.getVariable(Variables.CORRELATION_ID); var logCacheClient = clientFactory.createLogCacheClient(tokenService.getToken(userGuid), correlationId); - StepsUtil.saveAppLogs(context, logCacheClient, app.getGuid(), app.getName(), LOGGER, processLoggerProvider); + StepsUtil.saveAppLogs(context, logCacheClient, app.getGuid(), app.getName(), LOGGER, processLoggerProvider, operationLogsExporter); return checkStartupStatus(context, app, status); } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecution.java index 169ee05bc8..327e7d36c6 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecution.java @@ -7,13 +7,15 @@ import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.model.DeployedMtaApplication; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.variables.Variables; public class PollStartLiveAppExecution extends PollStartAppStatusExecution { - public PollStartLiveAppExecution(CloudControllerClientFactory clientFactory, TokenService tokenService) { - super(clientFactory, tokenService); + public PollStartLiveAppExecution(CloudControllerClientFactory clientFactory, TokenService tokenService, + OperationLogsExporter operationLogsExporter) { + super(clientFactory, tokenService, operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartServiceBrokerSubscriberStatusExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartServiceBrokerSubscriberStatusExecution.java index 4840c8b664..9435942c92 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartServiceBrokerSubscriberStatusExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartServiceBrokerSubscriberStatusExecution.java @@ -3,11 +3,13 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; public class PollStartServiceBrokerSubscriberStatusExecution extends PollStartAppStatusExecution { - public PollStartServiceBrokerSubscriberStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService) { - super(clientFactory, tokenService); + public PollStartServiceBrokerSubscriberStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService, + OperationLogsExporter operationLogsExporter) { + super(clientFactory, tokenService, operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelper.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelper.java index 78666e3308..6b3208cd50 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelper.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelper.java @@ -10,6 +10,7 @@ import org.cloudfoundry.multiapps.controller.persistence.model.HistoricOperationEvent; import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableProgressMessage; import org.cloudfoundry.multiapps.controller.persistence.model.ProgressMessage.ProgressMessageType; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLogger; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -36,9 +37,9 @@ public static boolean isProcessAborted(List historicOper } protected void postExecuteStep(ProcessContext context, StepPhase state) { - logDebug(MessageFormat.format(Messages.STEP_FINISHED, context.getExecution() - .getCurrentFlowElement() - .getName())); + logDebug(context, MessageFormat.format(Messages.STEP_FINISHED, context.getExecution() + .getCurrentFlowElement() + .getName())); getProcessLoggerPersister().persistLogs(context.getVariable(Variables.CORRELATION_ID), context.getVariable(Variables.TASK_ID)); context.setVariable(Variables.STEP_EXECUTION, state.toString()); @@ -72,8 +73,13 @@ protected void logExceptionAndStoreProgressMessage(ProcessContext context, Throw private void logException(ProcessContext context, Throwable t) { LOGGER.error(Messages.EXCEPTION_CAUGHT, t); - getProcessLogger().error(Messages.EXCEPTION_CAUGHT, t); + ProcessLogger processLogger = getProcessLogger(); + processLogger.error(Messages.EXCEPTION_CAUGHT, t); + if (context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION) != null) { + getOperationLogsExporter().error(context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION), + processLogger.getLogMessage()); + } if (t instanceof ContentException) { context.setVariable(Variables.ERROR_TYPE, ErrorType.CONTENT_ERROR); } else { @@ -90,7 +96,13 @@ private void storeExceptionInProgressMessageService(ProcessContext context, Thro .text(throwable.getMessage()) .build()); } catch (SLException e) { - getProcessLogger().error(Messages.SAVING_ERROR_MESSAGE_FAILED, e); + ProcessLogger processLogger = getProcessLogger(); + processLogger.error(Messages.SAVING_ERROR_MESSAGE_FAILED, e); + + if (context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION) != null) { + getOperationLogsExporter().error(context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION), + processLogger.getLogMessage()); + } } } @@ -112,8 +124,14 @@ private String getCurrentActivityId(DelegateExecution execution) { .getActivityId(); } - private void logDebug(String message) { - getProcessLogger().debug(message); + private void logDebug(ProcessContext context, String message) { + ProcessLogger processLogger = getProcessLogger(); + processLogger.debug(message); + + if (context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION) != null) { + getOperationLogsExporter().debug(context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION), + processLogger.getLogMessage()); + } } private ProcessLogger getProcessLogger() { @@ -134,6 +152,8 @@ public void failStepIfProcessIsAborted(ProcessContext context) { public abstract ProcessLoggerPersister getProcessLoggerPersister(); + public abstract OperationLogsExporter getOperationLogsExporter(); + public abstract ProcessEngineConfiguration getProcessEngineConfiguration(); public abstract ProcessHelper getProcessHelper(); diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartAppStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartAppStep.java index 8a897002a6..221ea0e1a8 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartAppStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartAppStep.java @@ -92,8 +92,8 @@ public List getHookPhasesBeforeStep(ProcessContext context) { @Override protected List getAsyncStepExecutions(ProcessContext context) { - return List.of(new PollStartAppStatusExecution(clientFactory, tokenService), - new PollExecuteAppStatusExecution(clientFactory, tokenService)); + return List.of(new PollStartAppStatusExecution(clientFactory, tokenService, operationLogsExporter), + new PollExecuteAppStatusExecution(clientFactory, tokenService, operationLogsExporter)); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartServiceBrokerSubscriberStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartServiceBrokerSubscriberStep.java index aaaa25f930..fec9181b20 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartServiceBrokerSubscriberStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartServiceBrokerSubscriberStep.java @@ -25,7 +25,7 @@ protected CloudApplication getAppToRestart(ProcessContext context) { @Override protected List getAsyncStepExecutions(ProcessContext context) { - return List.of(new PollStartServiceBrokerSubscriberStatusExecution(clientFactory, tokenService)); + return List.of(new PollStartServiceBrokerSubscriberStatusExecution(clientFactory, tokenService, operationLogsExporter)); } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StageAppStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StageAppStep.java index 8e11f5cafa..f203efefee 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StageAppStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StageAppStep.java @@ -40,7 +40,7 @@ protected String getStepErrorMessage(ProcessContext context) { @Override protected List getAsyncStepExecutions(ProcessContext context) { - return List.of(new PollStageAppStatusExecution(new ApplicationStager(context), clientFactory, tokenService)); + return List.of(new PollStageAppStatusExecution(new ApplicationStager(context), clientFactory, tokenService, operationLogsExporter)); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StepsUtil.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StepsUtil.java index 3dba3e9306..dffe6be5af 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StepsUtil.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StepsUtil.java @@ -32,6 +32,8 @@ import org.cloudfoundry.multiapps.controller.core.model.DeployedMta; import org.cloudfoundry.multiapps.controller.core.model.Phase; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; +import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLogger; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.process.Constants; import org.cloudfoundry.multiapps.controller.process.Messages; @@ -162,7 +164,7 @@ static CloudTask getTask(ProcessContext context) { } static void saveAppLogs(ProcessContext context, LogCacheClient client, UUID appGuid, String appName, Logger logger, - ProcessLoggerProvider processLoggerProvider) { + ProcessLoggerProvider processLoggerProvider, OperationLogsExporter operationLogsExporter) { LocalDateTime offset = context.getVariable(Variables.LOGS_OFFSET); var recentLogs = getRecentLogsSafely(client, appGuid, offset, logger); if (recentLogs.isEmpty()) { @@ -173,8 +175,12 @@ static void saveAppLogs(ProcessContext context, LogCacheClient client, UUID appG } var loggerPrefix = getLoggerPrefix(logger); for (ApplicationLog log : recentLogs) { - processLoggerProvider.getLogger(context.getExecution(), appName) - .debug(loggerPrefix + log.toString()); + ProcessLogger processLogger = processLoggerProvider.getLogger(context.getExecution(), appName); + processLogger.debug(loggerPrefix + log.toString()); + if (context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION) != null) { + operationLogsExporter.sendLogsToCloudLoggingService(context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION), + processLogger.getLogMessage()); + } } var lastLog = recentLogs.get(recentLogs.size() - 1); diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStep.java index 7241c4805c..13536f1df4 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStep.java @@ -1,5 +1,8 @@ package org.cloudfoundry.multiapps.controller.process.steps; +import java.util.function.BiFunction; +import java.util.function.Consumer; + import io.netty.handler.timeout.TimeoutException; import jakarta.inject.Inject; import jakarta.inject.Named; @@ -14,6 +17,7 @@ import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.core.util.LoggingUtil; import org.cloudfoundry.multiapps.controller.persistence.services.FileService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -33,9 +37,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.function.BiFunction; -import java.util.function.Consumer; - public abstract class SyncFlowableStep implements JavaDelegate { protected final Logger logger = LoggerFactory.getLogger(getClass()); @@ -66,6 +67,8 @@ public abstract class SyncFlowableStep implements JavaDelegate { private StepLogger stepLogger; @Inject private ProcessHelper processHelper; + @Inject + protected OperationLogsExporter operationLogsExporter; @Override public void execute(DelegateExecution execution) { @@ -209,7 +212,7 @@ protected StepLogger getStepLogger() { } protected void initializeStepLogger(DelegateExecution execution) { - stepLogger = stepLoggerFactory.create(execution, progressMessageService, processLoggerProvider, logger); + stepLogger = stepLoggerFactory.create(execution, progressMessageService, processLoggerProvider, logger, operationLogsExporter); } protected Exception getWithProperMessage(Exception e) { @@ -225,6 +228,7 @@ protected ProcessStepHelper getStepHelper() { .progressMessageService(getProgressMessageService()) .stepLogger(getStepLogger()) .processLoggerPersister(processLoggerPersister) + .operationLogsExporter(operationLogsExporter) .processEngineConfiguration(processEngineConfiguration) .processHelper(processHelper) .build(); diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecution.java index f68e1bd55f..4da2cc59c2 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecution.java @@ -4,6 +4,7 @@ import java.text.MessageFormat; import java.time.Duration; import java.time.LocalDateTime; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -23,6 +24,8 @@ import org.cloudfoundry.multiapps.controller.core.helpers.MtaArchiveElements; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.core.util.FileUtils; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.context.ApplicationToUploadContext; @@ -44,13 +47,16 @@ public class UploadAppAsyncExecution implements AsyncExecution { private final ProcessLoggerPersister processLoggerPersister; private final ApplicationConfiguration applicationConfiguration; private final ExecutorService appUploaderThreadPool; + private final OperationLogsExporter operationLogsExporter; public UploadAppAsyncExecution(ApplicationZipBuilder applicationZipBuilder, ProcessLoggerPersister processLoggerPersister, - ApplicationConfiguration applicationConfiguration, ExecutorService appUploaderThreadPool) { + ApplicationConfiguration applicationConfiguration, ExecutorService appUploaderThreadPool, + OperationLogsExporter operationLogsExporter) { this.applicationZipBuilder = applicationZipBuilder; this.processLoggerPersister = processLoggerPersister; this.applicationConfiguration = applicationConfiguration; this.appUploaderThreadPool = appUploaderThreadPool; + this.operationLogsExporter = operationLogsExporter; } @Override @@ -111,15 +117,16 @@ private CloudPackage doUpload(ProcessContext context, CloudApplicationExtended a context.getStepLogger() .debug(Messages.UPLOAD_OF_APPLICATION_0_STARTED_ON_INSTANCE_1, applicationToProcess.getName(), applicationConfiguration.getApplicationInstanceIndex()); - return proceedWithUpload(context.getControllerClient(), applicationToUploadContext); + return proceedWithUpload(context.getControllerClient(), applicationToUploadContext, context); } - private CloudPackage proceedWithUpload(CloudControllerClient client, ApplicationToUploadContext applicationToUploadContext) { + private CloudPackage proceedWithUpload(CloudControllerClient client, ApplicationToUploadContext applicationToUploadContext, + ProcessContext context) { applicationToUploadContext.getStepLogger() .debug(Messages.UPLOADING_FILE_0_FOR_APP_1, applicationToUploadContext.getModuleFileName(), applicationToUploadContext.getApplication() .getName()); - CloudPackage cloudPackage = asyncUploadFiles(client, applicationToUploadContext); + CloudPackage cloudPackage = asyncUploadFiles(client, applicationToUploadContext, context); applicationToUploadContext.getStepLogger() .info(Messages.STARTED_ASYNC_UPLOAD_OF_APP_0, applicationToUploadContext.getApplication() .getName()); @@ -127,7 +134,8 @@ private CloudPackage proceedWithUpload(CloudControllerClient client, Application return cloudPackage; } - private CloudPackage asyncUploadFiles(CloudControllerClient client, ApplicationToUploadContext applicationToUploadContext) { + private CloudPackage asyncUploadFiles(CloudControllerClient client, ApplicationToUploadContext applicationToUploadContext, + ProcessContext context) { Path extractedAppPath = extractApplicationFromArchive(applicationToUploadContext); LOGGER.debug(MessageFormat.format(Messages.APPLICATION_WITH_NAME_0_SAVED_TO_1, applicationToUploadContext.getApplication() .getName(), @@ -137,7 +145,7 @@ private CloudPackage asyncUploadFiles(CloudControllerClient client, ApplicationT .getName(), extractedAppPath.toFile() .length()); - return upload(client, applicationToUploadContext, extractedAppPath); + return upload(client, applicationToUploadContext, extractedAppPath, context); } private Path extractApplicationFromArchive(ApplicationToUploadContext applicationToUploadContext) { @@ -164,15 +172,16 @@ protected Path extractFromMtar(ApplicationArchiveContext applicationArchiveConte } private CloudPackage upload(CloudControllerClient client, ApplicationToUploadContext applicationToUploadContext, - Path extractedModulePath) { + Path extractedModulePath, ProcessContext context) { try { return client.asyncUploadApplicationWithExponentialBackoff(applicationToUploadContext.getApplication() .getName(), extractedModulePath, getMonitorUploadStatusCallback( - applicationToUploadContext.getApplication(), extractedModulePath, + applicationToUploadContext.getApplication(), + extractedModulePath, applicationToUploadContext.getStepLogger(), applicationToUploadContext.getCorrelationId(), - applicationToUploadContext.getTaskId()), null); + applicationToUploadContext.getTaskId(), context), null); } catch (Exception e) { FileUtils.cleanUp(extractedModulePath, LOGGER); throw new SLException(e, Messages.ERROR_WHILE_STARTING_ASYNC_UPLOAD_OF_APP_WITH_NAME_0, @@ -208,8 +217,8 @@ public String getPollingErrorMessage(ProcessContext context) { } MonitorUploadStatusCallback getMonitorUploadStatusCallback(CloudApplication app, Path file, StepLogger stepLogger, String correlationId, - String taskId) { - return new MonitorUploadStatusCallback(app, file, stepLogger, correlationId, taskId); + String taskId, ProcessContext context) { + return new MonitorUploadStatusCallback(app, file, stepLogger, correlationId, taskId, context); } class MonitorUploadStatusCallback implements UploadStatusCallbackExtended { @@ -219,13 +228,16 @@ class MonitorUploadStatusCallback implements UploadStatusCallbackExtended { private final StepLogger stepLogger; private final String correlationId; private final String taskId; + private final ProcessContext context; - public MonitorUploadStatusCallback(CloudApplication app, Path file, StepLogger stepLogger, String correlationId, String taskId) { + public MonitorUploadStatusCallback(CloudApplication app, Path file, StepLogger stepLogger, String correlationId, String taskId, + ProcessContext context) { this.app = app; this.file = file; this.stepLogger = stepLogger; this.correlationId = correlationId; this.taskId = taskId; + this.context = context; } @Override @@ -248,6 +260,7 @@ public boolean onProgress(String status) { stepLogger.debug(Messages.UPLOAD_STATUS_0, status); if (status.equals(Status.READY.toString())) { FileUtils.cleanUp(file, LOGGER); + sendApplicationLogsToCloudLoggingService(correlationId, taskId); processLoggerPersister.persistLogs(correlationId, taskId); } return false; @@ -264,6 +277,14 @@ public void onError(String description) { stepLogger.error(Messages.ERROR_UPLOADING_APP_0_STATUS_1_DESCRIPTION_2, app.getName(), Status.FAILED, description); FileUtils.cleanUp(file, LOGGER); } + + private void sendApplicationLogsToCloudLoggingService(String correlationId, String taskId) { + LoggingConfiguration loggingConfiguration = context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION); + List processLogs = processLoggerPersister.getApplicationProcessLogsMessages(correlationId, taskId); + for (String processLog : processLogs) { + operationLogsExporter.sendLogsToCloudLoggingService(loggingConfiguration, processLog); + } + } } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppStep.java index 0f19e1fa0e..3b0cbaa25a 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppStep.java @@ -185,7 +185,8 @@ private void removeApplicationDigestIfSet(ProcessContext context, Map getAsyncStepExecutions(ProcessContext context) { - return List.of(new UploadAppAsyncExecution(applicationZipBuilder, getProcessLogsPersister(), configuration, appUploaderThreadPool), + return List.of(new UploadAppAsyncExecution(applicationZipBuilder, getProcessLogsPersister(), configuration, appUploaderThreadPool, + operationLogsExporter), new PollUploadAppStatusExecution()); } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/LoggingConfigurationBuilder.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/LoggingConfigurationBuilder.java new file mode 100644 index 0000000000..d77eaf9175 --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/LoggingConfigurationBuilder.java @@ -0,0 +1,258 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.text.MessageFormat; +import java.util.Map; +import java.util.UUID; + +import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.common.util.MiscUtil; +import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; +import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; +import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.core.util.NameUtil; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.controller.process.steps.ProcessContext; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.Resource; + +public class LoggingConfigurationBuilder { + + private static final String CREDENTIAL_KEY_INGEST_MTLS_ENDPOINT = "ingest-mtls-endpoint"; + private static final String CREDENTIAL_KEY_SERVER_CA = "server-ca"; + private static final String CREDENTIAL_KEY_INGEST_MTLS_CERT = "ingest-mtls-cert"; + private static final String CREDENTIAL_KEY_INGEST_MTLS_KEY = "ingest-mtls-key"; + + private final CloudControllerClientFactory clientFactory; + private final ProcessContext context; + private final TokenService tokenService; + + public LoggingConfigurationBuilder(CloudControllerClientFactory clientFactory, ProcessContext context, + TokenService tokenService) { + this.clientFactory = clientFactory; + this.context = context; + this.tokenService = tokenService; + } + + public LoggingConfiguration exportOperationLogsToExternalSystem(Resource resource) { + + LoggingConfiguration loggingConfiguration = getCredentialsFromServiceKey(resource); + if (loggingConfiguration == null) { + return null; + } + + String correlationId = context.getVariable(Variables.CORRELATION_ID); + String spaceName = getTargetSpace(resource, context.getVariable(Variables.SPACE_NAME)); + String orgName = getTargetOrg(resource, context.getVariable(Variables.ORGANIZATION_NAME)); + String targetSpaceGuid = resolveTargetSpaceGuid(orgName, spaceName); + LogLevel logLevel = getLogLevelsFromConfiguration(resource); + + return ImmutableLoggingConfiguration.copyOf(loggingConfiguration) + .withId(UUID.randomUUID() + .toString()) + .withOperationId(correlationId) + .withTargetSpace(spaceName) + .withTargetSpaceGuid(targetSpaceGuid) + .withTargetOrg(orgName) + .withMtaId(context.getVariable(Variables.MTA_ID)) + .withMtaSpaceId(context.getVariable(Variables.SPACE_GUID)) + .withMtaSpace(context.getVariable(Variables.SPACE_NAME)) + .withMtaOrg(context.getVariable(Variables.ORGANIZATION_NAME)) + .withNamespace(context.getVariable(Variables.MTA_NAMESPACE)) + .withLogLevel(logLevel) + .withIsFailSafe(resource.isOptional()); + } + + public LoggingConfiguration exportOperationLogsToExternalSystem(LoggingConfiguration incomingLoggingConfiguration, + ProcessContext context) { + return getCredentialsFromServiceKey(incomingLoggingConfiguration, context); + } + + private LogLevel getLogLevelsFromConfiguration(Resource resource) { + if (!resource.getParameters() + .containsKey(SupportedParameters.LOG_LEVEL)) { + return LogLevel.INFO; + } + String logLevelFromDescriptor = MiscUtil.cast(resource.getParameters() + .get(SupportedParameters.LOG_LEVEL)); + if (LogLevel.isValid(logLevelFromDescriptor)) { + return LogLevel.get(logLevelFromDescriptor); + } + if (resource.isOptional()) { + return null; + } else { + throw new SLException(Messages.INVALID_LOG_LEVEL); + } + } + + private boolean areCloudLoggingParametersInvalid(String serviceInstanceName, String serviceKeyName) { + return serviceInstanceName == null || serviceInstanceName.isBlank() || serviceKeyName == null || serviceKeyName.isBlank(); + } + + private String getServiceKeyName(Resource resource) { + return MiscUtil.cast(resource.getParameters() + .get(SupportedParameters.SERVICE_KEY_NAME)); + } + + private LoggingConfiguration getCredentialsFromServiceKey(LoggingConfiguration loggingConfiguration, ProcessContext context) { + CloudServiceKey loggingServiceKey = getServiceKeyWithLoggingConfiguration(loggingConfiguration); + if (loggingServiceKey == null) { + return null; + } + Map credentials = loggingServiceKey.getCredentials(); + + String endpoint = getCredentialFromServiceKey(CREDENTIAL_KEY_INGEST_MTLS_ENDPOINT, credentials); + String serverCa = getCredentialFromServiceKey(CREDENTIAL_KEY_SERVER_CA, credentials); + String ingestMtlsCert = getCredentialFromServiceKey(CREDENTIAL_KEY_INGEST_MTLS_CERT, credentials); + String ingestMtlsKey = getCredentialFromServiceKey(CREDENTIAL_KEY_INGEST_MTLS_KEY, credentials); + + return ImmutableLoggingConfiguration.copyOf(loggingConfiguration) + .withOperationId(context.getVariable(Variables.CORRELATION_ID)) + .withMtaSpaceId(context.getVariable(Variables.SPACE_GUID)) + .withServiceInstanceGuid(toGuidString(loggingServiceKey.getServiceInstance() + .getGuid())) + .withServerCa(serverCa) + .withEndpointUrl(endpoint) + .withClientCert(ingestMtlsCert) + .withClientKey(ingestMtlsKey); + } + + private CloudServiceKey getServiceKeyWithResource(Resource resource) { + return getCloudLoggingServiceKey(NameUtil.getServiceInstanceNameOrDefault(resource), getServiceKeyName(resource), + getTargetOrg(resource, context.getVariable(Variables.ORGANIZATION_NAME)), + getTargetSpace(resource, context.getVariable(Variables.SPACE_NAME)), + resource.isOptional()); + } + + private CloudServiceKey getServiceKeyWithLoggingConfiguration(LoggingConfiguration loggingConfiguration) { + return getCloudLoggingServiceKey(loggingConfiguration.getServiceInstanceName(), loggingConfiguration.getServiceKeyName(), + loggingConfiguration.getTargetOrg(), loggingConfiguration.getTargetSpace(), + loggingConfiguration.isFailSafe()); + } + + private CloudServiceKey getCloudLoggingServiceKey(String serviceInstanceName, String serviceKeyName, String destinationOrg, + String destinationSpace, boolean isFailSafe) { + if (areCloudLoggingParametersInvalid(serviceInstanceName, serviceKeyName)) { + return handleMissingServiceKey(isFailSafe, null); + } + CloudControllerClient client = calculateExternalLoggingServiceConfiguration(destinationOrg, destinationSpace); + try { + CloudServiceKey loggingServiceKey = client.getServiceKey(serviceInstanceName, serviceKeyName); + if (loggingServiceKey != null) { + return loggingServiceKey; + } + return handleMissingServiceKey(isFailSafe, null); + } catch (CloudOperationException e) { + return handleMissingServiceKey(isFailSafe, e); + } + } + + private CloudServiceKey handleMissingServiceKey(boolean isFailSafe, CloudOperationException cause) { + if (isFailSafe) { + return null; + } + if (cause != null) { + throw new SLException(cause); + } + throw new SLException(MessageFormat.format(Messages.NO_CLOUD_LOGGING_SERVICE_KEY_FOUND_FOR_OPERATION_0_SKIPPING_LOG_EXPORT, + context.getVariable(Variables.CORRELATION_ID))); + } + + private LoggingConfiguration getCredentialsFromServiceKey(Resource resource) { + CloudServiceKey loggingServiceKey = getServiceKeyWithResource(resource); + if (loggingServiceKey == null) { + return null; + } + Map credentials = loggingServiceKey.getCredentials(); + + String endpoint = getCredentialFromServiceKey(CREDENTIAL_KEY_INGEST_MTLS_ENDPOINT, credentials); + String serverCa = getCredentialFromServiceKey(CREDENTIAL_KEY_SERVER_CA, credentials); + String ingestMtlsCert = getCredentialFromServiceKey(CREDENTIAL_KEY_INGEST_MTLS_CERT, credentials); + String ingestMtlsKey = getCredentialFromServiceKey(CREDENTIAL_KEY_INGEST_MTLS_KEY, credentials); + + return ImmutableLoggingConfiguration.builder() + .serverCa(serverCa) + .endpointUrl(endpoint) + .clientKey(ingestMtlsKey) + .clientCert(ingestMtlsCert) + .serviceInstanceName(NameUtil.getServiceInstanceNameOrDefault(resource)) + .serviceInstanceGuid(toGuidString(loggingServiceKey.getServiceInstance() + .getGuid())) + .serviceKeyName(loggingServiceKey.getName()) + .build(); + } + + private String getCredentialFromServiceKey(String credentialsName, Map credentials) { + String credential = (String) credentials.get(credentialsName); + + if (credential == null) { + throw new SLException(MessageFormat.format(Messages.MISSING_REQUIRED_1_CREDENTIAL_FROM_SCL_EXPORT, credentialsName)); + } + + return credential; + } + + private CloudControllerClient calculateExternalLoggingServiceConfiguration(String destinationOrg, String destinationSpace) { + String currentTargetOrg = context.getVariable(Variables.ORGANIZATION_NAME); + String currentTargetSpace = context.getVariable(Variables.SPACE_NAME); + + String targetOrg = getTargetOrg(destinationOrg, currentTargetOrg); + String targetSpace = getTargetSpace(destinationSpace, currentTargetSpace); + + if (targetOrg.equals(currentTargetOrg) && targetSpace.equals(currentTargetSpace)) { + return context.getControllerClient(); + } + return clientFactory.createClient(tokenService.getToken(context.getVariable(Variables.USER_GUID)), targetOrg, targetSpace, + context.getVariable(Variables.CORRELATION_ID)); + } + + private String resolveTargetSpaceGuid(String destinationOrg, String destinationSpace) { + CloudControllerClient client = calculateExternalLoggingServiceConfiguration(destinationOrg, destinationSpace); + return toGuidString(client.getTarget() + .getGuid()); + } + + private static String toGuidString(UUID guid) { + return guid == null ? null : guid.toString(); + } + + private String getTargetOrg(String existingLoggingConfigurationOrg, String org) { + return existingLoggingConfigurationOrg == null ? org : existingLoggingConfigurationOrg; + } + + private String getTargetSpace(String existingLoggingConfigurationSpace, String space) { + return existingLoggingConfigurationSpace == null ? space : existingLoggingConfigurationSpace; + } + + private String getTargetOrg(Resource resource, String org) { + Map destination = getDestination(resource); + if (destination == null) { + return org; + } + return getDestination(resource).get(SupportedParameters.CLS_ORG_NAME) == null + ? org + : getDestination(resource).get(SupportedParameters.CLS_ORG_NAME) + .toString(); + } + + private String getTargetSpace(Resource resource, String space) { + Map destination = getDestination(resource); + if (destination == null) { + return space; + } + return destination.get(SupportedParameters.CLS_SPACE_NAME) == null + ? space + : destination.get(SupportedParameters.CLS_SPACE_NAME) + .toString(); + } + + private Map getDestination(Resource resource) { + return MiscUtil.cast(resource.getParameters() + .get(SupportedParameters.DESTINATION)); + } +} diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandler.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandler.java index 87f8f900bd..b9c43150e1 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandler.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandler.java @@ -1,5 +1,12 @@ package org.cloudfoundry.multiapps.controller.process.util; +import java.text.MessageFormat; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + import jakarta.inject.Inject; import jakarta.inject.Named; import org.cloudfoundry.client.v3.Metadata; @@ -8,6 +15,7 @@ import org.cloudfoundry.multiapps.controller.api.model.Operation.State; import org.cloudfoundry.multiapps.controller.api.model.ProcessType; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientProvider; import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadataAnnotations; import org.cloudfoundry.multiapps.controller.core.model.DeployedMta; @@ -16,10 +24,13 @@ import org.cloudfoundry.multiapps.controller.core.util.SafeExecutor; import org.cloudfoundry.multiapps.controller.persistence.model.HistoricOperationEvent; import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableHistoricOperationEvent; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; import org.cloudfoundry.multiapps.controller.persistence.services.DescriptorBackupService; import org.cloudfoundry.multiapps.controller.persistence.services.FileService; import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.OperationService; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.dynatrace.DynatraceProcessDuration; @@ -34,13 +45,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.text.MessageFormat; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; - import static java.text.MessageFormat.format; @Named @@ -64,6 +68,14 @@ public class OperationInFinalStateHandler { private DynatracePublisher dynatracePublisher; @Inject private SecretTokenStoreFactory secretTokenStoreFactory; + @Inject + private OperationLogsExporter operationLogsExporter; + @Inject + private CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + @Inject + private ProcessTypeParser processTypeParser; + @Inject + private CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; private final SafeExecutor safeExecutor = new SafeExecutor(); @@ -81,6 +93,8 @@ private void handleInternal(DelegateExecution execution, ProcessType processType safeExecutor.execute(() -> deletePreviousBackupDescriptors(execution, processType, state)); safeExecutor.execute(() -> deleteSecretTokensForProcess(correlationId)); safeExecutor.execute(() -> trackOperationDuration(correlationId, execution, processType, state)); + safeExecutor.execute(() -> deleteCloudLoggingServiceConfiguration(execution)); + operationLogsExporter.removeClientFromCache(correlationId); } protected void deleteDeploymentFiles(String correlationId, DelegateExecution execution) throws FileStorageException { @@ -191,6 +205,21 @@ private void deleteSecretTokensForProcess(String correlationId) { secretTokenStore.deleteByProcessInstanceId(correlationId); } + private void deleteCloudLoggingServiceConfiguration(DelegateExecution execution) { + LoggingConfiguration loggingConfiguration = VariableHandling.get(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION); + if (loggingConfiguration == null || loggingConfiguration.getId() == null) { + return; + } + ProcessType processType = processTypeParser.getProcessType(execution); + if (processType.equals(ProcessType.UNDEPLOY)) { + + cloudLoggingServiceConfigurationService.deleteCloudLoggingServiceConfiguration(loggingConfiguration.getId()); + cloudLoggingServiceConfigurationAuditLog.logDeleteLoggingConfiguration(VariableHandling.get(execution, Variables.USER), + VariableHandling.get(execution, Variables.SPACE_GUID), + loggingConfiguration); + } + } + private void deleteDisposableUserProvidedServiceForProcess(DelegateExecution execution, String correlationId) { boolean isDisposableUserProvidedServiceEnabled = VariableHandling.get(execution, Variables.IS_DISPOSABLE_USER_PROVIDED_SERVICE_ENABLED); diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/StepLogger.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/StepLogger.java index b28b7c83ed..194510e519 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/StepLogger.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/StepLogger.java @@ -3,11 +3,13 @@ import java.text.MessageFormat; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.controller.core.util.UserMessageLogger; import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableProgressMessage; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; import org.cloudfoundry.multiapps.controller.persistence.model.ProgressMessage.ProgressMessageType; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLogger; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -19,7 +21,6 @@ /** * The purpose of this class is to group logging of progress messages and process logs in a single place. - * */ public class StepLogger implements UserMessageLogger { @@ -27,13 +28,15 @@ public class StepLogger implements UserMessageLogger { protected final ProgressMessageService progressMessageService; protected final ProcessLoggerProvider processLoggerProvider; protected final Logger simpleStepLogger; + protected final OperationLogsExporter operationLogsExporter; public StepLogger(DelegateExecution execution, ProgressMessageService progressMessageService, - ProcessLoggerProvider processLoggerProvider, Logger simpleStepLogger) { + ProcessLoggerProvider processLoggerProvider, Logger simpleStepLogger, OperationLogsExporter operationLogsExporter) { this.execution = execution; this.progressMessageService = progressMessageService; this.processLoggerProvider = processLoggerProvider; this.simpleStepLogger = simpleStepLogger; + this.operationLogsExporter = operationLogsExporter; } public void logFlowableTask() { @@ -46,7 +49,10 @@ public void infoWithoutProgressMessage(String pattern, Object... arguments) { public void infoWithoutProgressMessage(String message) { simpleStepLogger.info(message); - getProcessLogger().info(getPrefix(simpleStepLogger) + message); + ProcessLogger processLogger = getProcessLogger(); + processLogger.info(getPrefix(simpleStepLogger) + message); + String formattedMessage = processLogger.getLogMessage(); + sendLogsToCLoudLoggingService(formattedMessage, LogLevel.INFO); } public void info(String pattern, Object... arguments) { @@ -72,7 +78,12 @@ public void errorWithoutProgressMessage(String pattern, Object... arguments) { public void errorWithoutProgressMessage(String message) { simpleStepLogger.error(message); - getProcessLogger().error(getPrefix(simpleStepLogger) + message); + ProcessLogger processLogger = getProcessLogger(); + processLogger.error(getPrefix(simpleStepLogger) + message); + String formattedMessage = processLogger.getLogMessage(); + + sendLogsToCLoudLoggingService(formattedMessage, LogLevel.ERROR); + } public void error(Exception e, String pattern, Object... arguments) { @@ -98,16 +109,27 @@ public void warnWithoutProgressMessage(Exception e, String pattern, Object... ar public void warnWithoutProgressMessage(Exception e, String message) { simpleStepLogger.warn(message, e); - getProcessLogger().warn(getPrefix(simpleStepLogger) + message, e); + ProcessLogger processLogger = getProcessLogger(); + processLogger.warn(getPrefix(simpleStepLogger) + message, e); + String formattedMessage = processLogger.getLogMessage(); + + sendLogsToCLoudLoggingService(formattedMessage, LogLevel.WARN); + } public void warnWithoutProgressMessage(String pattern, Object... arguments) { warnWithoutProgressMessage(MessageFormat.format(pattern, arguments)); } + @Override public void warnWithoutProgressMessage(String message) { simpleStepLogger.warn(message); - getProcessLogger().warn(getPrefix(simpleStepLogger) + message); + ProcessLogger processLogger = getProcessLogger(); + processLogger.warn(getPrefix(simpleStepLogger) + message); + String formattedMessage = processLogger.getLogMessage(); + + sendLogsToCLoudLoggingService(formattedMessage, LogLevel.WARN); + } public void warn(Exception e, String pattern, Object... arguments) { @@ -139,7 +161,11 @@ public void debug(String pattern, Object... arguments) { public void debug(String message) { simpleStepLogger.debug(message); - getProcessLogger().debug(getPrefix(simpleStepLogger) + message); + + ProcessLogger processLogger = getProcessLogger(); + processLogger.debug(getPrefix(simpleStepLogger) + message); + String formattedMessage = processLogger.getLogMessage(); + sendLogsToCLoudLoggingService(formattedMessage, LogLevel.DEBUG); } public void trace(String pattern, Object... arguments) { @@ -148,7 +174,17 @@ public void trace(String pattern, Object... arguments) { public void trace(String message) { simpleStepLogger.trace(message); - getProcessLogger().trace(getPrefix(simpleStepLogger) + message); + ProcessLogger processLogger = getProcessLogger(); + processLogger.trace(getPrefix(simpleStepLogger) + message); + String formattedMessage = processLogger.getLogMessage(); + sendLogsToCLoudLoggingService(formattedMessage, LogLevel.TRACE); + } + + private void sendLogsToCLoudLoggingService(String message, LogLevel level) { + LoggingConfiguration loggingConfiguration = VariableHandling.get(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION); + if (loggingConfiguration != null) { + operationLogsExporter.sendLogsToCloudLoggingService(loggingConfiguration, message, level); + } } private static String getExtendedMessage(String message, Exception e) { @@ -191,8 +227,8 @@ private static String getPrefix(Logger logger) { public static class Factory { public StepLogger create(DelegateExecution execution, ProgressMessageService progressMessageService, - ProcessLoggerProvider processLoggerProvider, Logger logger) { - return new StepLogger(execution, progressMessageService, processLoggerProvider, logger); + ProcessLoggerProvider processLoggerProvider, Logger logger, OperationLogsExporter operationLogsExporter) { + return new StepLogger(execution, progressMessageService, processLoggerProvider, logger, operationLogsExporter); } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java index b041ef2a74..966fa96792 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java @@ -35,6 +35,7 @@ import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationSubscription; import org.cloudfoundry.multiapps.controller.persistence.model.FileEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; import org.cloudfoundry.multiapps.controller.process.DeployStrategy; import org.cloudfoundry.multiapps.controller.process.steps.StepPhase; import org.cloudfoundry.multiapps.controller.process.util.ArchiveEntryWithStreamPositions; @@ -581,9 +582,9 @@ public interface Variables { .defaultValue(Collections.emptyList()) .build(); Variable> HOOK_EXECUTION_PHASES = ImmutableSimpleVariable.> builder() - .name("hookExecutionPhases") - .defaultValue(Collections.emptyList()) - .build(); + .name("hookExecutionPhases") + .defaultValue(Collections.emptyList()) + .build(); Variable> MODULES_TO_DEPLOY = ImmutableJsonBinaryListVariable. builder() .name("modulesToDeploy") .type(Variable.typeReference(Module.class)) @@ -969,4 +970,17 @@ public Serializer> getSerializer() { .defaultValue(false) .build(); + Variable EXTERNAL_LOGGING_SERVICE_CONFIGURATION = ImmutableJsonStringVariable. builder() + .name( + "externalLoggingServiceConfigurations") + .type( + Variable.typeReference( + LoggingConfiguration.class)) + .defaultValue(null) + .build(); + + Variable PARENT_PROCESS_INSTANCE_ID = ImmutableSimpleVariable. builder() + .name("parentProcessInstanceId") + .defaultValue(null) + .build(); } diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn index 350f22b579..1f6dbeedad 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn @@ -30,6 +30,7 @@ + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/delete-services.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/delete-services.bpmn index 30f3e19405..cbdc1bdd14 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/delete-services.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/delete-services.bpmn @@ -37,6 +37,7 @@ + @@ -69,6 +70,7 @@ + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn index db163d450d..74aa96ee69 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn @@ -93,6 +93,7 @@ + @@ -121,6 +122,7 @@ + @@ -155,6 +157,7 @@ + @@ -186,6 +189,7 @@ + @@ -221,6 +225,7 @@ + @@ -351,6 +356,7 @@ + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/process-batches-sequentially.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/process-batches-sequentially.bpmn index 88c5f94466..898fb6a75d 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/process-batches-sequentially.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/process-batches-sequentially.bpmn @@ -40,6 +40,7 @@ + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/recreate-service-keys.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/recreate-service-keys.bpmn index 1fc0ad3a6c..6811f0846d 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/recreate-service-keys.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/recreate-service-keys.bpmn @@ -21,6 +21,7 @@ + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/stop-dependent-modules.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/stop-dependent-modules.bpmn index 46a2558e93..7d782b7b12 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/stop-dependent-modules.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/stop-dependent-modules.bpmn @@ -62,6 +62,7 @@ + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn index 9b393908d4..5d73109580 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn @@ -31,6 +31,7 @@ + @@ -67,6 +68,7 @@ + @@ -117,6 +119,7 @@ + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-bg-deploy.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-bg-deploy.bpmn index 984c919265..cb1efe93d7 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-bg-deploy.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-bg-deploy.bpmn @@ -1,5 +1,5 @@ - + @@ -24,7 +24,7 @@ - + @@ -80,10 +80,14 @@ - + + + - + + + @@ -93,13 +97,19 @@ - + + + - + + + - + + + @@ -107,14 +117,18 @@ - + + + - + + + @@ -123,7 +137,9 @@ - + + + @@ -172,20 +188,23 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -225,7 +244,9 @@ - + + + @@ -243,11 +264,17 @@ + + + + + + - + @@ -259,10 +286,10 @@ - + - + @@ -271,13 +298,13 @@ - + - + - + @@ -391,7 +418,7 @@ - + @@ -432,342 +459,349 @@ - + + + + - + - + - + - + - + - + - + - + - + - - - + + + - + - - - + + + - + - + - - - + + + - + - + - + - + - - - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - + + + - + - + - + - + - + - + - + - + - + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - + + + - + - + - - - + + + - + - + - + - + - + - + - + - + - + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-deploy.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-deploy.bpmn index 643263993b..a0b6d112e9 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-deploy.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-deploy.bpmn @@ -1,5 +1,5 @@ - + @@ -30,7 +30,6 @@ - @@ -75,10 +74,14 @@ - + + + - + + + @@ -95,13 +98,17 @@ - + + + - + + + @@ -113,10 +120,11 @@ - + + + - @@ -154,20 +162,23 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -176,11 +187,19 @@ + + + + + + + + - + @@ -195,10 +214,10 @@ - + - + @@ -207,13 +226,13 @@ - + - + - + @@ -228,7 +247,7 @@ - + @@ -291,7 +310,7 @@ - + @@ -314,11 +333,14 @@ - + + + + - + @@ -326,233 +348,237 @@ - + - + - + - - - + + + - - - + + + - - - + + + - + - + - - - - - - - - - + + + - - - + + + - + - + - + - - - + + + - + - - + + - + - - - + + + - + - + - + - + - - - - + + + + - + + + + + + + - + - + - - - + + + - + - + - + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - + + + - - - + + + - + - + - + - + - + - + - + - - - + + + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-undeploy.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-undeploy.bpmn index 691e6e4f6c..a45423fb8c 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-undeploy.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-undeploy.bpmn @@ -1,5 +1,5 @@ - + @@ -7,7 +7,7 @@ - + @@ -24,14 +24,10 @@ - + - - - - @@ -62,11 +58,15 @@ - + + + - + + + @@ -110,24 +110,10 @@ - - - - - - - - - - - - - - - - - - + + + + @@ -155,300 +141,320 @@ + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListenerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListenerTest.java index c9c770e640..aa8d138d7f 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListenerTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListenerTest.java @@ -1,11 +1,10 @@ package org.cloudfoundry.multiapps.controller.process.listeners; -import static org.junit.jupiter.api.Assertions.assertEquals; - import org.cloudfoundry.multiapps.controller.api.model.Operation; import org.cloudfoundry.multiapps.controller.api.model.ProcessType; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -24,6 +23,8 @@ import org.mockito.Mock; import org.mockito.Mockito; +import static org.junit.jupiter.api.Assertions.assertEquals; + class EndProcessListenerTest { private final static String SPACE_ID = "9ba1dfc7-9c2c-40d5-8bf9-fd04fa7a1722"; @@ -50,6 +51,8 @@ class EndProcessListenerTest { private StepLogger stepLogger; @Mock private ProcessLoggerPersister processLoggerPersister; + @Mock + private OperationLogsExporter operationLogsExporter; @Test void testNotifyInternal() { @@ -62,7 +65,8 @@ void testNotifyInternal() { configuration, eventHandler, dynatracePublisher, - processTypeParser); + processTypeParser, + operationLogsExporter); // set the process as root process VariableHandling.set(execution, Variables.CORRELATION_ID, execution.getProcessInstanceId()); VariableHandling.set(execution, Variables.SPACE_GUID, SPACE_ID); diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListenerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListenerTest.java index 5274db4857..2e195a4c5f 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListenerTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListenerTest.java @@ -93,7 +93,7 @@ private void prepareOperationService() { } private void prepareStepLogger() { - Mockito.when(stepLoggerFactory.create(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) + Mockito.when(stepLoggerFactory.create(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(stepLogger); } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ExportCloudLoggingConfigurationListenerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ExportCloudLoggingConfigurationListenerTest.java new file mode 100644 index 0000000000..aa431935d6 --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ExportCloudLoggingConfigurationListenerTest.java @@ -0,0 +1,169 @@ +package org.cloudfoundry.multiapps.controller.process.listeners; + +import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; +import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; +import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; +import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; +import org.cloudfoundry.multiapps.controller.process.flowable.FlowableFacade; +import org.cloudfoundry.multiapps.controller.process.util.MockDelegateExecution; +import org.cloudfoundry.multiapps.controller.process.util.StepLogger; +import org.cloudfoundry.multiapps.controller.process.variables.VariableHandling; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.Execution; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ExportCloudLoggingConfigurationListenerTest { + + private static final LoggingConfiguration LOGGING_CONFIGURATION = ImmutableLoggingConfiguration.builder() + .operationId("op-1") + .logLevel(LogLevel.INFO) + .isFailSafe(true) + .build(); + + @Mock + private ProgressMessageService progressMessageService; + @Mock + private ProcessLoggerProvider processLoggerProvider; + @Mock + private HistoricOperationEventService historicOperationEventService; + @Mock + private FlowableFacade flowableFacade; + @Mock + private ApplicationConfiguration configuration; + @Mock + private StepLogger.Factory stepLoggerFactory; + @Mock + private StepLogger stepLogger; + @Mock + private ProcessLoggerPersister processLoggerPersister; + @Mock + private OperationLogsExporter operationLogsExporter; + + @InjectMocks + private ExportCloudLoggingConfigurationListener listener; + + private final DelegateExecution execution = MockDelegateExecution.createSpyInstance(); + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + when(stepLoggerFactory.create(any(), any(), any(), any(), any())).thenReturn(stepLogger); + } + + @Test + void testNotifyInternal_withNoLoggingConfiguration_doesNothing() { + listener.notify(execution); + + verify(flowableFacade, never()).setVariableInParentProcess(any(), anyString(), any()); + verify(flowableFacade, never()).setVariableInParentProcessUsingParentProcessInstanceId(any(), anyString(), any()); + } + + @Test + void testNotifyInternal_withParentProcessInstanceId_setsVariableInParentProcessXSA() { + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, LOGGING_CONFIGURATION); + VariableHandling.set(execution, Variables.PARENT_PROCESS_INSTANCE_ID, "parent-process-1"); + + listener.notify(execution); + + verify(flowableFacade).setVariableInParentProcessUsingParentProcessInstanceId(eq(execution), + eq(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION.getName()), + any()); + } + + @Test + void testNotifyInternal_withParentProcessInstanceId_setsSerializedValue() { + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, LOGGING_CONFIGURATION); + VariableHandling.set(execution, Variables.PARENT_PROCESS_INSTANCE_ID, "parent-process-1"); + + listener.notify(execution); + + verify(flowableFacade).setVariableInParentProcessUsingParentProcessInstanceId(eq(execution), + eq(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION.getName()), + anyString()); + } + + @Test + void testNotifyInternal_withSuperExecution_setsVariableInParentProcess() { + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, LOGGING_CONFIGURATION); + prepareSuperExecution(); + + listener.notify(execution); + + verify(flowableFacade).setVariableInParentProcess(eq(execution), + eq(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION.getName()), any()); + } + + @Test + void testNotifyInternal_withSuperExecution_setsSerializedValue() { + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, LOGGING_CONFIGURATION); + prepareSuperExecution(); + + listener.notify(execution); + + verify(flowableFacade).setVariableInParentProcess(eq(execution), + eq(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION.getName()), + anyString()); + } + + @Test + void testNotifyInternal_withNoParentAndNoSuperExecution_setsVariableInCurrentExecution() { + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, LOGGING_CONFIGURATION); + + listener.notify(execution); + + LoggingConfiguration result = VariableHandling.get(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION); + assertEquals(LOGGING_CONFIGURATION, result); + } + + @Test + void testNotifyInternal_withNoParentAndNoSuperExecution_doesNotPropagateToParent() { + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, LOGGING_CONFIGURATION); + + listener.notify(execution); + + verify(flowableFacade, never()).setVariableInParentProcess(any(), anyString(), any()); + verify(flowableFacade, never()).setVariableInParentProcessUsingParentProcessInstanceId(any(), anyString(), any()); + } + + @Test + void testNotifyInternal_parentProcessInstanceIdTakesPrecedenceOverSuperExecution() { + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, LOGGING_CONFIGURATION); + VariableHandling.set(execution, Variables.PARENT_PROCESS_INSTANCE_ID, "parent-process-1"); + prepareSuperExecution(); + + listener.notify(execution); + + verify(flowableFacade).setVariableInParentProcessUsingParentProcessInstanceId(any(), anyString(), any()); + verify(flowableFacade, never()).setVariableInParentProcess(any(), anyString(), any()); + } + + private void prepareSuperExecution() { + String parentId = "parent-execution-id"; + Mockito.doReturn(parentId) + .when(execution) + .getParentId(); + Execution parentExecution = Mockito.mock(Execution.class); + when(parentExecution.getSuperExecutionId()).thenReturn("super-execution-id"); + when(flowableFacade.getParentExecution(parentId)).thenReturn(parentExecution); + } +} diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListenerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListenerTest.java index 285f3c6ee6..db64a474ab 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListenerTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListenerTest.java @@ -8,6 +8,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudApplication; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -53,6 +54,9 @@ class ManageAppServiceBindingEndListenerTest { private ProcessLoggerPersister processLoggerPersister; @Mock private ApplicationConfiguration configuration; + @Mock + private OperationLogsExporter operationLogsExporter; + private ManageAppServiceBindingEndListener manageAppServiceBindingEndListener; @BeforeEach @@ -66,7 +70,8 @@ void setUp() throws Exception { historicOperationEventService, flowableFacade, configuration, - processTypeParser); + processTypeParser, + operationLogsExporter); } // @formatter:off diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListenerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListenerTest.java index bd4c656ab0..8b86ebbdf6 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListenerTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListenerTest.java @@ -1,8 +1,5 @@ package org.cloudfoundry.multiapps.controller.process.listeners; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; - import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -21,6 +18,7 @@ import org.cloudfoundry.multiapps.controller.persistence.services.FileService; import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.OperationService; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; @@ -47,6 +45,9 @@ import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; + class StartProcessListenerTest { private static final String SPACE_ID = "9ba1dfc7-9c2c-40d5-8bf9-fd04fa7a1722"; @@ -88,15 +89,17 @@ class StartProcessListenerTest { private FlowableFacade flowableFacade; @Mock private ProcessLoggerPersister processLoggerPersister; + @Mock + private OperationLogsExporter operationLogsExporter; private StartProcessListener listener; static Stream testVerify() { return Stream.of( - // (0) Create Operation for process undeploy - Arguments.of("process-instance-id", ProcessType.UNDEPLOY), - // (1) Create Operation for process deploy - Arguments.of("process-instance-id", ProcessType.DEPLOY)); + // (0) Create Operation for process undeploy + Arguments.of("process-instance-id", ProcessType.UNDEPLOY), + // (1) Create Operation for process deploy + Arguments.of("process-instance-id", ProcessType.DEPLOY)); } @BeforeEach @@ -114,7 +117,8 @@ void setUp() throws Exception { operationService, operationMetadataMapper, dynatracePublisher, - fileService); + fileService, + operationLogsExporter); } @ParameterizedTest @@ -132,7 +136,7 @@ void testVerify(String processInstanceId, ProcessType processType) throws Except private void prepare() { prepareContext(); - Mockito.when(stepLoggerFactory.create(any(), any(), any(), any())) + Mockito.when(stepLoggerFactory.create(any(), any(), any(), any(), any())) .thenReturn(stepLogger); Mockito.when(operationService.createQuery()) .thenReturn(operationQuery); diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/CollectCloudLoggingServiceParametersStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/CollectCloudLoggingServiceParametersStepTest.java new file mode 100644 index 0000000000..95ee954db5 --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/CollectCloudLoggingServiceParametersStepTest.java @@ -0,0 +1,281 @@ +package org.cloudfoundry.multiapps.controller.process.steps; + +import java.util.List; + +import org.cloudfoundry.multiapps.controller.api.model.ProcessType; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; +import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; +import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableOperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.OperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; +import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; +import org.cloudfoundry.multiapps.controller.process.util.ProcessTypeParser; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class CollectCloudLoggingServiceParametersStepTest extends SyncFlowableStepTest { + + private static final String MTA_ID = "test-mta"; + private static final String NAMESPACE = "ns-1"; + private static final String CONFIG_ID = "config-id-1"; + private static final String CLOUD_LOGGING_RESOURCE_TYPE = "org.cloudfoundry.cloud-logging-service"; + + private TokenService tokenService; + private CloudControllerClientFactory clientFactory; + private CloudLoggingServiceConfigurationService configurationService; + private ProcessTypeParser processTypeParser; + private CloudLoggingServiceConfigurationAuditLog auditLog; + + /** + * Production path goes through new LoggingConfigurationBuilder(...).exportOperationLogsToExternalSystem(...) which needs a real + * TokenService + CloudControllerClientFactory + an HTTP call. We override both protected hooks so the test pins the step's branching + * logic without dragging the builder into the unit test. + */ + static class TestableStep extends CollectCloudLoggingServiceParametersStep { + + LoggingConfiguration nextBuiltFromDescriptor; + LoggingConfiguration nextBuiltFromExisting; + + TestableStep(TokenService tokenService, CloudControllerClientFactory clientFactory, + CloudLoggingServiceConfigurationService configurationService, ProcessTypeParser processTypeParser, + CloudLoggingServiceConfigurationAuditLog auditLog) { + super(tokenService, clientFactory, configurationService, processTypeParser, auditLog); + } + + @Override + protected LoggingConfiguration setExternalLoggingServiceConfigurationIfRequired(ProcessContext context, + DeploymentDescriptor deploymentDescriptor) { + return nextBuiltFromDescriptor; + } + + @Override + protected LoggingConfiguration setExternalLoggingServiceConfigurationIfRequired(ProcessContext context, + LoggingConfiguration loggingConfiguration) { + return nextBuiltFromExisting == null ? loggingConfiguration : nextBuiltFromExisting; + } + } + + @BeforeEach + void prepareContext() { + context.setVariable(Variables.MTA_ID, MTA_ID); + context.setVariable(Variables.MTA_NAMESPACE, NAMESPACE); + } + + @Override + protected TestableStep createStep() { + tokenService = Mockito.mock(TokenService.class); + clientFactory = Mockito.mock(CloudControllerClientFactory.class); + configurationService = Mockito.mock(CloudLoggingServiceConfigurationService.class); + processTypeParser = Mockito.mock(ProcessTypeParser.class); + auditLog = Mockito.mock(CloudLoggingServiceConfigurationAuditLog.class); + // operationLogsExporter is supplied by the parent harness as a @Mock so the parent's StepLogger setup keeps working. + return new TestableStep(tokenService, clientFactory, configurationService, processTypeParser, auditLog); + } + + // --- UNDEPLOY --- + + @Test + void undeploy_noExistingConfig_finishesWithoutSettingVariable() { + when(processTypeParser.getProcessType(any())).thenReturn(ProcessType.UNDEPLOY); + when(configurationService.getCloudLoggingServiceConfiguration(SPACE_NAME, MTA_ID, NAMESPACE)).thenReturn(null); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertNull(context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION)); + verify(auditLog, never()).logGetLoggingConfiguration(any(), any(), any()); + verify(operationLogsExporter, never()).getUnsendProcessLogs(any()); + } + + @Test + void undeploy_existingConfig_setsVariableAndAuditsGet() throws FileStorageException { + LoggingConfiguration existing = buildConfig(); + when(processTypeParser.getProcessType(any())).thenReturn(ProcessType.UNDEPLOY); + when(configurationService.getCloudLoggingServiceConfiguration(SPACE_NAME, MTA_ID, NAMESPACE)).thenReturn(existing); + when(operationLogsExporter.getUnsendProcessLogs(existing)).thenReturn(List.of()); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(existing, context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION)); + verify(auditLog).logGetLoggingConfiguration(USER_NAME, SPACE_GUID, existing); + verify(configurationService, never()).storeCloudLoggingServiceConfiguration(any()); + verify(configurationService, never()).updateCloudLoggingServiceConfiguration(any()); + verify(configurationService, never()).deleteCloudLoggingServiceConfiguration(any()); + } + + // --- DEPLOY: descriptor has no cloud-logging resource --- + + @Test + void deploy_noCloudLoggingResource_noExistingConfig_finishesWithoutSideEffects() { + prepareDeployContext(descriptorWithoutCloudLogging()); + when(configurationService.getCloudLoggingServiceConfiguration(SPACE_NAME, MTA_ID, NAMESPACE)).thenReturn(null); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertNull(context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION)); + verify(configurationService, never()).deleteCloudLoggingServiceConfiguration(any()); + verify(auditLog, never()).logDeleteLoggingConfiguration(any(), any(), any()); + verify(operationLogsExporter, never()).getUnsendProcessLogs(any()); + } + + @Test + void deploy_noCloudLoggingResource_existingConfig_deletesAndAudits() { + LoggingConfiguration existing = buildConfig(); + prepareDeployContext(descriptorWithoutCloudLogging()); + when(configurationService.getCloudLoggingServiceConfiguration(SPACE_NAME, MTA_ID, NAMESPACE)).thenReturn(existing); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertNull(context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION)); + verify(auditLog).logDeleteLoggingConfiguration(USER_NAME, SPACE_GUID, existing); + verify(configurationService).deleteCloudLoggingServiceConfiguration(CONFIG_ID); + verify(operationLogsExporter, never()).getUnsendProcessLogs(any()); + } + + // --- DEPLOY: descriptor has a cloud-logging resource --- + + @Test + void deploy_cloudLoggingResource_builderReturnsNull_finishesWithoutPersistingOrSettingVariable() { + prepareDeployContext(descriptorWithCloudLogging()); + when(configurationService.getCloudLoggingServiceConfiguration(SPACE_NAME, MTA_ID, NAMESPACE)).thenReturn(null); + step.nextBuiltFromDescriptor = null; + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertNull(context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION)); + verify(configurationService, never()).storeCloudLoggingServiceConfiguration(any()); + verify(configurationService, never()).updateCloudLoggingServiceConfiguration(any()); + verify(operationLogsExporter, never()).getUnsendProcessLogs(any()); + } + + @Test + void deploy_cloudLoggingResource_noExistingConfig_storesAndAuditsCreate() throws FileStorageException { + LoggingConfiguration newConfig = buildConfig(); + prepareDeployContext(descriptorWithCloudLogging()); + when(configurationService.getCloudLoggingServiceConfiguration(SPACE_NAME, MTA_ID, NAMESPACE)).thenReturn(null); + step.nextBuiltFromDescriptor = newConfig; + when(operationLogsExporter.getUnsendProcessLogs(newConfig)).thenReturn(List.of()); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(newConfig, context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION)); + verify(auditLog).logCreateLoggingConfiguration(USER_NAME, SPACE_GUID, newConfig); + verify(configurationService).storeCloudLoggingServiceConfiguration(newConfig); + verify(configurationService, never()).updateCloudLoggingServiceConfiguration(any()); + } + + @Test + void deploy_cloudLoggingResource_existingConfig_updatesAndAuditsUpdateAndGet() throws FileStorageException { + LoggingConfiguration existing = buildConfig(); + LoggingConfiguration rebuilt = ImmutableLoggingConfiguration.builder() + .from(existing) + .logLevel(LogLevel.ERROR) + .build(); + prepareDeployContext(descriptorWithCloudLogging()); + when(configurationService.getCloudLoggingServiceConfiguration(SPACE_NAME, MTA_ID, NAMESPACE)).thenReturn(existing); + step.nextBuiltFromDescriptor = rebuilt; + when(operationLogsExporter.getUnsendProcessLogs(rebuilt)).thenReturn(List.of()); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(rebuilt, context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION)); + verify(auditLog).logGetLoggingConfiguration(USER_NAME, SPACE_GUID, existing); + verify(auditLog).logUpdateLoggingConfiguration(USER_NAME, SPACE_GUID, rebuilt); + verify(configurationService).updateCloudLoggingServiceConfiguration(rebuilt); + verify(configurationService, never()).storeCloudLoggingServiceConfiguration(any()); + } + + // --- log forwarding --- + + @Test + void deploy_cloudLoggingResource_forwardsEveryUnsentEntry() throws FileStorageException { + LoggingConfiguration newConfig = buildConfig(); + OperationLogEntry entry1 = ImmutableOperationLogEntry.builder() + .operationId("op-1") + .operationLog("log-1") + .operationLogName("svc-1") + .build(); + OperationLogEntry entry2 = ImmutableOperationLogEntry.builder() + .operationId("op-2") + .operationLog("log-2") + .operationLogName("svc-2") + .build(); + prepareDeployContext(descriptorWithCloudLogging()); + when(configurationService.getCloudLoggingServiceConfiguration(SPACE_NAME, MTA_ID, NAMESPACE)).thenReturn(null); + step.nextBuiltFromDescriptor = newConfig; + when(operationLogsExporter.getUnsendProcessLogs(newConfig)).thenReturn(List.of(entry1, entry2)); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + verify(operationLogsExporter).sendLogsToCloudLoggingService(newConfig, entry1); + verify(operationLogsExporter).sendLogsToCloudLoggingService(newConfig, entry2); + } + + @Test + void deploy_cloudLoggingResource_noUnsentEntries_doesNotForward() throws FileStorageException { + LoggingConfiguration newConfig = buildConfig(); + prepareDeployContext(descriptorWithCloudLogging()); + when(configurationService.getCloudLoggingServiceConfiguration(SPACE_NAME, MTA_ID, NAMESPACE)).thenReturn(null); + step.nextBuiltFromDescriptor = newConfig; + when(operationLogsExporter.getUnsendProcessLogs(newConfig)).thenReturn(List.of()); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + verify(operationLogsExporter, never()).sendLogsToCloudLoggingService(any(), Mockito.any()); + } + + // --- helpers --- + + private void prepareDeployContext(DeploymentDescriptor descriptor) { + when(processTypeParser.getProcessType(any())).thenReturn(ProcessType.DEPLOY); + context.setVariable(Variables.DEPLOYMENT_DESCRIPTOR, descriptor); + } + + private static DeploymentDescriptor descriptorWithCloudLogging() { + return DeploymentDescriptor.createV3() + .setResources(List.of(Resource.createV3() + .setName("my-cls") + .setType(CLOUD_LOGGING_RESOURCE_TYPE))); + } + + private static DeploymentDescriptor descriptorWithoutCloudLogging() { + return DeploymentDescriptor.createV3() + .setResources(List.of(Resource.createV3() + .setName("not-cls") + .setType("org.cloudfoundry.managed-service"))); + } + + private static LoggingConfiguration buildConfig() { + return ImmutableLoggingConfiguration.builder() + .id(CONFIG_ID) + .mtaId(MTA_ID) + .mtaSpaceId(SPACE_NAME) + .namespace(NAMESPACE) + .logLevel(LogLevel.INFO) + .isFailSafe(true) + .build(); + } +} diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteAppStatusExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteAppStatusExecutionTest.java index e9efd4546c..29b7e97f74 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteAppStatusExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteAppStatusExecutionTest.java @@ -27,6 +27,7 @@ import org.cloudfoundry.multiapps.controller.core.cf.apps.ApplicationStateAction; import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLogger; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.process.util.MockDelegateExecution; @@ -77,6 +78,8 @@ class PollExecuteAppStatusExecutionTest { private TokenService tokenService; @Mock private LogCacheClient logCacheClient; + @Mock + private OperationLogsExporter operationLogsExporter; private DelegateExecution execution; private ProcessContext context; @@ -88,7 +91,7 @@ void setUp() throws Exception { .close(); execution = MockDelegateExecution.createSpyInstance(); context = new ProcessContext(execution, stepLogger, clientProvider); - step = new PollExecuteAppStatusExecution(clientFactory, tokenService); + step = new PollExecuteAppStatusExecution(clientFactory, tokenService, operationLogsExporter); } static Stream testStep() { diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollIncrementalAppInstanceUpdateExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollIncrementalAppInstanceUpdateExecutionTest.java index eea3294143..60eb98260d 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollIncrementalAppInstanceUpdateExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollIncrementalAppInstanceUpdateExecutionTest.java @@ -31,7 +31,6 @@ class PollIncrementalAppInstanceUpdateExecutionTest extends AsyncStepOperationTe private CloudControllerClientFactory clientFactory; private TokenService tokenService; - private AsyncExecutionState expectedAsyncExecutionState; @Test diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecutionTest.java index d580c993a7..77077ba6e1 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecutionTest.java @@ -12,6 +12,7 @@ import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientProvider; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.util.ApplicationStager; import org.cloudfoundry.multiapps.controller.process.util.ImmutableStagingState; import org.cloudfoundry.multiapps.controller.process.util.MockDelegateExecution; @@ -51,6 +52,8 @@ class PollStageAppStatusExecutionTest { private CloudControllerClientFactory clientFactory; @Mock private TokenService tokenService; + @Mock + private OperationLogsExporter operationLogsExporter; private ProcessContext context; private DelegateExecution execution; @@ -62,7 +65,7 @@ void setUp() throws Exception { .close(); execution = MockDelegateExecution.createSpyInstance(); context = new ProcessContext(execution, stepLogger, clientProvider); - step = new PollStageAppStatusExecution(applicationStager, clientFactory, tokenService); + step = new PollStageAppStatusExecution(applicationStager, clientFactory, tokenService, operationLogsExporter); } static Stream testStep() { diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecutionTest.java index a7bcb41110..46759e9b2f 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecutionTest.java @@ -39,7 +39,6 @@ class PollStartAppExecutionWithRollbackExecutionTest extends AsyncStepOperationT private CloudControllerClientFactory clientFactory; private TokenService tokenService; - private AsyncExecutionState expectedAsyncExecutionState; @Test @@ -151,7 +150,7 @@ void testOnSuccessWhenNewAppStillIsStarted() { @Override protected List getAsyncOperations(ProcessContext wrapper) { - return List.of(new PollStartAppExecutionWithRollbackExecution(clientFactory, tokenService)); + return List.of(new PollStartAppExecutionWithRollbackExecution(clientFactory, tokenService, operationLogsExporter)); } @Override diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecutionTest.java index 0a5d2cebb3..c593c7bfdd 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecutionTest.java @@ -21,6 +21,7 @@ import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientProvider; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.util.MockDelegateExecution; import org.cloudfoundry.multiapps.controller.process.util.StepLogger; import org.cloudfoundry.multiapps.controller.process.variables.Variables; @@ -60,6 +61,8 @@ class PollStartAppStatusExecutionTest { private CloudControllerClientFactory clientFactory; @Mock private TokenService tokenService; + @Mock + private OperationLogsExporter operationLogsExporter; private ProcessContext context; private PollStartAppStatusExecution step; @@ -70,7 +73,7 @@ void setUp() throws Exception { .close(); DelegateExecution execution = MockDelegateExecution.createSpyInstance(); context = new ProcessContext(execution, stepLogger, clientProvider); - step = new PollStartAppStatusExecution(clientFactory, tokenService); + step = new PollStartAppStatusExecution(clientFactory, tokenService, operationLogsExporter); } static Stream testStep() { diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecutionTest.java index 9e321293e1..039199c6df 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecutionTest.java @@ -14,6 +14,7 @@ import org.cloudfoundry.multiapps.controller.core.model.ImmutableDeployedMta; import org.cloudfoundry.multiapps.controller.core.model.ImmutableDeployedMtaApplication; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,6 +36,8 @@ class PollStartLiveAppExecutionTest { private TokenService tokenService; @Mock private ProcessContext context; + @Mock + private OperationLogsExporter operationLogsExporter; private PollStartLiveAppExecution pollStartLiveAppExecution; @@ -42,7 +45,7 @@ class PollStartLiveAppExecutionTest { void setUp() throws Exception { MockitoAnnotations.openMocks(this) .close(); - pollStartLiveAppExecution = new PollStartLiveAppExecution(clientFactory, tokenService); + pollStartLiveAppExecution = new PollStartLiveAppExecution(clientFactory, tokenService, operationLogsExporter); } @Test diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelperTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelperTest.java index 7db241c7e4..4b4b117a27 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelperTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelperTest.java @@ -1,7 +1,5 @@ package org.cloudfoundry.multiapps.controller.process.steps; -import static org.mockito.ArgumentMatchers.any; - import java.util.Arrays; import java.util.List; import java.util.UUID; @@ -12,6 +10,7 @@ import org.cloudfoundry.multiapps.controller.core.model.ErrorType; import org.cloudfoundry.multiapps.controller.persistence.model.HistoricOperationEvent; import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableHistoricOperationEvent; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLogger; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -33,6 +32,8 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import static org.mockito.ArgumentMatchers.any; + class ProcessStepHelperTest { private static final String CORRELATION_GUID = UUID.randomUUID() @@ -54,6 +55,9 @@ class ProcessStepHelperTest { private ProcessContext context; @Mock private DelegateExecution execution; + @Mock + private OperationLogsExporter operationLogsExporter; + private ProcessStepHelper processStepHelper; @BeforeEach @@ -68,6 +72,7 @@ void setUp() throws Exception { .processEngineConfiguration(processEngineConfiguration) .processHelper(processHelper) .processLoggerPersister(processLoggerPersister) + .operationLogsExporter(operationLogsExporter) .build(); } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStepTest.java index a083c69ff0..984e032fde 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStepTest.java @@ -21,6 +21,7 @@ import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.FileService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLogsPersistenceService; @@ -109,6 +110,8 @@ public abstract class SyncFlowableStepTest { protected final ProcessLoggerProvider processLoggerProvider = Mockito.spy(ProcessLoggerProvider.class); @Mock protected ProcessHelper processHelper; + @Mock + protected OperationLogsExporter operationLogsExporter; protected ProcessContext context; @InjectMocks @@ -120,9 +123,10 @@ public abstract class SyncFlowableStepTest { public void initMocks() throws Exception { MockitoAnnotations.openMocks(this) .close(); - this.stepLogger = Mockito.spy(new StepLogger(execution, progressMessageService, processLoggerProvider, LOGGER)); + this.stepLogger = Mockito.spy( + new StepLogger(execution, progressMessageService, processLoggerProvider, LOGGER, operationLogsExporter)); + when(stepLoggerFactory.create(any(), any(), any(), any(), any())).thenReturn(stepLogger); this.context = step.createProcessContext(execution); - when(stepLoggerFactory.create(any(), any(), any(), any())).thenReturn(stepLogger); context.setVariable(Variables.SPACE_NAME, SPACE_NAME); context.setVariable(Variables.SPACE_GUID, SPACE_GUID); context.setVariable(Variables.USER, USER_NAME); diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecutionTest.java index 452fb59c15..4e832ac010 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecutionTest.java @@ -26,6 +26,9 @@ import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.helpers.MtaArchiveElements; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.FileContentConsumer; import org.cloudfoundry.multiapps.controller.persistence.services.FileService; import org.cloudfoundry.multiapps.controller.process.util.ApplicationArchiveIterator; @@ -37,6 +40,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; import org.springframework.http.HttpStatus; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -49,7 +53,9 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class UploadAppAsyncExecutionTest extends AsyncStepOperationTest { @@ -198,6 +204,93 @@ void testSkippingUpload() { testExecuteOperations(); } + @Test + void testSendApplicationLogsToCloudLoggingService_sendsLogsForEachProcessLog() { + prepareExecutorService(); + context.setVariable(Variables.ARCHIVE_ENTRIES_POSITIONS, List.of(ARCHIVE_ENTRY_WITH_STREAM_POSITIONS)); + LoggingConfiguration loggingConfiguration = buildLoggingConfiguration(); + context.setVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, loggingConfiguration); + when(client.asyncUploadApplicationWithExponentialBackoff(eq(APP_NAME), eq(appFile), any(UploadStatusCallback.class), + any())).thenReturn(CLOUD_PACKAGE); + when(step.getProcessLogsPersister().getApplicationProcessLogsMessages(TEST_CORRELATION_ID, + TEST_TASK_ID)).thenReturn(List.of("log-1", "log-2")); + expectedStatus = AsyncExecutionState.FINISHED; + + testExecuteOperations(); + triggerOnProgress(Status.READY.toString()); + + verify(operationLogsExporter).sendLogsToCloudLoggingService(loggingConfiguration, "log-1"); + verify(operationLogsExporter).sendLogsToCloudLoggingService(loggingConfiguration, "log-2"); + } + + @Test + void testSendApplicationLogsToCloudLoggingService_withNullLoggingConfiguration_stillSendsLogs() { + prepareExecutorService(); + context.setVariable(Variables.ARCHIVE_ENTRIES_POSITIONS, List.of(ARCHIVE_ENTRY_WITH_STREAM_POSITIONS)); + context.setVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, null); + when(client.asyncUploadApplicationWithExponentialBackoff(eq(APP_NAME), eq(appFile), any(UploadStatusCallback.class), + any())).thenReturn(CLOUD_PACKAGE); + when(step.getProcessLogsPersister().getApplicationProcessLogsMessages(TEST_CORRELATION_ID, + TEST_TASK_ID)).thenReturn(List.of("log-1")); + expectedStatus = AsyncExecutionState.FINISHED; + + testExecuteOperations(); + triggerOnProgress(Status.READY.toString()); + + verify(operationLogsExporter).sendLogsToCloudLoggingService(null, "log-1"); + } + + @Test + void testSendApplicationLogsToCloudLoggingService_withNoLogs_doesNotSendProcessLogs() { + prepareExecutorService(); + context.setVariable(Variables.ARCHIVE_ENTRIES_POSITIONS, List.of(ARCHIVE_ENTRY_WITH_STREAM_POSITIONS)); + LoggingConfiguration loggingConfiguration = buildLoggingConfiguration(); + context.setVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, loggingConfiguration); + when(client.asyncUploadApplicationWithExponentialBackoff(eq(APP_NAME), eq(appFile), any(UploadStatusCallback.class), + any())).thenReturn(CLOUD_PACKAGE); + when(step.getProcessLogsPersister().getApplicationProcessLogsMessages(TEST_CORRELATION_ID, + TEST_TASK_ID)).thenReturn(Collections.emptyList()); + expectedStatus = AsyncExecutionState.FINISHED; + + testExecuteOperations(); + triggerOnProgress(Status.READY.toString()); + + verify(operationLogsExporter, never()).sendLogsToCloudLoggingService(eq(loggingConfiguration), eq("process-log-content")); + } + + @Test + void testSendApplicationLogsToCloudLoggingService_notCalledWhenStatusIsNotReady() { + prepareExecutorService(); + context.setVariable(Variables.ARCHIVE_ENTRIES_POSITIONS, List.of(ARCHIVE_ENTRY_WITH_STREAM_POSITIONS)); + LoggingConfiguration loggingConfiguration = buildLoggingConfiguration(); + context.setVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, loggingConfiguration); + when(client.asyncUploadApplicationWithExponentialBackoff(eq(APP_NAME), eq(appFile), any(UploadStatusCallback.class), + any())).thenReturn(CLOUD_PACKAGE); + when(step.getProcessLogsPersister().getApplicationProcessLogsMessages(TEST_CORRELATION_ID, + TEST_TASK_ID)).thenReturn(List.of("process-log-content")); + expectedStatus = AsyncExecutionState.FINISHED; + + testExecuteOperations(); + triggerOnProgress(Status.AWAITING_UPLOAD.toString()); + + verify(operationLogsExporter, never()).sendLogsToCloudLoggingService(eq(loggingConfiguration), eq("process-log-content")); + } + + private void triggerOnProgress(String status) { + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(UploadStatusCallback.class); + verify(client).asyncUploadApplicationWithExponentialBackoff(eq(APP_NAME), eq(appFile), callbackCaptor.capture(), any()); + callbackCaptor.getValue() + .onProgress(status); + } + + private LoggingConfiguration buildLoggingConfiguration() { + return ImmutableLoggingConfiguration.builder() + .operationId(TEST_CORRELATION_ID) + .logLevel(LogLevel.INFO) + .isFailSafe(true) + .build(); + } + @Override protected List getAsyncOperations(ProcessContext wrapper) { return step.getAsyncStepExecutions(wrapper); @@ -220,7 +313,8 @@ protected List getAsyncStepExecutions(ProcessContext context) { return List.of(new UploadAppAsyncExecution(applicationZipBuilder, getProcessLogsPersister(), configuration, - appUploaderThreadPool) { + appUploaderThreadPool, + operationLogsExporter) { }); } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/LoggingConfigurationBuilderTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/LoggingConfigurationBuilderTest.java new file mode 100644 index 0000000000..2730bb301f --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/LoggingConfigurationBuilderTest.java @@ -0,0 +1,382 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceInstance; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceKey; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudSpace; +import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; +import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientProvider; +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; +import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.process.steps.ProcessContext; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.Resource; +import org.flowable.engine.delegate.DelegateExecution; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class LoggingConfigurationBuilderTest { + + private static final String CORRELATION_ID = "op-1"; + private static final String SPACE_NAME = "my-space"; + private static final UUID SPACE_GUID_UUID = UUID.randomUUID(); + private static final String SPACE_GUID = SPACE_GUID_UUID.toString(); + private static final String ORG_NAME = "my-org"; + private static final String USER_GUID = "user-guid-1"; + private static final String MTA_ID = "my-mta"; + private static final String NAMESPACE = "dev"; + private static final String SERVICE_INSTANCE = "my-cls-instance"; + private static final String SERVICE_KEY_NAME = "my-cls-key"; + + private static final Map SERVICE_KEY_CREDENTIALS = Map.of( + "ingest-mtls-endpoint", "https://cls.example.com", + "server-ca", "server-ca-cert", + "ingest-mtls-cert", "client-cert", + "ingest-mtls-key", "client-key" + ); + + @Mock + private CloudControllerClientFactory clientFactory; + @Mock + private CloudControllerClient client; + @Mock + private CloudControllerClientProvider clientProvider; + @Mock + private TokenService tokenService; + @Mock + private StepLogger stepLogger; + + private ProcessContext context; + private LoggingConfigurationBuilder calculator; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + DelegateExecution execution = MockDelegateExecution.createSpyInstance(); + when(clientProvider.getControllerClient(anyString(), anyString(), anyString())).thenReturn(client); + when(client.getTarget()).thenReturn(ImmutableCloudSpace.builder() + .metadata(ImmutableCloudMetadata.builder() + .guid(SPACE_GUID_UUID) + .build()) + .name(SPACE_NAME) + .build()); + context = new ProcessContext(execution, stepLogger, clientProvider); + context.setVariable(Variables.CORRELATION_ID, CORRELATION_ID); + context.setVariable(Variables.SPACE_NAME, SPACE_NAME); + context.setVariable(Variables.SPACE_GUID, SPACE_GUID); + context.setVariable(Variables.ORGANIZATION_NAME, ORG_NAME); + context.setVariable(Variables.USER_GUID, USER_GUID); + context.setVariable(Variables.MTA_ID, MTA_ID); + context.setVariable(Variables.MTA_NAMESPACE, NAMESPACE); + calculator = new LoggingConfigurationBuilder(clientFactory, context, tokenService); + } + + // --- exportOperationLogsToExternalSystem(Resource) --- + + @Test + void testExportWithResource_returnsNullWhenServiceKeyIsNull() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(null); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNull(result); + } + + @Test + void testExportWithResource_throwsWhenServiceKeyIsNullAndNotFailSafe() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(null); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, false); + + assertThrows(SLException.class, () -> calculator.exportOperationLogsToExternalSystem(resource)); + } + + @Test + void testExportWithResource_returnsNullWhenServiceInstanceNameIsBlank() { + // NameUtil.getServiceInstanceNameOrDefault falls back to resource.getName() when SERVICE_NAME is blank, + // so we must also blank the resource name to hit the "invalid params" branch. + Resource resource = Resource.createV3() + .setName("") + .setOptional(true) + .setParameters(Map.of(SupportedParameters.SERVICE_NAME, "", + SupportedParameters.SERVICE_KEY_NAME, SERVICE_KEY_NAME)); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNull(result); + verify(client, never()).getServiceKey(anyString(), anyString()); + } + + @Test + void testExportWithResource_returnsNullWhenServiceKeyNameIsBlank() { + Resource resource = buildResource(SERVICE_INSTANCE, "", true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNull(result); + verify(client, never()).getServiceKey(anyString(), anyString()); + } + + @Test + void testExportWithResource_throwsWhenMissingParametersAndNotFailSafe() { + Resource resource = buildResource(null, SERVICE_KEY_NAME, false); + + assertThrows(SLException.class, () -> calculator.exportOperationLogsToExternalSystem(resource)); + } + + @Test + void testExportWithResource_returnsNullWhenCloudOperationExceptionAndFailSafe() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenThrow(new CloudOperationException(HttpStatus.NOT_FOUND)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNull(result); + } + + @Test + void testExportWithResource_throwsWhenCloudOperationExceptionAndNotFailSafe() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenThrow(new CloudOperationException(HttpStatus.NOT_FOUND)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, false); + + assertThrows(SLException.class, () -> calculator.exportOperationLogsToExternalSystem(resource)); + } + + @Test + void testExportWithResource_populatesCredentialsFromServiceKey() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn( + buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNotNull(result); + assertEquals("https://cls.example.com", result.getEndpointUrl()); + assertEquals("server-ca-cert", result.getServerCa()); + assertEquals("client-cert", result.getClientCert()); + assertEquals("client-key", result.getClientKey()); + } + + @Test + void testExportWithResource_populatesContextFields() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn( + buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNotNull(result); + assertEquals(CORRELATION_ID, result.getOperationId()); + assertEquals(MTA_ID, result.getMtaId()); + assertEquals(SPACE_GUID, result.getMtaSpaceId()); + assertEquals(SPACE_NAME, result.getMtaSpace()); + assertEquals(ORG_NAME, result.getMtaOrg()); + assertEquals(NAMESPACE, result.getNamespace()); + } + + @Test + void testExportWithResource_setsFailSafeFromResourceOptional() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn( + buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNotNull(result); + assertEquals(true, result.isFailSafe()); + } + + @Test + void testExportWithResource_defaultLogLevelIsInfo() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn( + buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNotNull(result); + assertEquals(LogLevel.INFO, result.getLogLevel()); + } + + static Stream testExportWithResource_logLevelFromDescriptor() { + return Stream.of(Arguments.of("INFO", LogLevel.INFO), Arguments.of("WARN", LogLevel.WARN), Arguments.of("DEBUG", LogLevel.DEBUG), + Arguments.of("ERROR", LogLevel.ERROR), Arguments.of("TRACE", LogLevel.TRACE)); + } + + @ParameterizedTest + @MethodSource + void testExportWithResource_logLevelFromDescriptor(String descriptorLevel, LogLevel expectedLevel) { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn( + buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true, Map.of(SupportedParameters.LOG_LEVEL, descriptorLevel)); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNotNull(result); + assertEquals(expectedLevel, result.getLogLevel()); + } + + @Test + void testExportWithResource_usesResourceNameAsServiceInstanceWhenNoServiceNameParameter() { + when(client.getServiceKey("resource-name", SERVICE_KEY_NAME)).thenReturn( + buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + Resource resource = Resource.createV3() + .setName("resource-name") + .setOptional(true) + .setParameters(Map.of(SupportedParameters.SERVICE_KEY_NAME, SERVICE_KEY_NAME)); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNotNull(result); + assertEquals("resource-name", result.getServiceInstanceName()); + } + + @Test + void testExportWithResource_usesDestinationOrgAndSpace() { + when(clientFactory.createClient(any(), anyString(), anyString(), anyString())).thenReturn(client); + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn( + buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true, + Map.of(SupportedParameters.DESTINATION, + Map.of("org-name", "other-org", "space-name", "other-space"))); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNotNull(result); + assertEquals("other-org", result.getTargetOrg()); + assertEquals("other-space", result.getTargetSpace()); + } + + // --- exportOperationLogsToExternalSystem(LoggingConfiguration, ProcessContext) --- + + @Test + void testExportWithLoggingConfiguration_returnsNullWhenServiceKeyIsNull() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(null); + LoggingConfiguration incomingConfig = buildIncomingConfig(true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(incomingConfig, context); + + assertNull(result); + } + + @Test + void testExportWithLoggingConfiguration_throwsWhenServiceKeyIsNullAndNotFailSafe() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(null); + LoggingConfiguration incomingConfig = buildIncomingConfig(false); + + assertThrows(SLException.class, () -> calculator.exportOperationLogsToExternalSystem(incomingConfig, context)); + } + + @Test + void testExportWithLoggingConfiguration_populatesCredentialsFromServiceKey() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn( + buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + LoggingConfiguration incomingConfig = buildIncomingConfig(true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(incomingConfig, context); + + assertNotNull(result); + assertEquals("https://cls.example.com", result.getEndpointUrl()); + assertEquals("server-ca-cert", result.getServerCa()); + assertEquals("client-cert", result.getClientCert()); + assertEquals("client-key", result.getClientKey()); + } + + @Test + void testExportWithLoggingConfiguration_setsOperationIdAndSpaceIdFromContext() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn( + buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + LoggingConfiguration incomingConfig = buildIncomingConfig(true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(incomingConfig, context); + + assertNotNull(result); + assertEquals(CORRELATION_ID, result.getOperationId()); + assertEquals(SPACE_GUID, result.getMtaSpaceId()); + } + + @Test + void testExportWithLoggingConfiguration_throwsWhenMissingCredentialInServiceKey() { + Map incompleteCredentials = Map.of("ingest-mtls-endpoint", "https://cls.example.com"); + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(buildServiceKey(SERVICE_KEY_NAME, incompleteCredentials)); + LoggingConfiguration incomingConfig = buildIncomingConfig(true); + + assertThrows(SLException.class, () -> calculator.exportOperationLogsToExternalSystem(incomingConfig, context)); + } + + // --- Helpers --- + + private static Resource buildResource(String serviceInstanceName, String serviceKeyName, boolean optional) { + return buildResource(serviceInstanceName, serviceKeyName, optional, Map.of()); + } + + private static Resource buildResource(String serviceInstanceName, String serviceKeyName, boolean optional, + Map extraParameters) { + java.util.Map params = new java.util.HashMap<>(extraParameters); + if (serviceInstanceName != null) { + params.put(SupportedParameters.SERVICE_NAME, serviceInstanceName); + } + if (serviceKeyName != null) { + params.put(SupportedParameters.SERVICE_KEY_NAME, serviceKeyName); + } + return Resource.createV3() + .setName("cls-resource") + .setOptional(optional) + .setParameters(params); + } + + private static LoggingConfiguration buildIncomingConfig(boolean failSafe) { + return ImmutableLoggingConfiguration.builder() + .serviceInstanceName(SERVICE_INSTANCE) + .serviceKeyName(SERVICE_KEY_NAME) + .targetOrg(ORG_NAME) + .targetSpace(SPACE_NAME) + .logLevel(LogLevel.INFO) + .isFailSafe(failSafe) + .build(); + } + + private static ImmutableCloudServiceKey buildServiceKey(String name, Map credentials) { + return ImmutableCloudServiceKey.builder() + .name(name) + .metadata(ImmutableCloudMetadata.builder() + .build()) + .credentials(credentials) + .serviceInstance(ImmutableCloudServiceInstance.builder() + .name(SERVICE_INSTANCE) + .metadata(ImmutableCloudMetadata.builder() + .guid( + UUID.randomUUID()) + .build()) + .build()) + .build(); + } + +} diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandlerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandlerTest.java index 15ffab11e9..1ca19a1445 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandlerTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandlerTest.java @@ -1,19 +1,36 @@ package org.cloudfoundry.multiapps.controller.process.util; import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.List; import java.util.stream.Stream; +import org.cloudfoundry.client.v3.Metadata; import org.cloudfoundry.multiapps.controller.api.model.ImmutableOperation; import org.cloudfoundry.multiapps.controller.api.model.Operation; import org.cloudfoundry.multiapps.controller.api.model.Operation.State; import org.cloudfoundry.multiapps.controller.api.model.ProcessType; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientProvider; +import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadataAnnotations; +import org.cloudfoundry.multiapps.controller.core.model.DeployedMta; +import org.cloudfoundry.multiapps.controller.core.model.DeployedMtaApplication; +import org.cloudfoundry.multiapps.controller.core.model.ImmutableDeployedMta; +import org.cloudfoundry.multiapps.controller.core.model.ImmutableDeployedMtaApplication; import org.cloudfoundry.multiapps.controller.persistence.model.FileEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.HistoricOperationEvent; import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableFileEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.query.DescriptorBackupQuery; import org.cloudfoundry.multiapps.controller.persistence.query.impl.OperationQueryImpl; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; +import org.cloudfoundry.multiapps.controller.persistence.services.DescriptorBackupService; import org.cloudfoundry.multiapps.controller.persistence.services.FileService; import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; +import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.OperationService; import org.cloudfoundry.multiapps.controller.process.dynatrace.DynatraceProcessDuration; import org.cloudfoundry.multiapps.controller.process.dynatrace.DynatracePublisher; @@ -21,6 +38,7 @@ import org.cloudfoundry.multiapps.controller.process.security.store.SecretTokenStoreFactory; import org.cloudfoundry.multiapps.controller.process.variables.VariableHandling; import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; import org.flowable.engine.delegate.DelegateExecution; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,9 +53,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class OperationInFinalStateHandlerTest { @@ -50,6 +72,10 @@ class OperationInFinalStateHandlerTest { private static final String USER_GUID = "test-user"; private static final long PROCESS_DURATION = 1000; private static final String DISPOSABLE_USER_PROVIDED_SERVICE_NAME = "__mta-secure-my-mta-fake343"; + private static final String LOGGING_CONFIG_ID = "logging-config-1"; + private static final String USER_NAME = "test-user-name"; + private static final String NAMESPACE = "dev"; + private static final String MTA_VERSION_1 = "1.0.0"; private static final Operation OPERATION = createOperation("1", ProcessType.DEPLOY, "spaceId", "mtaId", "user", true, ZonedDateTime.parse("2010-10-08T10:00:00.000Z[UTC]"), @@ -79,6 +105,20 @@ class OperationInFinalStateHandlerTest { private CloudControllerClientProvider cloudControllerClientProvider; @Mock private CloudControllerClient cloudControllerClient; + @Mock + private OperationLogsExporter operationLogsExporter; + @Mock + private HistoricOperationEventService historicOperationEventService; + @Mock + private DescriptorBackupService descriptorBackupService; + @Mock + private DescriptorBackupQuery descriptorBackupQuery; + @Mock + private CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + @Mock + private CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; + @Mock + private ProcessTypeParser processTypeParser; @InjectMocks private final OperationInFinalStateHandler eventHandler = new OperationInFinalStateHandler(); @@ -105,7 +145,7 @@ public static Stream testHandle() { public void setUp() throws Exception { MockitoAnnotations.openMocks(this) .close(); - Mockito.when(stepLoggerFactory.create(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) + Mockito.when(stepLoggerFactory.create(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(stepLogger); Mockito.when(secretTokenStoreFactory.createSecretTokenStoreDeletionRelated()) .thenReturn(secretTokenStoreDeletion); @@ -167,7 +207,7 @@ private void prepareContext(String archiveIds, String extensionDescriptorIds, bo } private void prepareOperationTimeAggregator() { - Mockito.when(operationTimeAggregator.computeOverallProcessTime(Mockito.eq(PROCESS_ID), Mockito.any())) + Mockito.when(operationTimeAggregator.computeOverallProcessTime(eq(PROCESS_ID), Mockito.any())) .thenReturn(processTime); Mockito.when(processTime.getProcessDuration()) .thenReturn(PROCESS_DURATION); @@ -232,6 +272,232 @@ private void verifyDynatracePublisher() { assertEquals(PROCESS_DURATION, actualDynatraceEvent.getProcessDuration()); } + @Test + void testRemovesClientFromCacheAfterHandle() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + + eventHandler.handle(execution, PROCESS_TYPE, OPERATION_STATE); + + verify(operationLogsExporter).removeClientFromCache(PROCESS_ID); + } + + @Test + void testHistoricOperationEventEmittedWithFinishedEventTypeWhenStateIsFinished() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + + eventHandler.handle(execution, PROCESS_TYPE, State.FINISHED); + + ArgumentCaptor captor = ArgumentCaptor.forClass(HistoricOperationEvent.class); + verify(historicOperationEventService).add(captor.capture()); + assertEquals(HistoricOperationEvent.EventType.FINISHED, captor.getValue() + .getType()); + } + + @Test + void testHistoricOperationEventEmittedWithAbortedEventTypeWhenStateIsAborted() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + + eventHandler.handle(execution, PROCESS_TYPE, State.ABORTED); + + ArgumentCaptor captor = ArgumentCaptor.forClass(HistoricOperationEvent.class); + verify(historicOperationEventService).add(captor.capture()); + assertEquals(HistoricOperationEvent.EventType.ABORTED, captor.getValue() + .getType()); + } + + @Test + void testSetOperationStateSkippedWhenOperationAlreadyFinal() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + OperationQueryImpl operationQuery = Mockito.mock(OperationQueryImpl.class); + when(operationService.createQuery()).thenReturn(operationQuery); + when(operationQuery.processId(any())).thenReturn(operationQuery); + Operation alreadyFinalOperation = ImmutableOperation.builder() + .from(OPERATION) + .state(State.FINISHED) + .hasAcquiredLock(false) + .endedAt(ZonedDateTime.parse("2010-10-15T10:00:00.000Z[UTC]")) + .build(); + when(operationQuery.singleResult()).thenReturn(alreadyFinalOperation); + + eventHandler.handle(execution, PROCESS_TYPE, State.FINISHED); + + verify(operationService, never()).update(any(), any()); + verify(historicOperationEventService, never()).add(any()); + } + + @Test + void testDeleteCloudLoggingServiceConfiguration_skippedWhenLoggingConfigurationIsNull() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + + eventHandler.handle(execution, PROCESS_TYPE, OPERATION_STATE); + + verify(cloudLoggingServiceConfigurationService, never()).deleteCloudLoggingServiceConfiguration(anyString()); + verify(cloudLoggingServiceConfigurationAuditLog, never()).logDeleteLoggingConfiguration(anyString(), anyString(), any()); + } + + @Test + void testDeleteCloudLoggingServiceConfiguration_skippedWhenLoggingConfigurationIdIsNull() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + LoggingConfiguration loggingConfiguration = ImmutableLoggingConfiguration.builder() + .build(); + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, loggingConfiguration); + + eventHandler.handle(execution, PROCESS_TYPE, OPERATION_STATE); + + verify(cloudLoggingServiceConfigurationService, never()).deleteCloudLoggingServiceConfiguration(anyString()); + verify(cloudLoggingServiceConfigurationAuditLog, never()).logDeleteLoggingConfiguration(anyString(), anyString(), any()); + } + + @Test + void testDeleteCloudLoggingServiceConfiguration_skippedWhenProcessTypeIsNotUndeploy() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + LoggingConfiguration loggingConfiguration = ImmutableLoggingConfiguration.builder() + .id(LOGGING_CONFIG_ID) + .build(); + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, loggingConfiguration); + when(processTypeParser.getProcessType(execution)).thenReturn(ProcessType.DEPLOY); + + eventHandler.handle(execution, PROCESS_TYPE, OPERATION_STATE); + + verify(cloudLoggingServiceConfigurationService, never()).deleteCloudLoggingServiceConfiguration(anyString()); + verify(cloudLoggingServiceConfigurationAuditLog, never()).logDeleteLoggingConfiguration(anyString(), anyString(), any()); + } + + @Test + void testDeleteCloudLoggingServiceConfiguration_deletesAndAuditsWhenProcessTypeIsUndeploy() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + LoggingConfiguration loggingConfiguration = ImmutableLoggingConfiguration.builder() + .id(LOGGING_CONFIG_ID) + .build(); + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, loggingConfiguration); + VariableHandling.set(execution, Variables.USER, USER_NAME); + when(processTypeParser.getProcessType(execution)).thenReturn(ProcessType.UNDEPLOY); + + eventHandler.handle(execution, PROCESS_TYPE, OPERATION_STATE); + + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration(LOGGING_CONFIG_ID); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration(USER_NAME, SPACE_ID, loggingConfiguration); + } + + @Test + void testDeletePreviousBackupDescriptors_skippedWhenStateIsNotFinished() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + + eventHandler.handle(execution, PROCESS_TYPE, State.ABORTED); + + verify(descriptorBackupService, never()).createQuery(); + } + + @Test + void testDeletePreviousBackupDescriptors_undeployWithDeployedMtaVersion_deletesByVersion() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + prepareDescriptorBackupQuery(); + VariableHandling.set(execution, Variables.MTA_NAMESPACE, NAMESPACE); + DeployedMta deployedMta = buildDeployedMta(MTA_VERSION_1); + VariableHandling.set(execution, Variables.DEPLOYED_MTA, deployedMta); + + eventHandler.handle(execution, ProcessType.UNDEPLOY, State.FINISHED); + + verify(descriptorBackupQuery).mtaId(MTA_ID); + verify(descriptorBackupQuery).spaceId(SPACE_ID); + verify(descriptorBackupQuery).namespace(NAMESPACE); + verify(descriptorBackupQuery).mtaVersion(MTA_VERSION_1); + verify(descriptorBackupQuery).delete(); + } + + @Test + void testDeletePreviousBackupDescriptors_undeployWithoutDeployedMta_doesNotDelete() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + VariableHandling.set(execution, Variables.MTA_NAMESPACE, NAMESPACE); + + eventHandler.handle(execution, ProcessType.UNDEPLOY, State.FINISHED); + + verify(descriptorBackupService, never()).createQuery(); + } + + @Test + void testDeletePreviousBackupDescriptors_deployWithCurrentDescriptorVersion_skipsCurrentVersion() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + prepareDescriptorBackupQuery(); + VariableHandling.set(execution, Variables.MTA_NAMESPACE, NAMESPACE); + DeploymentDescriptor descriptor = DeploymentDescriptor.createV3() + .setVersion(MTA_VERSION_1); + VariableHandling.set(execution, Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR, descriptor); + VariableHandling.set(execution, Variables.APPS_TO_UNDEPLOY, Collections.emptyList()); + + eventHandler.handle(execution, ProcessType.DEPLOY, State.FINISHED); + + ArgumentCaptor> versionsCaptor = ArgumentCaptor.forClass(List.class); + verify(descriptorBackupQuery).mtaVersionsNotMatch(versionsCaptor.capture()); + assertEquals(List.of(MTA_VERSION_1), versionsCaptor.getValue()); + verify(descriptorBackupQuery).delete(); + } + + @Test + void testDeletePreviousBackupDescriptors_deployWithNoVersionsToSkip_doesNotDelete() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + VariableHandling.set(execution, Variables.MTA_NAMESPACE, NAMESPACE); + DeploymentDescriptor descriptor = DeploymentDescriptor.createV3(); + VariableHandling.set(execution, Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR, descriptor); + VariableHandling.set(execution, Variables.APPS_TO_UNDEPLOY, Collections.emptyList()); + + eventHandler.handle(execution, ProcessType.DEPLOY, State.FINISHED); + + verify(descriptorBackupService, never()).createQuery(); + } + + private void prepareDescriptorBackupQuery() { + when(descriptorBackupService.createQuery()).thenReturn(descriptorBackupQuery); + when(descriptorBackupQuery.mtaId(anyString())).thenReturn(descriptorBackupQuery); + when(descriptorBackupQuery.spaceId(anyString())).thenReturn(descriptorBackupQuery); + when(descriptorBackupQuery.namespace(any())).thenReturn(descriptorBackupQuery); + when(descriptorBackupQuery.mtaVersion(anyString())).thenReturn(descriptorBackupQuery); + when(descriptorBackupQuery.mtaVersionsNotMatch(any())).thenReturn(descriptorBackupQuery); + } + + private DeployedMta buildDeployedMta(String mtaVersion) { + DeployedMtaApplication application = ImmutableDeployedMtaApplication.builder() + .name("app-1") + .moduleName("module-1") + .v3Metadata(Metadata.builder() + .annotation( + MtaMetadataAnnotations.MTA_VERSION, + mtaVersion) + .build()) + .build(); + return ImmutableDeployedMta.builder() + .metadata(org.cloudfoundry.multiapps.controller.core.cf.metadata.ImmutableMtaMetadata.builder() + .id(MTA_ID) + .build()) + .applications(List.of(application)) + .build(); + } + private static Operation createOperation(String processId, ProcessType type, String spaceId, String mtaId, String user, boolean acquiredLock, ZonedDateTime startedAt, ZonedDateTime endedAt, Operation.State state) { return ImmutableOperation.builder() diff --git a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/configuration/AsyncProcessLoggerConfiguration.java b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/configuration/AsyncProcessLoggerConfiguration.java index ea8963e9a9..4f09c6b6f5 100644 --- a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/configuration/AsyncProcessLoggerConfiguration.java +++ b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/configuration/AsyncProcessLoggerConfiguration.java @@ -1,5 +1,7 @@ package org.cloudfoundry.multiapps.controller.web.configuration; +import java.util.concurrent.Executor; + import jakarta.inject.Inject; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.springframework.context.annotation.Bean; @@ -7,12 +9,10 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import java.util.concurrent.Executor; - @Configuration @EnableAsync public class AsyncProcessLoggerConfiguration { - + @Inject private ApplicationConfiguration configuration; @@ -26,4 +26,13 @@ public Executor getAsyncExecutor() { return executor; } + @Bean("cloudLoggingServiceAsyncExecutor") + public Executor getCloudLoggingServiceAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(configuration.getCloudLoggingServiceExecutorCoreThreads()); + executor.setMaxPoolSize(configuration.getCloudLoggingServiceExecutorMaxThreads()); + executor.setQueueCapacity(configuration.getCloudLoggingServiceExecutorQueueCapacity()); + executor.initialize(); + return executor; + } } diff --git a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/resources/ConfigurationEntriesResource.java b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/resources/ConfigurationEntriesResource.java index 7a2a36dddd..afb18ed341 100644 --- a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/resources/ConfigurationEntriesResource.java +++ b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/resources/ConfigurationEntriesResource.java @@ -3,6 +3,7 @@ import jakarta.inject.Inject; import jakarta.inject.Named; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; import org.cloudfoundry.multiapps.controller.core.auditlogging.MtaConfigurationPurgerAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientProvider; @@ -10,6 +11,7 @@ import org.cloudfoundry.multiapps.controller.core.helpers.MtaConfigurationPurger; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; import org.cloudfoundry.multiapps.controller.core.util.UserInfo; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationEntryService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationSubscriptionService; import org.cloudfoundry.multiapps.controller.web.Constants; @@ -44,6 +46,10 @@ public class ConfigurationEntriesResource { private TokenService tokenService; @Inject private MtaConfigurationPurgerAuditLog mtaConfigurationPurgerAuditLog; + @Inject + private CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + @Inject + private CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; @PostMapping(value = Constants.Endpoints.PURGE) public ResponseEntity purgeConfigurationRegistry(@RequestParam(REQUEST_PARAM_ORGANIZATION) String organization, @@ -58,8 +64,10 @@ public ResponseEntity purgeConfigurationRegistry(@RequestParam(REQUEST_PAR .toString()); MtaConfigurationPurger configurationPurger = new MtaConfigurationPurger(client, spaceClient, configurationEntryService, configurationSubscriptionService, mtaMetadataParser, - mtaConfigurationPurgerAuditLog); - configurationPurger.purge(organization, space); + mtaConfigurationPurgerAuditLog, + cloudLoggingServiceConfigurationService, + cloudLoggingServiceConfigurationAuditLog); + configurationPurger.purge(organization, space, user.getName()); return ResponseEntity.status(HttpStatus.NO_CONTENT) .build(); } diff --git a/pom.xml b/pom.xml index 7376507892..b197596f08 100644 --- a/pom.xml +++ b/pom.xml @@ -863,6 +863,16 @@ reactor-netty ${reactor-netty.version} + + io.projectreactor.netty + reactor-netty-http + ${reactor-netty.version} + + + io.projectreactor.netty + reactor-netty-core + ${reactor-netty.version} + org.cloudfoundry.multiapps