From 6123daf3f02d816b470d854a451cbd1ca4f21b30 Mon Sep 17 00:00:00 2001 From: IvanBorislavovDimitrov Date: Mon, 24 Nov 2025 17:24:12 +0200 Subject: [PATCH] Add binding id during debug --- .../controller/process/Messages.java | 10 +- ...indingUnbindingOperationBaseExecution.java | 14 +- ...erviceUnbindingOperationExecutionTest.java | 255 +++++++++++++++++- 3 files changed, 259 insertions(+), 20 deletions(-) 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 de999943a7..14714311fe 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 @@ -183,12 +183,12 @@ public class Messages { public static final String ERROR_WHILE_POLLING_SERVICE_BINDING_OPERATIONS_BETWEEN_APP_0_AND_SERVICE_INSTANCE_1 = "Error while polling service binding operations between app: \"{0}\" and service instance \"{1}\""; public static final String ERROR_WHILE_CHECKING_SERVICE_BINDING_OPERATIONS_BETWEEN_APP_0_AND_SERVICE_INSTANCE_1 = "Error while checking service binding operations between app: \"{0}\" and service instance \"{1}\""; public static final String ERROR_WHILE_CHECKING_SERVICE_BINDING_OPERATIONS_0 = "Error while checking service binding operations for service binding: \"{0}\""; - public static final String ASYNC_OPERATION_FOR_SERVICE_BINDING_FAILED_WITH = "Async operation for service binding between app \"{0}\" and service instance \"{1}\" with offering \"{2}\" and plan \"{3}\" failed with \"{4}\""; - public static final String ASYNC_OPERATION_FOR_USER_PROVIDED_SERVICE_BINDING_FAILED_WITH = "Async operation for service binding between app \"{0}\" and user-provided service instance \"{1}\" failed with \"{2}\""; - public static final String ASYNC_OPERATION_FOR_SERVICE_BINDING_FAILED_INSTANCE_MISSING = "Async operation for service binding between app \"{0}\" and service instance \"{1}\" failed: Instance not found. Cause: {2}"; + public static final String ASYNC_OPERATION_FOR_SERVICE_BINDING_FAILED_WITH = "Async operation for service binding \"{0}\" between app \"{1}\" and service instance \"{2}\" with offering \"{3}\" and plan \"{4}\" failed with \"{5}\""; + public static final String ASYNC_OPERATION_FOR_USER_PROVIDED_SERVICE_BINDING_FAILED_WITH = "Async operation for service binding \"{0}\" between app \"{1}\" and user-provided service instance \"{2}\" failed with \"{3}\""; + public static final String ASYNC_OPERATION_FOR_SERVICE_BINDING_FAILED_INSTANCE_MISSING = "Async operation for service binding \"{0}\" between app \"{1}\" and service instance \"{2}\" failed: Instance not found. Cause: {3}"; public static final String ASYNC_OPERATION_FOR_SERVICE_KEY_FAILED_WITH = "Async operation for service key of service instance \"{0}\" failed with \"{1}\""; public static final String ASYNC_OPERATION_FOR_OPTIONAL_SERVICE_KEY_FAILED_WITH = "Async operation for service key of optional service instance \"{0}\" failed with \"{1}\""; - public static final String ASYNC_OPERATION_FOR_SERVICE_BINDING_FOR_OPTIONAL_SERVICE_FAILED_WITH = "Async operation for service binding for optional service between app \"{0}\" and service instance \"{1}\" failed with \"{2}\""; + public static final String ASYNC_OPERATION_FOR_SERVICE_BINDING_FOR_OPTIONAL_SERVICE_FAILED_WITH = "Async operation for service binding \"{0}\" for optional service between app \"{1}\" and service instance \"{2}\" failed with \"{3}\""; public static final String ERROR_WHILE_CALCULATING_SERVICE_BINDINGS_TO_DELETE_0 = "Error while calculating service bindings to delete \"{0}\""; public static final String ERROR_WHILE_CREATING_SERVICE_KEY_0 = "Error while creating service key \"{0}\""; public static final String ERROR_WHILE_DETERMINING_SERVICE_BINDINGS_TO_DELETE = "Error while determining service bindings to delete"; @@ -694,7 +694,7 @@ public class Messages { public static final String CHECK_SHOULD_REBIND_APPLICATION_SERVICE_INSTANCE = "Check should rebind application \"{0}\" and service instance \"{1}\""; public static final String POLLING_ASYNC_OPERATION_SERVICE_BROKER = "Polling async operation of service broker \"{0}\""; public static final String ASYNC_OPERATION_SERVICE_BROKER_IN_STATE_WITH_WARNINGS = "Async operation of service broker \"{0}\" is in state \"{1}\" warnings \"{2}\""; - public static final String ASYNC_OPERATION_SERVICE_BINDING_IN_STATE_WITH_WARNINGS = "Async operation of service binding is in state \"{0}\" warnings \"{1}\""; + public static final String ASYNC_OPERATION_SERVICE_BINDING_IN_STATE_WITH_WARNINGS = "Async operation of service binding \"{0}\" is in state \"{1}\" warnings \"{2}\""; public static final String ASYNC_OPERATION_SERVICE_KEY_IN_STATE_WITH_WARNINGS = "Async operation of service key is in state \"{0}\" warnings \"{1}\""; public static final String SETTING_WAIT_AFTER_STOP_FOR_APP_0_TO_1_SECONDS = "Setting wait after stop for \"{0}\" to \"{1}\" seconds"; public static final String DETECTING_SERVICE_KEYS_FOR_DELETION = "Detecting service keys to delete."; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBindingUnbindingOperationBaseExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBindingUnbindingOperationBaseExecution.java index cc7110f74f..8652c0f50a 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBindingUnbindingOperationBaseExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBindingUnbindingOperationBaseExecution.java @@ -4,7 +4,6 @@ import java.util.List; import java.util.Optional; import java.util.function.Consumer; - 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.CloudAsyncJob; @@ -28,6 +27,7 @@ protected boolean isOptional(ProcessContext context) { protected Consumer getInProgressHandler(ProcessContext context) { return serviceBindingJob -> context.getStepLogger() .debug(Messages.ASYNC_OPERATION_SERVICE_BINDING_IN_STATE_WITH_WARNINGS, + serviceBindingJob.getGuid(), serviceBindingJob.getState(), serviceBindingJob.getWarnings()); } @@ -60,19 +60,22 @@ private String buildErrorMessage(CloudApplication app, CloudServiceInstance serv CloudAsyncJob serviceBindingJob) { if (serviceInstance == null) { - return MessageFormat.format(Messages.ASYNC_OPERATION_FOR_SERVICE_BINDING_FAILED_INSTANCE_MISSING, app.getName(), + return MessageFormat.format(Messages.ASYNC_OPERATION_FOR_SERVICE_BINDING_FAILED_INSTANCE_MISSING, serviceBindingJob.getGuid(), + app.getName(), serviceInstanceName, serviceBindingJob.getErrors()); } if (serviceInstance.isUserProvided()) { - return MessageFormat.format(Messages.ASYNC_OPERATION_FOR_USER_PROVIDED_SERVICE_BINDING_FAILED_WITH, app.getName(), + return MessageFormat.format(Messages.ASYNC_OPERATION_FOR_USER_PROVIDED_SERVICE_BINDING_FAILED_WITH, serviceBindingJob.getGuid(), + app.getName(), serviceInstanceName, serviceBindingJob.getErrors()); } String serviceOffering = getValueOrMissing(serviceInstance.getLabel()); String servicePlan = getValueOrMissing(serviceInstance.getPlan()); - return MessageFormat.format(Messages.ASYNC_OPERATION_FOR_SERVICE_BINDING_FAILED_WITH, app.getName(), serviceInstanceName, + return MessageFormat.format(Messages.ASYNC_OPERATION_FOR_SERVICE_BINDING_FAILED_WITH, serviceBindingJob.getGuid(), app.getName(), + serviceInstanceName, serviceOffering, servicePlan, serviceBindingJob.getErrors()); } @@ -88,7 +91,8 @@ protected Consumer getOnErrorHandlerForOptionalResource(ProcessCo String serviceInstanceName = context.getVariable(Variables.SERVICE_TO_UNBIND_BIND); return serviceBindingJob -> context.getStepLogger() .warnWithoutProgressMessage( - Messages.ASYNC_OPERATION_FOR_SERVICE_BINDING_FOR_OPTIONAL_SERVICE_FAILED_WITH, app.getName(), + Messages.ASYNC_OPERATION_FOR_SERVICE_BINDING_FOR_OPTIONAL_SERVICE_FAILED_WITH, + serviceBindingJob.getGuid(), app.getName(), serviceInstanceName, serviceBindingJob.getErrors()); } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceUnbindingOperationExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceUnbindingOperationExecutionTest.java index f80ba24287..72621bd1b6 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceUnbindingOperationExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceUnbindingOperationExecutionTest.java @@ -2,40 +2,275 @@ import java.util.List; import java.util.UUID; - import org.cloudfoundry.client.v3.jobs.JobState; +import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudAsyncJob; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceInstance; +import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; +import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudApplicationExtended; +import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudServiceInstanceExtended; +import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.junit.jupiter.api.Test; - +import org.mockito.ArgumentCaptor; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +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 PollServiceUnbindingOperationExecutionTest extends AsyncStepOperationTest { - private static final String JOB_ID = "123"; - private static final UUID SERVICE_BINDING_GUID = UUID.randomUUID(); + private static final UUID JOB_GUID = UUID.fromString("00000000-0000-0000-0000-000000000123"); + private static final String JOB_ID = JOB_GUID.toString(); + private static final String APP_NAME = "test-app"; + private static final String SERVICE_NAME = "test-service"; + private static final String SERVICE_OFFERING = "test-offering"; + private static final String SERVICE_PLAN = "test-plan"; + private static final String ERROR_MESSAGE = "Test error"; private AsyncExecutionState expectedAsyncExecutionState; @Test - void testExecution() { - context.setVariable(Variables.SERVICE_UNBINDING_JOB_ID, JOB_ID); - CloudAsyncJob asyncJob = buildAsyncJob(); + void testGetInProgressHandler() { + prepareContext(); + CloudAsyncJob asyncJob = buildAsyncJobWithState(JobState.PROCESSING); + when(client.getAsyncJob(JOB_ID)).thenReturn(asyncJob); + expectedAsyncExecutionState = AsyncExecutionState.RUNNING; + + testExecuteOperations(); + + verify(stepLogger).debug(eq(Messages.ASYNC_OPERATION_SERVICE_BINDING_IN_STATE_WITH_WARNINGS), + eq(JOB_GUID), eq(JobState.PROCESSING), any()); + } + + @Test + void testGetOnCompleteHandler() { + prepareContext(); + CloudAsyncJob asyncJob = buildAsyncJobWithState(JobState.COMPLETE); when(client.getAsyncJob(JOB_ID)).thenReturn(asyncJob); expectedAsyncExecutionState = AsyncExecutionState.FINISHED; + + testExecuteOperations(); + + verify(stepLogger).debug(Messages.ASYNC_OPERATION_FOR_SERVICE_BINDING_FINISHED, JOB_GUID); + } + + @Test + void testGetOnErrorHandlerWithManagedServiceInstance() { + prepareContext(); + CloudServiceInstance serviceInstance = buildManagedServiceInstance(); + when(client.getServiceInstance(SERVICE_NAME, false)).thenReturn(serviceInstance); + + CloudAsyncJob asyncJob = buildAsyncJobWithError(); + when(client.getAsyncJob(JOB_ID)).thenReturn(asyncJob); + expectedAsyncExecutionState = AsyncExecutionState.ERROR; + + testExecuteOperations(); + + ArgumentCaptor errorMessageCaptor = ArgumentCaptor.forClass(String.class); + verify(stepLogger).error(errorMessageCaptor.capture()); + String errorMessage = errorMessageCaptor.getValue(); + assertTrue(errorMessage.contains(JOB_ID)); + assertTrue(errorMessage.contains(APP_NAME)); + assertTrue(errorMessage.contains(SERVICE_NAME)); + assertTrue(errorMessage.contains(SERVICE_OFFERING)); + assertTrue(errorMessage.contains(SERVICE_PLAN)); + assertTrue(errorMessage.contains(ERROR_MESSAGE)); + } + + @Test + void testGetOnErrorHandlerWithUserProvidedServiceInstance() { + prepareContext(); + CloudServiceInstance serviceInstance = buildUserProvidedServiceInstance(); + when(client.getServiceInstance(SERVICE_NAME, false)).thenReturn(serviceInstance); + + CloudAsyncJob asyncJob = buildAsyncJobWithError(); + when(client.getAsyncJob(JOB_ID)).thenReturn(asyncJob); + expectedAsyncExecutionState = AsyncExecutionState.ERROR; + + testExecuteOperations(); + + ArgumentCaptor errorMessageCaptor = ArgumentCaptor.forClass(String.class); + verify(stepLogger).error(errorMessageCaptor.capture()); + String errorMessage = errorMessageCaptor.getValue(); + assertTrue(errorMessage.contains(JOB_ID)); + assertTrue(errorMessage.contains(APP_NAME)); + assertTrue(errorMessage.contains(SERVICE_NAME)); + assertTrue(errorMessage.contains(ERROR_MESSAGE)); + assertTrue(!errorMessage.contains(SERVICE_OFFERING) || !errorMessage.contains(SERVICE_PLAN)); + } + + @Test + void testGetOnErrorHandlerWithMissingServiceInstance() { + prepareContext(); + when(client.getServiceInstance(SERVICE_NAME, false)).thenReturn(null); + + CloudAsyncJob asyncJob = buildAsyncJobWithError(); + when(client.getAsyncJob(JOB_ID)).thenReturn(asyncJob); + expectedAsyncExecutionState = AsyncExecutionState.ERROR; + testExecuteOperations(); + + ArgumentCaptor errorMessageCaptor = ArgumentCaptor.forClass(String.class); + verify(stepLogger).error(errorMessageCaptor.capture()); + String errorMessage = errorMessageCaptor.getValue(); + assertTrue(errorMessage.contains(JOB_ID)); + assertTrue(errorMessage.contains(APP_NAME)); + assertTrue(errorMessage.contains(SERVICE_NAME)); + assertTrue(errorMessage.contains(ERROR_MESSAGE)); + assertTrue(errorMessage.contains("Instance not found")); } - private CloudAsyncJob buildAsyncJob() { + @Test + void testGetOnErrorHandlerWithMissingServiceOfferingAndPlan() { + prepareContext(); + CloudServiceInstance serviceInstance = buildManagedServiceInstanceWithNullFields(); + when(client.getServiceInstance(SERVICE_NAME, false)).thenReturn(serviceInstance); + + CloudAsyncJob asyncJob = buildAsyncJobWithError(); + when(client.getAsyncJob(JOB_ID)).thenReturn(asyncJob); + expectedAsyncExecutionState = AsyncExecutionState.ERROR; + + testExecuteOperations(); + + ArgumentCaptor errorMessageCaptor = ArgumentCaptor.forClass(String.class); + verify(stepLogger).error(errorMessageCaptor.capture()); + String errorMessage = errorMessageCaptor.getValue(); + assertTrue(errorMessage.contains("missing")); + } + + @Test + void testGetOnErrorHandlerForOptionalResource() { + prepareContextWithOptionalService(); + CloudServiceInstance serviceInstance = buildManagedServiceInstance(); + when(client.getServiceInstance(SERVICE_NAME, false)).thenReturn(serviceInstance); + + CloudAsyncJob asyncJob = buildAsyncJobWithError(); + when(client.getAsyncJob(JOB_ID)).thenReturn(asyncJob); + expectedAsyncExecutionState = AsyncExecutionState.FINISHED; + + testExecuteOperations(); + + verify(stepLogger).warnWithoutProgressMessage(eq(Messages.ASYNC_OPERATION_FOR_SERVICE_BINDING_FOR_OPTIONAL_SERVICE_FAILED_WITH), + eq(JOB_GUID), eq(APP_NAME), eq(SERVICE_NAME), eq(ERROR_MESSAGE)); + verify(stepLogger, never()).error(anyString()); + } + + @Test + void testGetOnErrorHandlerWithServiceToDelete() { + prepareContextWithServiceToDelete(); + CloudServiceInstance serviceInstance = buildManagedServiceInstance(); + when(client.getServiceInstance(SERVICE_NAME, false)).thenReturn(serviceInstance); + + CloudAsyncJob asyncJob = buildAsyncJobWithError(); + when(client.getAsyncJob(JOB_ID)).thenReturn(asyncJob); + expectedAsyncExecutionState = AsyncExecutionState.ERROR; + + testExecuteOperations(); + + ArgumentCaptor errorMessageCaptor = ArgumentCaptor.forClass(String.class); + verify(stepLogger).error(errorMessageCaptor.capture()); + String errorMessage = errorMessageCaptor.getValue(); + assertTrue(errorMessage.contains(SERVICE_NAME)); + } + + private void prepareContext() { + context.setVariable(Variables.SERVICE_UNBINDING_JOB_ID, JOB_ID); + context.setVariable(Variables.APP_TO_PROCESS, ImmutableCloudApplicationExtended.builder() + .name(APP_NAME) + .metadata(ImmutableCloudMetadata.builder() + .guid( + UUID.randomUUID()) + .build()) + .build()); + context.setVariable(Variables.SERVICE_TO_UNBIND_BIND, SERVICE_NAME); + context.setVariable(Variables.SERVICES_TO_BIND, List.of()); + } + + private void prepareContextWithOptionalService() { + prepareContext(); + CloudServiceInstanceExtended optionalService = ImmutableCloudServiceInstanceExtended.builder() + .name(SERVICE_NAME) + .isOptional(true) + .metadata(ImmutableCloudMetadata.builder() + .guid( + UUID.randomUUID()) + .build()) + .build(); + context.setVariable(Variables.SERVICES_TO_BIND, List.of(optionalService)); + } + + private void prepareContextWithServiceToDelete() { + context.setVariable(Variables.SERVICE_UNBINDING_JOB_ID, JOB_ID); + context.setVariable(Variables.APP_TO_PROCESS, ImmutableCloudApplicationExtended.builder() + .name(APP_NAME) + .metadata(ImmutableCloudMetadata.builder() + .guid( + UUID.randomUUID()) + .build()) + .build()); + context.setVariable(Variables.SERVICE_TO_DELETE, SERVICE_NAME); + context.setVariable(Variables.SERVICES_TO_BIND, List.of()); + } + + private CloudAsyncJob buildAsyncJobWithState(JobState state) { return ImmutableCloudAsyncJob.builder() - .state(JobState.COMPLETE) - .metadata(ImmutableCloudMetadata.of(SERVICE_BINDING_GUID)) + .state(state) + .metadata(ImmutableCloudMetadata.builder() + .guid(JOB_GUID) + .build()) .build(); } + private CloudAsyncJob buildAsyncJobWithError() { + return ImmutableCloudAsyncJob.builder() + .state(JobState.FAILED) + .metadata(ImmutableCloudMetadata.builder() + .guid(JOB_GUID) + .build()) + .errors(ERROR_MESSAGE) + .build(); + } + + private CloudServiceInstance buildManagedServiceInstance() { + return ImmutableCloudServiceInstance.builder() + .name(SERVICE_NAME) + .label(SERVICE_OFFERING) + .plan(SERVICE_PLAN) + .type(ServiceInstanceType.MANAGED) + .metadata(ImmutableCloudMetadata.builder() + .guid(UUID.randomUUID()) + .build()) + .build(); + } + + private CloudServiceInstance buildUserProvidedServiceInstance() { + return ImmutableCloudServiceInstance.builder() + .name(SERVICE_NAME) + .type(ServiceInstanceType.USER_PROVIDED) + .metadata(ImmutableCloudMetadata.builder() + .guid(UUID.randomUUID()) + .build()) + .build(); + } + + private CloudServiceInstance buildManagedServiceInstanceWithNullFields() { + return ImmutableCloudServiceInstance.builder() + .name(SERVICE_NAME) + .type(ServiceInstanceType.MANAGED) + .metadata(ImmutableCloudMetadata.builder() + .guid(UUID.randomUUID()) + .build()) + .build(); + } + @Override protected List getAsyncOperations(ProcessContext wrapper) { return List.of(new PollServiceUnbindingOperationExecution());