From e957b226e9600b0b7a42f20943d7d749ff30509f Mon Sep 17 00:00:00 2001 From: Krasimir Kargov Date: Wed, 5 Aug 2026 15:12:57 +0300 Subject: [PATCH 1/6] CF Java Client PoC migration LMCROSSITXSADEPLOY-3563 --- .../ResilientCloudControllerClient.java | 2 +- .../client/facade/CloudControllerClient.java | 2 +- .../facade/CloudControllerClientImpl.java | 2 +- .../facade/adapters/RawCloudApplication.java | 2 +- .../facade/adapters/RawCloudAsyncJob.java | 3 +- .../facade/adapters/RawCloudEntity.java | 31 + .../adapters/RawCloudServiceBinding.java | 2 +- .../adapters/RawCloudServiceInstance.java | 7 +- .../facade/adapters/RawCloudServiceKey.java | 4 +- .../facade/adapters/RawInstancesInfo.java | 3 +- .../client/facade/adapters/RawUserRole.java | 3 +- .../adapters/RawV3CloudServiceInstance.java | 2 +- .../client/facade/domain/CloudAsyncJob.java | 1 - .../client/facade/domain/CloudEntity.java | 2 +- .../facade/domain/CloudServiceInstance.java | 1 - .../client/facade/domain/HealthCheckType.java | 10 +- .../client/facade/domain/InstanceState.java | 10 +- .../client/facade/domain/JobState.java | 36 + .../client/facade/domain/LastOperation.java | 9 + .../client/facade/domain/Metadata.java | 122 +++ .../ServiceCredentialBindingOperation.java | 11 +- .../facade/domain/ServiceInstanceType.java | 40 + .../facade/domain/ServiceOperation.java | 10 +- .../client/facade/domain/UserRole.java | 12 +- .../facade/dto/ApplicationToCreateDto.java | 2 +- .../facade/rest/BuildsV3Operations.java | 64 ++ .../rest/CloudControllerRestClient.java | 2 +- .../CloudControllerRestClientBuilder.java | 128 +++ .../CloudControllerRestClientFactory.java | 96 ++ .../rest/CloudControllerRestClientImpl.java | 34 +- .../rest/CloudControllerRestClientV3Impl.java | 831 ++++++++++++++++++ .../facade/rest/CloudControllerV3Client.java | 191 ++++ .../facade/rest/DomainsV3Operations.java | 184 ++++ .../facade/rest/EventsV3Operations.java | 83 ++ .../client/facade/rest/JobsV3Operations.java | 31 + .../facade/rest/PackagesV3Operations.java | 212 +++++ .../facade/rest/ProcessesV3Operations.java | 237 +++++ .../client/facade/rest/RolesV3Operations.java | 46 + .../facade/rest/RoutesV3Operations.java | 362 ++++++++ .../rest/ServiceBindingsV3Operations.java | 269 ++++++ .../rest/ServiceBrokersV3Operations.java | 228 +++++ .../rest/ServiceInstancesV3Operations.java | 442 ++++++++++ .../facade/rest/ServiceKeysV3Operations.java | 188 ++++ .../rest/ServiceOfferingsV3Operations.java | 82 ++ .../facade/rest/ServicePlansV3Operations.java | 122 +++ .../facade/rest/StacksV3Operations.java | 63 ++ .../client/facade/rest/TasksV3Operations.java | 112 +++ .../facade/rest/resources/V3Application.java | 53 ++ .../rest/resources/V3ApplicationMapper.java | 59 ++ .../facade/rest/resources/V3AuditEvent.java | 30 + .../rest/resources/V3AuditEventMapper.java | 40 + .../client/facade/rest/resources/V3Build.java | 35 + .../facade/rest/resources/V3BuildMapper.java | 60 ++ .../facade/rest/resources/V3Domain.java | 45 + .../facade/rest/resources/V3DomainMapper.java | 22 + .../facade/rest/resources/V3Droplet.java | 24 + .../rest/resources/V3InstancesInfoMapper.java | 49 ++ .../client/facade/rest/resources/V3Job.java | 45 + .../facade/rest/resources/V3JobMapper.java | 57 ++ .../facade/rest/resources/V3ListResponse.java | 49 ++ .../facade/rest/resources/V3Metadata.java | 24 + .../facade/rest/resources/V3Package.java | 36 + .../rest/resources/V3PackageMapper.java | 76 ++ .../facade/rest/resources/V3Process.java | 71 ++ .../rest/resources/V3ProcessMapper.java | 60 ++ .../rest/resources/V3ResourceMappers.java | 73 ++ .../client/facade/rest/resources/V3Role.java | 22 + .../facade/rest/resources/V3RoleMapper.java | 18 + .../client/facade/rest/resources/V3Route.java | 57 ++ .../facade/rest/resources/V3RouteMapper.java | 104 +++ .../rest/resources/V3ServiceBinding.java | 46 + .../resources/V3ServiceBindingMapper.java | 71 ++ .../rest/resources/V3ServiceBroker.java | 35 + .../rest/resources/V3ServiceBrokerMapper.java | 40 + .../rest/resources/V3ServiceInstance.java | 47 + .../resources/V3ServiceInstanceMapper.java | 88 ++ .../rest/resources/V3ServiceKeyDetails.java | 28 + .../rest/resources/V3ServiceKeyMapper.java | 59 ++ .../rest/resources/V3ServiceOffering.java | 54 ++ .../resources/V3ServiceOfferingMapper.java | 64 ++ .../facade/rest/resources/V3ServicePlan.java | 43 + .../rest/resources/V3ServicePlanMapper.java | 55 ++ .../client/facade/rest/resources/V3Stack.java | 20 + .../facade/rest/resources/V3StackMapper.java | 23 + .../client/facade/rest/resources/V3Task.java | 28 + .../facade/rest/resources/V3TaskMapper.java | 45 + ...sCloudControllerClientIntegrationTest.java | 6 +- ...sCloudControllerClientIntegrationTest.java | 4 +- .../adapters/RawCloudApplicationTest.java | 2 +- .../facade/adapters/RawCloudAsyncJobTest.java | 2 +- .../facade/adapters/RawCloudEntityTest.java | 7 + .../adapters/RawCloudServiceBindingTest.java | 5 +- .../adapters/RawCloudServiceInstanceTest.java | 6 +- .../adapters/RawCloudServiceKeyTest.java | 7 +- .../facade/adapters/RawCloudServiceTest.java | 4 +- .../cf/clients/CloudEntityResourceMapper.java | 4 +- .../cf/clients/CustomServiceKeysClient.java | 2 +- .../metadata/processor/MtaMetadataParser.java | 2 +- .../cf/metadata/util/MtaMetadataUtil.java | 2 +- .../cf/v2/ApplicationMetadataBuilder.java | 2 +- .../core/cf/v2/MtaMetadataBuilder.java | 4 +- .../core/cf/v2/ServiceKeyMetadataBuilder.java | 4 +- .../cf/v2/ServiceKeysCloudModelBuilder.java | 2 +- .../core/cf/v2/ServiceMetadataBuilder.java | 4 +- .../core/cf/v2/ServicesCloudModelBuilder.java | 2 +- .../clients/CustomServiceKeysClientTest.java | 2 +- .../cf/detect/DeployedMtaDetectorTest.java | 2 +- .../client/LoggingCloudControllerClient.java | 2 +- .../BuildApplicationDeployModelStep.java | 2 +- .../steps/ClearMtaMetadataBaseStep.java | 2 +- ...ServiceCreateUpdateServiceActionsStep.java | 2 +- .../DetermineServiceKeysToRecreateStep.java | 2 +- .../PrepareApplicationForBackupStep.java | 2 +- .../steps/RemoveMtaBackupMetadataStep.java | 2 +- .../controller/process/steps/StepsUtil.java | 2 +- .../AsyncJobToAsyncExecutionStateAdapter.java | 2 +- .../util/OperationInFinalStateHandler.java | 10 +- .../LoggingCloudControllerClientTest.java | 2 +- .../process/steps/CreateServiceStepTest.java | 2 +- .../DeleteServiceMtaMetadataStepTest.java | 2 +- .../DetachServiceKeysFromMtaStepTest.java | 2 +- .../steps/DetachServicesFromMtaStepTest.java | 2 +- .../steps/DetectDeployedMtaStepTest.java | 2 +- ...iceCreateUpdateServiceActionsStepTest.java | 2 +- ...ServiceDeleteActionsToExecuteStepTest.java | 2 +- ...etermineServiceKeysToRecreateStepTest.java | 2 +- ...WithResolvedDynamicParametersStepTest.java | 2 +- ...lServiceBindingOperationExecutionTest.java | 2 +- ...lServiceBrokerOperationsExecutionTest.java | 2 +- ...ServiceBrokersOperationsExecutionTest.java | 2 +- ...viceKeyCreationOperationExecutionTest.java | 2 +- ...rviceUnbindingsOperationExecutionTest.java | 4 +- .../PrepareApplicationForBackupStepTest.java | 2 +- ...PrepareBackupMtaForDeploymentStepTest.java | 2 +- .../RemoveMtaBackupMetadataStepTest.java | 2 +- .../process/steps/StepsTestUtil.java | 2 +- .../process/steps/SyncFlowableStepTest.java | 2 +- .../UpdateServiceKeysMetadataStepTest.java | 2 +- .../ExistingAppsToBackupCalculatorTest.java | 2 +- .../process/util/ModuleDeterminerTest.java | 2 +- 140 files changed, 6221 insertions(+), 121 deletions(-) create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/JobState.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/LastOperation.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Metadata.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceInstanceType.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/BuildsV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientV3Impl.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/DomainsV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/EventsV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/JobsV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/PackagesV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ProcessesV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RolesV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RoutesV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBindingsV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBrokersV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceKeysV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceOfferingsV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServicePlansV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/StacksV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/TasksV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Application.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ApplicationMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEvent.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEventMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Build.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3BuildMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Domain.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3DomainMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Droplet.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3InstancesInfoMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Job.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3JobMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ListResponse.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Metadata.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Package.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3PackageMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Process.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ProcessMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ResourceMappers.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Role.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RoleMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Route.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RouteMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBinding.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBindingMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBroker.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBrokerMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstance.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstanceMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyDetails.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceOffering.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceOfferingMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServicePlan.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServicePlanMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Stack.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3StackMapper.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Task.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3TaskMapper.java diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/ResilientCloudControllerClient.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/ResilientCloudControllerClient.java index acffeeb34c..94da22f577 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/ResilientCloudControllerClient.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/ResilientCloudControllerClient.java @@ -10,7 +10,6 @@ import java.util.function.Function; import java.util.function.Supplier; -import org.cloudfoundry.client.v3.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.ApplicationServicesUpdateCallback; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClientImpl; @@ -35,6 +34,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.DockerInfo; import org.cloudfoundry.multiapps.controller.client.facade.domain.DropletInfo; import org.cloudfoundry.multiapps.controller.client.facade.domain.InstancesInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.ServicePlanVisibility; import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; import org.cloudfoundry.multiapps.controller.client.facade.domain.Upload; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClient.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClient.java index 2d8f04ccdf..89f8dec346 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClient.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClient.java @@ -8,7 +8,7 @@ import java.util.Set; import java.util.UUID; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudBuild; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientImpl.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientImpl.java index ca0386040c..bdb1855440 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientImpl.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientImpl.java @@ -12,7 +12,6 @@ import java.util.function.Supplier; import org.cloudfoundry.AbstractCloudFoundryException; -import org.cloudfoundry.client.v3.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudBuild; @@ -32,6 +31,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.DockerInfo; import org.cloudfoundry.multiapps.controller.client.facade.domain.DropletInfo; import org.cloudfoundry.multiapps.controller.client.facade.domain.InstancesInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.ServicePlanVisibility; import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; import org.cloudfoundry.multiapps.controller.client.facade.domain.Upload; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplication.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplication.java index 898cea1a8a..63489ddd70 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplication.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplication.java @@ -32,7 +32,7 @@ public CloudApplication derive() { Application app = getApplication(); return ImmutableCloudApplication.builder() .metadata(parseResourceMetadata(app)) - .v3Metadata(app.getMetadata()) + .v3Metadata(toDomainMetadata(app.getMetadata())) .name(app.getName()) .state(parseState(app.getState())) .lifecycle(parseLifecycle(app.getLifecycle())) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJob.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJob.java index 1e8bad359c..3650a8de77 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJob.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJob.java @@ -20,7 +20,8 @@ public CloudAsyncJob derive() { Job job = getJob(); return ImmutableCloudAsyncJob.builder() .metadata(parseResourceMetadata(job)) - .state(job.getState()) + .state(org.cloudfoundry.multiapps.controller.client.facade.domain.JobState.valueOf(job.getState() + .name())) .operation(job.getOperation()) .warnings(getWarnings(job)) .errors(getErrors(job)) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntity.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntity.java index 537b1260e2..e5da78b0d5 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntity.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntity.java @@ -33,6 +33,37 @@ public static CloudMetadata parseResourceMetadata(Resource resource) { .build(); } + /** + * Convert the OSS {@code LastOperation} to the project-owned {@link org.cloudfoundry.multiapps.controller.client.facade.domain.LastOperation}. + * Shared by the service instance / binding / key adapters. (Part of the OSS-backed path; removed with the OSS client.) + */ + protected static org.cloudfoundry.multiapps.controller.client.facade.domain.LastOperation toDomainLastOperation( + org.cloudfoundry.client.v3.LastOperation lastOperation) { + if (lastOperation == null) { + return null; + } + return new org.cloudfoundry.multiapps.controller.client.facade.domain.LastOperation(lastOperation.getType(), + lastOperation.getState(), + lastOperation.getDescription(), + lastOperation.getCreatedAt(), + lastOperation.getUpdatedAt()); + } + + /** + * Convert the OSS v3 {@code Metadata} to the project-owned + * {@link org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata}. (OSS-backed path; removed with the OSS client.) + */ + protected static org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata toDomainMetadata( + org.cloudfoundry.client.v3.Metadata metadata) { + if (metadata == null) { + return null; + } + return org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata.builder() + .labels(metadata.getLabels()) + .annotations(metadata.getAnnotations()) + .build(); + } + protected static UUID parseNullableGuid(String guid) { return guid == null ? null : parseGuid(guid); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBinding.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBinding.java index ab9ba4cad8..b91a44dac7 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBinding.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBinding.java @@ -30,7 +30,7 @@ public CloudServiceBinding derive() { .getServiceInstance() .getData() .getId())) - .serviceBindingOperation(ServiceCredentialBindingOperation.from(getServiceBinding().getLastOperation())) + .serviceBindingOperation(ServiceCredentialBindingOperation.from(toDomainLastOperation(getServiceBinding().getLastOperation()))) .build(); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstance.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstance.java index af4d58b936..f66b1580aa 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstance.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstance.java @@ -29,13 +29,14 @@ public CloudServiceInstance derive() { ServiceInstanceResource resource = getResource(); return ImmutableCloudServiceInstance.builder() .metadata(parseResourceMetadata(resource)) - .v3Metadata(resource.getMetadata()) + .v3Metadata(toDomainMetadata(resource.getMetadata())) .name(resource.getName()) .plan(getServicePlanName()) .label(getLabelName()) - .type(resource.getType()) + .type(org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType.valueOf(resource.getType() + .name())) .tags(resource.getTags()) - .lastOperation(ServiceOperation.fromLastOperation(resource.getLastOperation())) + .lastOperation(ServiceOperation.fromLastOperation(toDomainLastOperation(resource.getLastOperation()))) .syslogDrainUrl(resource.getSyslogDrainUrl()) .build(); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKey.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKey.java index 97f6e8eb31..87afdbe471 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKey.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKey.java @@ -30,11 +30,11 @@ public CloudServiceKey derive() { Derivable serviceInstance = getServiceInstance(); return ImmutableCloudServiceKey.builder() .metadata(parseResourceMetadata(serviceBindingResource)) - .v3Metadata(serviceBindingResource.getMetadata()) + .v3Metadata(toDomainMetadata(serviceBindingResource.getMetadata())) .name(serviceBindingResource.getName()) .credentials(getCredentials()) .serviceInstance(serviceInstance.derive()) - .serviceKeyOperation(ServiceCredentialBindingOperation.from(serviceBindingResource.getLastOperation())) + .serviceKeyOperation(ServiceCredentialBindingOperation.from(toDomainLastOperation(serviceBindingResource.getLastOperation()))) .build(); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawInstancesInfo.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawInstancesInfo.java index 2c3a83c130..1b55721485 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawInstancesInfo.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawInstancesInfo.java @@ -39,7 +39,8 @@ private static List parseProcessStatistics(List { @Override public UserRole derive() { RoleResource role = getRoleResource(); - return UserRole.fromRoleType(role.getType()); + return UserRole.fromRoleType(role.getType() + .getValue()); } } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawV3CloudServiceInstance.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawV3CloudServiceInstance.java index 8155a4ad41..db610973ef 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawV3CloudServiceInstance.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawV3CloudServiceInstance.java @@ -16,7 +16,7 @@ public CloudServiceInstance derive() { ServiceInstance serviceInstance = getServiceInstance(); return ImmutableCloudServiceInstance.builder() .metadata(parseResourceMetadata(serviceInstance)) - .v3Metadata(serviceInstance.getMetadata()) + .v3Metadata(toDomainMetadata(serviceInstance.getMetadata())) .name(serviceInstance.getName()) .tags(serviceInstance.getTags()) .build(); diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudAsyncJob.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudAsyncJob.java index 394a40c3b7..20b52febc8 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudAsyncJob.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudAsyncJob.java @@ -1,6 +1,5 @@ package org.cloudfoundry.multiapps.controller.client.facade.domain; -import org.cloudfoundry.client.v3.jobs.JobState; import org.immutables.value.Value; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudEntity.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudEntity.java index 16bf0aef9b..aef922d829 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudEntity.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudEntity.java @@ -2,7 +2,7 @@ import java.util.UUID; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.Nullable; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceInstance.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceInstance.java index 78db202828..06f546412c 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceInstance.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceInstance.java @@ -4,7 +4,6 @@ import java.util.Map; import org.cloudfoundry.AllowNulls; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; import org.immutables.value.Value; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/HealthCheckType.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/HealthCheckType.java index 6cd1a86842..e618ee4b45 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/HealthCheckType.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/HealthCheckType.java @@ -3,8 +3,16 @@ public enum HealthCheckType { HTTP, PORT, PROCESS, @Deprecated NONE; + /** + * The CF v3 wire value (lowercase), matching the OSS {@code HealthCheckType.getValue()} this enum replaces at call sites. + */ + public String getValue() { + return name().toLowerCase(); + } + @Override public String toString() { - return this.name().toLowerCase(); + return this.name() + .toLowerCase(); } } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstanceState.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstanceState.java index 89d88d803d..90bd5f95d6 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstanceState.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstanceState.java @@ -1,16 +1,18 @@ package org.cloudfoundry.multiapps.controller.client.facade.domain; -import org.cloudfoundry.client.v3.processes.ProcessState; - public enum InstanceState { CRASHED, DOWN, RUNNING, STARTING, UNKNOWN; - public static InstanceState valueOfWithDefault(ProcessState state) { + /** + * Resolve from the CF v3 process {@code state} wire value (e.g. {@code RUNNING}). Takes the raw string rather than the OSS + * {@code ProcessState} enum, so the domain no longer depends on cf-java-client. Unknown/blank values map to {@link #UNKNOWN}. + */ + public static InstanceState valueOfWithDefault(String state) { if (state == null) { return UNKNOWN; } try { - return InstanceState.valueOf(state.getValue()); + return InstanceState.valueOf(state); } catch (IllegalArgumentException e) { return InstanceState.UNKNOWN; } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/JobState.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/JobState.java new file mode 100644 index 0000000000..ccb2f6521f --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/JobState.java @@ -0,0 +1,36 @@ +package org.cloudfoundry.multiapps.controller.client.facade.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * State of an asynchronous Cloud Controller v3 job. Project-owned replacement for {@code org.cloudfoundry.client.v3.jobs.JobState}, + * so the domain model no longer depends on the OSS cf-java-client. JSON (de)serialization uses the lowercase wire value + * ({@code processing} / {@code polling} / {@code complete} / {@code failed}) via {@link JsonValue} / {@link JsonCreator} — matching the + * OSS enum so persisted models round-trip identically. + */ +public enum JobState { + + PROCESSING("processing"), POLLING("polling"), COMPLETE("complete"), FAILED("failed"); + + private final String value; + + JobState(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static JobState from(String value) { + for (JobState state : values()) { + if (state.value.equals(value)) { + return state; + } + } + throw new IllegalArgumentException("Unknown job state: " + value); + } +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/LastOperation.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/LastOperation.java new file mode 100644 index 0000000000..8aefe77484 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/LastOperation.java @@ -0,0 +1,9 @@ +package org.cloudfoundry.multiapps.controller.client.facade.domain; + +/** + * Project-owned view of a Cloud Controller v3 resource's {@code last_operation} (raw string fields), replacing + * {@code org.cloudfoundry.client.v3.LastOperation} so the domain no longer depends on the OSS cf-java-client. All fields are the raw CF + * wire values; any may be {@code null}. + */ +public record LastOperation(String type, String state, String description, String createdAt, String updatedAt) { +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Metadata.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Metadata.java new file mode 100644 index 0000000000..979e357d6d --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Metadata.java @@ -0,0 +1,122 @@ +package org.cloudfoundry.multiapps.controller.client.facade.domain; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Project-owned replacement for {@code org.cloudfoundry.client.v3.Metadata} (CF v3 resource labels + annotations), so the domain model no + * longer depends on the OSS cf-java-client. + *

+ * It is a faithful drop-in: the fluent {@link Builder} exposes the same {@code label(k,v)} / {@code annotation(k,v)} / + * {@code labels(map)} / {@code annotations(map)} methods the codebase already calls, and JSON (de)serialization uses the same + * {@code {"labels": {...}, "annotations": {...}}} shape as the OSS type, so persisted models (e.g. {@code CloudEntity.getV3Metadata()}) + * round-trip identically. + */ +public final class Metadata { + + private final Map labels; + private final Map annotations; + + @JsonCreator + Metadata(@JsonProperty("labels") Map labels, @JsonProperty("annotations") Map annotations) { + this.labels = labels == null ? Collections.emptyMap() : Collections.unmodifiableMap(new LinkedHashMap<>(labels)); + this.annotations = annotations == null ? Collections.emptyMap() : Collections.unmodifiableMap(new LinkedHashMap<>(annotations)); + } + + @JsonProperty("labels") + public Map getLabels() { + return labels; + } + + @JsonProperty("annotations") + public Map getAnnotations() { + return annotations; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Metadata metadata = (Metadata) o; + return labels.equals(metadata.labels) && annotations.equals(metadata.annotations); + } + + @Override + public int hashCode() { + return Objects.hash(labels, annotations); + } + + @Override + public String toString() { + return "Metadata{annotations=" + annotations + ", labels=" + labels + "}"; + } + + public static final class Builder { + + private final Map labels = new LinkedHashMap<>(); + private final Map annotations = new LinkedHashMap<>(); + + private Builder() { + } + + /** + * Copy labels and annotations from an existing {@link Metadata}. Mirrors the OSS builder's {@code from(Metadata)}. + */ + public Builder from(Metadata metadata) { + if (metadata != null) { + this.labels.putAll(metadata.getLabels()); + this.annotations.putAll(metadata.getAnnotations()); + } + return this; + } + + public Builder label(String key, String value) { + this.labels.put(key, value); + return this; + } + + public Builder labels(Map labels) { + if (labels != null) { + this.labels.putAll(labels); + } + return this; + } + + public Builder putAllLabels(Map labels) { + return labels(labels); + } + + public Builder annotation(String key, String value) { + this.annotations.put(key, value); + return this; + } + + public Builder annotations(Map annotations) { + if (annotations != null) { + this.annotations.putAll(annotations); + } + return this; + } + + public Builder putAllAnnotations(Map annotations) { + return annotations(annotations); + } + + public Metadata build() { + return new Metadata(labels, annotations); + } + } +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceCredentialBindingOperation.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceCredentialBindingOperation.java index 4f202834cc..ea06444480 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceCredentialBindingOperation.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceCredentialBindingOperation.java @@ -5,7 +5,6 @@ import java.time.format.DateTimeFormatter; import java.util.Arrays; -import org.cloudfoundry.client.v3.LastOperation; import org.immutables.value.Value; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -31,11 +30,11 @@ public abstract class ServiceCredentialBindingOperation { public abstract LocalDateTime getUpdatedAt(); public static ServiceCredentialBindingOperation from(LastOperation lastOperation) { - String lastOperationType = lastOperation.getType(); - String lastOperationState = lastOperation.getState(); - String lastOperationDescription = lastOperation.getDescription(); - String lastOperationCreatedAt = lastOperation.getCreatedAt(); - String lastOperationUpdatedAt = lastOperation.getUpdatedAt(); + String lastOperationType = lastOperation.type(); + String lastOperationState = lastOperation.state(); + String lastOperationDescription = lastOperation.description(); + String lastOperationCreatedAt = lastOperation.createdAt(); + String lastOperationUpdatedAt = lastOperation.updatedAt(); return ImmutableServiceCredentialBindingOperation.builder() .type(ServiceCredentialBindingOperation.Type.fromString(lastOperationType)) .state(ServiceCredentialBindingOperation.State.fromString(lastOperationState)) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceInstanceType.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceInstanceType.java new file mode 100644 index 0000000000..90618abbf9 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceInstanceType.java @@ -0,0 +1,40 @@ +package org.cloudfoundry.multiapps.controller.client.facade.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Type of a Cloud Controller v3 service instance. Project-owned replacement for + * {@code org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType}, so the domain model no longer depends on the OSS + * cf-java-client. Values mirror the CF v3 service instance {@code type} field, and JSON (de)serialization uses the wire value + * ({@code managed} / {@code user-provided}) via {@link JsonValue} / {@link JsonCreator} — matching the OSS enum's behaviour so persisted + * models round-trip identically. + */ +public enum ServiceInstanceType { + + MANAGED("managed"), USER_PROVIDED("user-provided"); + + private final String value; + + ServiceInstanceType(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + /** + * Resolve from the CF v3 wire value ({@code managed} / {@code user-provided}). Mirrors the OSS {@code ServiceInstanceType.from}. + */ + @JsonCreator + public static ServiceInstanceType from(String value) { + for (ServiceInstanceType type : values()) { + if (type.value.equals(value)) { + return type; + } + } + throw new IllegalArgumentException("Unknown service instance type: " + value); + } +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceOperation.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceOperation.java index 151dae6bfa..b799c4c3ed 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceOperation.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceOperation.java @@ -3,8 +3,6 @@ import java.text.MessageFormat; import java.util.Objects; -import org.cloudfoundry.client.v3.LastOperation; - public class ServiceOperation { public enum Type { @@ -82,12 +80,12 @@ public State getState() { } public static ServiceOperation fromLastOperation(LastOperation lastOperation) { - if (lastOperation == null || lastOperation.getType() == null || lastOperation.getState() == null) { + if (lastOperation == null || lastOperation.type() == null || lastOperation.state() == null) { return null; } - Type type = Type.fromString(lastOperation.getType()); - State state = State.fromString(lastOperation.getState()); - String description = lastOperation.getDescription(); + Type type = Type.fromString(lastOperation.type()); + State state = State.fromString(lastOperation.state()); + String description = lastOperation.description(); return new ServiceOperation(type, description, state); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/UserRole.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/UserRole.java index 3ee5e84386..ed0b02389f 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/UserRole.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/UserRole.java @@ -4,8 +4,6 @@ import java.util.Map; import java.util.stream.Collectors; -import org.cloudfoundry.client.v3.roles.RoleType; - public enum UserRole { ORGANIZATION_AUDITOR, @@ -20,10 +18,14 @@ public enum UserRole { .collect(Collectors.toMap(UserRole::getName, roleType -> roleType)); - public static UserRole fromRoleType(RoleType roleType) { - UserRole userRole = NAMES_TO_VALUES.get(roleType.getValue()); + /** + * Resolve from the CF v3 role {@code type} wire value (e.g. {@code space_developer}). Takes the raw string rather than the OSS + * {@code RoleType} enum, so the domain no longer depends on cf-java-client. + */ + public static UserRole fromRoleType(String roleTypeValue) { + UserRole userRole = NAMES_TO_VALUES.get(roleTypeValue); if (userRole == null) { - throw new IllegalArgumentException("Unknown user role: " + roleType.getValue()); + throw new IllegalArgumentException("Unknown user role: " + roleTypeValue); } return userRole; } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/dto/ApplicationToCreateDto.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/dto/ApplicationToCreateDto.java index e23244f2f4..c32aa17139 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/dto/ApplicationToCreateDto.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/dto/ApplicationToCreateDto.java @@ -3,7 +3,7 @@ import java.util.Map; import java.util.Set; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.immutables.value.Value; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/BuildsV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/BuildsV3Operations.java new file mode 100644 index 0000000000..2605aeb4a8 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/BuildsV3Operations.java @@ -0,0 +1,64 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudBuild; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Build; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3BuildMapper; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.springframework.core.ParameterizedTypeReference; + +/** + * CF v3 Builds operations for the in-house {@link CloudControllerRestClientV3Impl}. Reproduces the HTTP shape of the OSS + * {@code CloudControllerRestClientImpl} build methods (endpoints from {@code builds()} / {@code applicationsV3().listBuilds}), mapping the + * v3 wire model to the {@link CloudBuild} domain type via {@link V3BuildMapper}. + */ +public class BuildsV3Operations { + + private static final int MAX_PAGE_SIZE = 5000; + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public BuildsV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + public CloudBuild createBuild(UUID packageGuid) { + V3Build build = cc.getRestClient() + .post() + .uri("/v3/builds") + .body(Map.of("package", Map.of("guid", packageGuid.toString()))) + .retrieve() + .body(V3Build.class); + return V3BuildMapper.toCloudBuild(build); + } + + public CloudBuild getBuild(UUID buildGuid) { + V3Build build = cc.get("/v3/builds/" + buildGuid, V3Build.class); + return V3BuildMapper.toCloudBuild(build); + } + + public List getBuildsForApplication(UUID applicationGuid) { + String uri = "/v3/apps/" + applicationGuid + "/builds?per_page=" + MAX_PAGE_SIZE; + return cc.list(uri, new ParameterizedTypeReference>() { + }) + .stream() + .map(V3BuildMapper::toCloudBuild) + .toList(); + } + + public List getBuildsForPackage(UUID packageGuid) { + String uri = "/v3/builds?package_guids=" + packageGuid + "&per_page=" + MAX_PAGE_SIZE; + return cc.list(uri, new ParameterizedTypeReference>() { + }) + .stream() + .map(V3BuildMapper::toCloudBuild) + .toList(); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClient.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClient.java index 8c79aec33e..4ed03f71d4 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClient.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClient.java @@ -8,7 +8,7 @@ import java.util.Set; import java.util.UUID; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.UploadStatusCallback; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java new file mode 100644 index 0000000000..4c0d158f1d --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java @@ -0,0 +1,128 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.net.URL; +import java.net.http.HttpClient; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.client.JdkClientHttpRequestFactory; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.RestClient; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; + +/** + * Builds the blocking Spring {@link RestClient} used by {@link CloudControllerRestClientV3Impl} to talk to the Cloud Controller v3 + * REST API directly, without the OSS cf-java-client. + *

+ * The transport is the JDK {@link java.net.http.HttpClient} (via {@link JdkClientHttpRequestFactory}). This is deliberately chosen over + * reactor-netty: it is already available through {@code spring-web} and the JDK, so it pulls no extra transitive dependencies + * (no reactor-netty, no spring-webflux) into the deploy-service WAR — which is exactly the dependency-shedding this migration is about. + * The JDK client provides connection reuse (HTTP/2 multiplexing + keep-alive) and connect/read timeouts; TLS trust is configurable for + * self-signed landscapes. Every request carries a bearer token from {@link OAuthClient} (refreshed as needed), and non-2xx responses are + * mapped to {@code CloudOperationException} by the existing {@link CloudControllerResponseErrorHandler}. + */ +public final class CloudControllerRestClientBuilder { + + private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofMinutes(1); + + private CloudControllerRestClientBuilder() { + } + + public static RestClient build(URL v3ApiUrl, OAuthClient oAuthClient, Map requestTags, TransportOptions options) { + JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(buildHttpClient(options)); + options.responseTimeout() + .ifPresent(requestFactory::setReadTimeout); + return RestClient.builder() + .baseUrl(v3ApiUrl.toString()) + .requestFactory(requestFactory) + // Pass a fully-formed converter list via the List overload — deliberately NOT the Consumer overload. + // messageConverters(Consumer) first calls initMessageConverters() to build Spring's DEFAULT list and only then + // hands it to the consumer; that default init eagerly constructs AllEncompassingFormHttpMessageConverter -> + // MappingJackson2YamlHttpMessageConverter -> Jackson2ObjectMapperBuilder.yaml(), which loads a YAMLFactory that is + // incompatible with this codebase's jackson-core (mixed Jackson 2/3 classpath) and throws VerifyError at load time + // — so clearing the list afterwards is too late. The List overload skips initMessageConverters() entirely, so the + // YAML converter is never built. CF v3 is JSON-only; JSON + String + byte[] (from an ObjectMapper we control) suffice. + .messageConverters(List.of(new ByteArrayHttpMessageConverter(), + new StringHttpMessageConverter(), + new MappingJackson2HttpMessageConverter(new ObjectMapper()))) + .requestInterceptor((request, body, execution) -> { + String authorization = oAuthClient.getAuthorizationHeaderValue(); + if (authorization != null) { + request.getHeaders() + .set(HttpHeaders.AUTHORIZATION, authorization); + } + requestTags.forEach((key, value) -> request.getHeaders() + .set(key, value)); + return execution.execute(request, body); + }) + .defaultStatusHandler(new CloudControllerResponseErrorHandler()) + .build(); + } + + private static HttpClient buildHttpClient(TransportOptions options) { + HttpClient.Builder builder = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .connectTimeout(options.connectTimeout() + .orElse(DEFAULT_CONNECT_TIMEOUT)); + if (options.trustSelfSignedCertificates()) { + builder.sslContext(buildTrustAllSslContext()); + } + return builder.build(); + } + + private static SSLContext buildTrustAllSslContext() { + try { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[] { trustAllManager() }, new SecureRandom()); + return sslContext; + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new IllegalStateException("An error occurred setting up the SSLContext", e); + } + } + + private static X509TrustManager trustAllManager() { + return new X509TrustManager() { + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + // NOSONAR - self-signed certificate trust is opt-in via TransportOptions + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + // NOSONAR - self-signed certificate trust is opt-in via TransportOptions + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[] {}; + } + + }; + } + + /** + * The transport-tuning surface carried over from the OSS-backed factory, so operators keep the same knobs. {@code connectionPoolSize} + * is accepted for parity but the JDK client manages connection reuse internally; it is retained for a future backend swap. + */ + public record TransportOptions(Optional connectTimeout, Optional responseTimeout, + Optional connectionPoolSize, boolean trustSelfSignedCertificates) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java index d2258ed03c..47a209a587 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java @@ -5,11 +5,17 @@ import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableCloudFoundryClientFactory; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; +import org.cloudfoundry.multiapps.controller.client.facade.util.JsonUtil; import org.cloudfoundry.multiapps.controller.client.facade.util.RestUtil; import org.cloudfoundry.client.CloudFoundryClient; import org.immutables.value.Value; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.client.RestClient; import java.net.URL; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.time.Duration; import java.util.Collections; import java.util.Map; @@ -17,6 +23,7 @@ @Value.Immutable public abstract class CloudControllerRestClientFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(CloudControllerRestClientFactory.class); private final RestUtil restUtil = new RestUtil(); public abstract Optional getSslHandshakeTimeout(); @@ -34,6 +41,19 @@ public boolean shouldTrustSelfSignedCertificates() { return false; } + /** + * Feature flag for the cf-java-client migration PoC. When {@code true}, {@link #createClient} builds the in-house + * {@link CloudControllerRestClientV3Impl} (direct CF v3 REST, no cf-java-client); when {@code false} (default), it builds the + * OSS-backed {@link CloudControllerRestClientImpl}. Kept default-off so existing behaviour is unchanged and the two can be A/B + * compared against the same live Cloud Foundry. + */ + @Value.Default + public boolean useV3RestClient() { + // cf_java_client_poc branch: default ON so the in-house CloudControllerRestClientV3Impl is always chosen for OQ verification. + // Do NOT merge this default to main — the switch belongs off there until the client is validated. + return true; + } + @Value.Derived public CloudFoundryClientFactory getCloudFoundryClientFactory() { ImmutableCloudFoundryClientFactory.Builder builder = ImmutableCloudFoundryClientFactory.builder(); @@ -61,10 +81,86 @@ public CloudControllerRestClient createClient(URL controllerUrl, CloudCredential public CloudControllerRestClient createClient(URL controllerUrl, CloudCredentials credentials, CloudSpace target, OAuthClient oAuthClient, Map requestTags) { oAuthClient.init(credentials); + if (useV3RestClient()) { + return createV3RestClient(controllerUrl, target, oAuthClient, requestTags); + } CloudFoundryClient delegate = getCloudFoundryClientFactory().createClient(controllerUrl, oAuthClient, requestTags); return new CloudControllerRestClientImpl(delegate, target); } + private CloudControllerRestClient createV3RestClient(URL controllerUrl, CloudSpace target, OAuthClient oAuthClient, + Map requestTags) { + URL v3ApiUrl = deriveV3ApiUrl(controllerUrl, requestTags); + // The RestClient baseUrl must be the API ORIGIN (scheme://host[:port]) only — every operation path already carries the + // "/v3/..." prefix, so using the ".../v3" href as the base would produce a duplicated "/v3/v3/apps" path (CF -> 404 Unknown + // request). We keep v3ApiUrl (which includes /v3) for reference/root-link semantics but strip it to the origin for the base. + URL baseUrl = toOrigin(v3ApiUrl); + LOGGER.warn("[cf-v3-poc] controllerUrl={} resolved v3ApiUrl={} restClientBaseUrl={}", controllerUrl, v3ApiUrl, baseUrl); + var transportOptions = new CloudControllerRestClientBuilder.TransportOptions(getConnectTimeout(), getResponseTimeout(), + getConnectionPoolSize(), + shouldTrustSelfSignedCertificates()); + RestClient restClient = CloudControllerRestClientBuilder.build(baseUrl, oAuthClient, requestTags, transportOptions); + return new CloudControllerRestClientV3Impl(v3ApiUrl, oAuthClient, target, restClient); + } + + // scheme://host[:port] of the given URL, dropping any path (e.g. the trailing /v3). + private URL toOrigin(URL url) { + try { + int port = url.getPort(); + String origin = url.getProtocol() + "://" + url.getHost() + (port == -1 ? "" : ":" + port); + return java.net.URI.create(origin) + .toURL(); + } catch (java.net.MalformedURLException e) { + throw new IllegalArgumentException("Could not derive the API origin from " + url, e); + } + } + + // Resolve the CF v3 API base URL the same way the OSS factory does: GET the CF root and read links.cloud_controller_v3.href. + // The conventional controllerUrl + "/v3" is only a fallback — on some landscapes the real v3 href differs, and using the naive + // path yields "404 Not Found: Unknown request" from the Cloud Controller. + @SuppressWarnings("unchecked") + private URL deriveV3ApiUrl(URL controllerUrl, Map requestTags) { + try { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .GET() + .uri(controllerUrl.toURI()) + .timeout(Duration.ofMinutes(1)); + requestTags.forEach(requestBuilder::header); + HttpResponse response = rootHttpClient().send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() / 100 != 2) { + return fallbackV3ApiUrl(controllerUrl); + } + Map links = (Map) JsonUtil.convertJsonToMap(response.body()) + .get("links"); + Map ccv3 = (Map) links.get("cloud_controller_v3"); + String href = (String) ccv3.get("href"); + return java.net.URI.create(href) + .toURL(); + } catch (InterruptedException e) { + Thread.currentThread() + .interrupt(); + return fallbackV3ApiUrl(controllerUrl); + } catch (Exception e) { + return fallbackV3ApiUrl(controllerUrl); + } + } + + private static java.net.http.HttpClient rootHttpClient() { + return java.net.http.HttpClient.newBuilder() + .connectTimeout(Duration.ofMinutes(1)) + .build(); + } + + private URL fallbackV3ApiUrl(URL controllerUrl) { + try { + return java.net.URI.create(controllerUrl.toString() + "/v3") + .toURL(); + } catch (java.net.MalformedURLException e) { + throw new IllegalArgumentException("Could not derive the CF v3 API URL from " + controllerUrl, e); + } + } + + private OAuthClient createOAuthClient(URL controllerUrl, String origin) { return restUtil.createOAuthClientByControllerUrl(controllerUrl, shouldTrustSelfSignedCertificates()); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientImpl.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientImpl.java index c1e6718d16..95172fea6c 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientImpl.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientImpl.java @@ -25,7 +25,7 @@ import org.cloudfoundry.client.v3.DockerData; import org.cloudfoundry.client.v3.Lifecycle; import org.cloudfoundry.client.v3.LifecycleType; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.client.v3.Relationship; import org.cloudfoundry.client.v3.Resource; import org.cloudfoundry.client.v3.ToOneRelationship; @@ -305,7 +305,7 @@ public void createApplication(ApplicationToCreateDto applicationToCreateDto) { assertSpaceProvided("create application"); CreateApplicationRequest createApplicationRequest = CreateApplicationRequest.builder() .name(applicationToCreateDto.getName()) - .metadata(applicationToCreateDto.getMetadata()) + .metadata(toOssMetadata(applicationToCreateDto.getMetadata())) .lifecycle(buildApplicationLifecycle( applicationToCreateDto.getStaging())) .relationships(buildApplicationRelationships()) @@ -472,7 +472,7 @@ public void createServiceInstance(CloudServiceInstance serviceInstance) { .create(CreateServiceInstanceRequest.builder() .type(ServiceInstanceType.MANAGED) .name(serviceInstance.getName()) - .metadata(serviceInstance.getV3Metadata()) + .metadata(toOssMetadata(serviceInstance.getV3Metadata())) .relationships(ServiceInstanceRelationships.builder() .servicePlan(buildToOneRelationship( servicePlanGuid.toString())) @@ -557,7 +557,7 @@ private Optional doCreateServiceKey(String name, Map par private CreateServiceBindingRequest buildServiceCredentialBindingRequest(String name, Map parameters, Metadata metadata, CloudServiceInstance serviceInstance) { - if (serviceInstance.getType() != ServiceInstanceType.MANAGED) { + if (serviceInstance.getType() != org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType.MANAGED) { throw new IllegalArgumentException( String.format(Messages.CANT_CREATE_SERVICE_KEY_FOR_USER_PROVIDED_SERVICE, serviceInstance.getName())); } @@ -566,7 +566,7 @@ private CreateServiceBindingRequest buildServiceCredentialBindingRequest(String var createBindingRequest = CreateServiceBindingRequest.builder() .type(ServiceBindingType.KEY) .name(name) - .metadata(metadata) + .metadata(toOssMetadata(metadata)) .relationships(ServiceBindingRelationships.builder() .serviceInstance( buildToOneRelationship( @@ -578,6 +578,16 @@ private CreateServiceBindingRequest buildServiceCredentialBindingRequest(String return createBindingRequest.build(); } + private static org.cloudfoundry.client.v3.Metadata toOssMetadata(Metadata metadata) { + if (metadata == null) { + return null; + } + return org.cloudfoundry.client.v3.Metadata.builder() + .labels(metadata.getLabels()) + .annotations(metadata.getAnnotations()) + .build(); + } + @Override public void createUserProvidedServiceInstance(CloudServiceInstance serviceInstance) { assertSpaceProvided("create service instance"); @@ -586,7 +596,7 @@ public void createUserProvidedServiceInstance(CloudServiceInstance serviceInstan delegate.serviceInstancesV3() .create(CreateServiceInstanceRequest.builder() .name(serviceInstance.getName()) - .metadata(serviceInstance.getV3Metadata()) + .metadata(toOssMetadata(serviceInstance.getV3Metadata())) .type(ServiceInstanceType.USER_PROVIDED) .credentials(serviceInstance.getCredentials()) .syslogDrainUrl(syslogDrainUrl) @@ -1325,11 +1335,11 @@ public void updateApplicationMemory(String applicationName, int memory) { } @Override - public void updateApplicationMetadata(UUID guid, org.cloudfoundry.client.v3.Metadata metadata) { + public void updateApplicationMetadata(UUID guid, Metadata metadata) { delegate.applicationsV3() .update(org.cloudfoundry.client.v3.applications.UpdateApplicationRequest.builder() .applicationId(guid.toString()) - .metadata(metadata) + .metadata(toOssMetadata(metadata)) .build()) .block(); } @@ -1424,21 +1434,21 @@ public String updateServiceBroker(CloudServiceBroker serviceBroker) { } @Override - public void updateServiceInstanceMetadata(UUID guid, org.cloudfoundry.client.v3.Metadata metadata) { + public void updateServiceInstanceMetadata(UUID guid, Metadata metadata) { delegate.serviceInstancesV3() .update(UpdateServiceInstanceRequest.builder() .serviceInstanceId(guid.toString()) - .metadata(metadata) + .metadata(toOssMetadata(metadata)) .build()) .block(); } @Override - public void updateServiceBindingMetadata(UUID guid, org.cloudfoundry.client.v3.Metadata metadata) { + public void updateServiceBindingMetadata(UUID guid, Metadata metadata) { delegate.serviceBindingsV3() .update(UpdateServiceBindingRequest.builder() .serviceBindingId(guid.toString()) - .metadata(metadata) + .metadata(toOssMetadata(metadata)) .build()) .block(); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientV3Impl.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientV3Impl.java new file mode 100644 index 0000000000..d0152f3d7b --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientV3Impl.java @@ -0,0 +1,831 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.net.URL; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.UploadStatusCallback; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudBuild; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudDomain; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEvent; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudPackage; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudProcess; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudRoute; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBinding; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBroker; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceOffering; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudStack; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudTask; +import org.cloudfoundry.multiapps.controller.client.facade.domain.DockerInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.DropletInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.InstancesInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServicePlanVisibility; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Upload; +import org.cloudfoundry.multiapps.controller.client.facade.domain.UserRole; +import org.cloudfoundry.multiapps.controller.client.facade.dto.ApplicationToCreateDto; +import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Application; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ApplicationMapper; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.web.client.RestClient; + +/** + * A {@link CloudControllerRestClient} implementation that talks to the Cloud Controller v3 REST API directly through a blocking Spring + * {@link RestClient}, with no dependency on the OSS cf-java-client. + *

+ * This is the target implementation of the "migrate off cf-java-client" PoC. It is selected in + * {@link CloudControllerRestClientFactory} via a feature flag; when the flag is off, the OSS-backed + * {@link CloudControllerRestClientImpl} is used instead, so the two can be A/B compared against the same live Cloud Foundry. + *

+ * SCAFFOLD STATE: every operation currently throws {@link UnsupportedOperationException}. Operations are being filled in + * resource-group by resource-group per docs/cf-java-client-migration/02-endpoint-inventory.md. The shared machinery (pagination walker, + * async job poller, JSON model) lands first and is reused by every operation. + */ +public class CloudControllerRestClientV3Impl implements CloudControllerRestClient { + + // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). + private static final int DEFAULT_PAGE_SIZE = 5000; + // Matches the OSS impl's DELETE_JOB_TIMEOUT for async resource deletions. + private static final Duration DELETE_JOB_TIMEOUT = Duration.ofMinutes(5); + + private final URL v3ApiUrl; + private final OAuthClient oAuthClient; + private final CloudSpace target; + private final RestClient restClient; + private final CloudControllerV3Client cc; + private final BuildsV3Operations buildsOps; + private final DomainsV3Operations domainsOps; + private final EventsV3Operations eventsOps; + private final JobsV3Operations jobsOps; + private final PackagesV3Operations packagesOps; + private final ProcessesV3Operations processesOps; + private final RolesV3Operations rolesOps; + private final RoutesV3Operations routesOps; + private final ServiceBindingsV3Operations serviceBindingsOps; + private final ServiceBrokersV3Operations serviceBrokersOps; + private final ServiceInstancesV3Operations serviceInstancesOps; + private final ServiceKeysV3Operations serviceKeysOps; + private final ServiceOfferingsV3Operations serviceOfferingsOps; + private final ServicePlansV3Operations servicePlansOps; + private final StacksV3Operations stacksOps; + private final TasksV3Operations tasksOps; + + public CloudControllerRestClientV3Impl(URL v3ApiUrl, OAuthClient oAuthClient, CloudSpace target, RestClient restClient) { + this.v3ApiUrl = v3ApiUrl; + this.oAuthClient = oAuthClient; + this.target = target; + this.restClient = restClient; + this.cc = new CloudControllerV3Client(restClient); + this.buildsOps = new BuildsV3Operations(cc, target); + this.domainsOps = new DomainsV3Operations(cc, target); + this.eventsOps = new EventsV3Operations(cc, target); + this.jobsOps = new JobsV3Operations(cc, target); + this.packagesOps = new PackagesV3Operations(cc, target); + this.processesOps = new ProcessesV3Operations(cc, target); + this.rolesOps = new RolesV3Operations(cc, target); + this.routesOps = new RoutesV3Operations(cc, target); + this.serviceBindingsOps = new ServiceBindingsV3Operations(cc, target); + this.serviceBrokersOps = new ServiceBrokersV3Operations(cc, target); + this.serviceInstancesOps = new ServiceInstancesV3Operations(cc, target); + this.serviceKeysOps = new ServiceKeysV3Operations(cc, target); + this.serviceOfferingsOps = new ServiceOfferingsV3Operations(cc, target); + this.servicePlansOps = new ServicePlansV3Operations(cc, target); + this.stacksOps = new StacksV3Operations(cc, target); + this.tasksOps = new TasksV3Operations(cc, target); + } + + private static UnsupportedOperationException notImplemented() { + StackWalker.StackFrame caller = StackWalker.getInstance() + .walk(frames -> frames.skip(1) + .findFirst() + .orElse(null)); + String operation = caller == null ? "operation" : caller.getMethodName(); + return new UnsupportedOperationException( + "CloudControllerRestClientV3Impl." + operation + " is not implemented yet (cf-java-client migration PoC scaffold)."); + } + + @Override + public CloudSpace getTarget() { + return target; + } + + @Override + public void addDomain(String domainName) { + domainsOps.addDomain(domainName); + } + + @Override + public void addRoute(String host, String domainName, String path) { + routesOps.addRoute(host, domainName, path); + } + + @Override + public Optional bindServiceInstance(String bindingName, String applicationName, String serviceInstanceName) { + return serviceBindingsOps.bindServiceInstance(bindingName, applicationName, serviceInstanceName); + } + + @Override + public Optional bindServiceInstance(String bindingName, String applicationName, String serviceInstanceName, + Map parameters) { + return serviceBindingsOps.bindServiceInstance(bindingName, applicationName, serviceInstanceName, parameters); + } + + @Override + public void createApplication(ApplicationToCreateDto dto) { + if (target == null || target.getGuid() == null) { + throw new CloudOperationException(HttpStatus.BAD_REQUEST, "Bad Request", "A target space is required to create an application."); + } + Map body = new java.util.HashMap<>(); + body.put("name", dto.getName()); + body.put("lifecycle", buildLifecycle(dto.getStaging())); + body.put("relationships", Map.of("space", Map.of("data", Map.of("guid", target.getGuid() + .toString())))); + if (dto.getEnv() != null) { + body.put("environment_variables", dto.getEnv()); + } + if (dto.getMetadata() != null) { + body.put("metadata", Map.of("labels", dto.getMetadata() + .getLabels(), + "annotations", dto.getMetadata() + .getAnnotations())); + } + V3Application created = cc.getRestClient() + .post() + .uri("/v3/apps") + .body(body) + .retrieve() + .body(V3Application.class); + UUID appGuid = UUID.fromString(created.guid()); + // Follow-up attributes, mirroring the OSS updateApplicationAttributes: staging/process, memory+disk scale, then routes. + if (dto.getStaging() != null) { + processesOps.updateApplicationStaging(dto.getName(), dto.getStaging()); + } + Map scale = new java.util.HashMap<>(); + if (dto.getMemoryInMb() != null) { + scale.put("memory_in_mb", dto.getMemoryInMb()); + } + if (dto.getDiskQuotaInMb() != null) { + scale.put("disk_in_mb", dto.getDiskQuotaInMb()); + } + if (!scale.isEmpty()) { + scaleWebProcess(appGuid, scale); + } + if (dto.getRoutes() != null && !dto.getRoutes() + .isEmpty()) { + updateApplicationRoutes(dto.getName(), dto.getRoutes()); + } + } + + // Builds the CF v3 lifecycle object: docker when DockerInfo is present, otherwise buildpack/cnb with buildpacks + stack. + private Map buildLifecycle(Staging staging) { + if (staging == null) { + return Map.of("type", "buildpack", "data", Map.of()); + } + if (staging.getDockerInfo() != null) { + return Map.of("type", "docker", "data", Map.of()); + } + String type = staging.getLifecycleType() != null ? staging.getLifecycleType() + .name() + .toLowerCase() + : "buildpack"; + Map data = new java.util.HashMap<>(); + if (staging.getBuildpacks() != null) { + data.put("buildpacks", staging.getBuildpacks()); + } + if (staging.getStackName() != null) { + data.put("stack", staging.getStackName()); + } + return Map.of("type", type, "data", data); + } + + @Override + public void createServiceInstance(CloudServiceInstance serviceInstance) { + serviceInstancesOps.createServiceInstance(serviceInstance); + } + + @Override + public String createServiceBroker(CloudServiceBroker serviceBroker) { + return serviceBrokersOps.createServiceBroker(serviceBroker); + } + + @Override + public CloudServiceKey createAndFetchServiceKey(CloudServiceKey keyModel, String serviceInstanceName) { + return serviceKeysOps.createAndFetchServiceKey(keyModel, getServiceInstance(serviceInstanceName)); + } + + @Override + public Optional createServiceKey(CloudServiceKey keyModel, String serviceInstanceName) { + return serviceKeysOps.createServiceKey(keyModel, getServiceInstance(serviceInstanceName)); + } + + @Override + public Optional createServiceKey(String serviceInstanceName, String serviceKeyName, Map parameters) { + return serviceKeysOps.createServiceKey(getServiceInstance(serviceInstanceName), serviceKeyName, parameters); + } + + @Override + public void createUserProvidedServiceInstance(CloudServiceInstance serviceInstance) { + serviceInstancesOps.createUserProvidedServiceInstance(serviceInstance); + } + + @Override + public void deleteApplication(String applicationName) { + UUID applicationGuid = getApplicationGuid(applicationName); + var response = cc.getRestClient() + .delete() + .uri("/v3/apps/{guid}", applicationGuid) + .retrieve() + .toBodilessEntity(); + cc.followAsyncJob(response, DELETE_JOB_TIMEOUT); + } + + @Override + public void deleteDomain(String domainName) { + domainsOps.deleteDomain(domainName); + } + + @Override + public void deleteOrphanedRoutes() { + routesOps.deleteOrphanedRoutes(); + } + + @Override + public void deleteRoute(String host, String domainName, String path) { + routesOps.deleteRoute(host, domainName, path); + } + + @Override + public void deleteServiceInstance(String serviceInstanceName) { + serviceInstancesOps.deleteServiceInstance(serviceInstanceName); + } + + @Override + public void deleteServiceInstance(CloudServiceInstance serviceInstance) { + serviceInstancesOps.deleteServiceInstance(serviceInstance); + } + + @Override + public String deleteServiceBroker(String name) { + return serviceBrokersOps.deleteServiceBroker(name); + } + + @Override + public Optional deleteServiceBinding(String serviceInstanceName, String serviceKeyName) { + // Mirrors the OSS impl: resolve the named service key, then delete its binding by GUID. + CloudServiceKey serviceKey = getServiceKey(serviceInstanceName, serviceKeyName); + if (serviceKey == null) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service key " + serviceKeyName + " not found."); + } + return deleteServiceBinding(serviceKey.getGuid()); + } + + @Override + public Optional deleteServiceBinding(UUID bindingGuid) { + return serviceBindingsOps.deleteServiceBinding(bindingGuid); + } + + @Override + public CloudApplication getApplication(String applicationName) { + return getApplication(applicationName, true); + } + + @Override + public CloudApplication getApplication(String applicationName, boolean required) { + V3Application app = findApplicationByName(applicationName); + if (app == null) { + if (required) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + applicationName + " not found."); + } + return null; + } + return V3ApplicationMapper.toCloudApplication(app, target); + } + + @Override + public UUID getApplicationGuid(String applicationName) { + return getApplication(applicationName).getGuid(); + } + + @Override + public String getApplicationName(UUID applicationGuid) { + V3Application app = cc.get("/v3/apps/" + applicationGuid, V3Application.class); + return app == null ? null : app.name(); + } + + @Override + public Map getApplicationEnvironment(UUID applicationGuid) { + V3Application.V3EnvironmentVariables env = cc.get("/v3/apps/" + applicationGuid + "/environment_variables", + V3Application.V3EnvironmentVariables.class); + return env == null || env.var() == null ? Map.of() : env.var(); + } + + @Override + public Map getApplicationEnvironment(String applicationName) { + return getApplicationEnvironment(getApplicationGuid(applicationName)); + } + + @Override + public List getApplications() { + return listApplications(applicationsQuery(null)).stream() + .map(app -> V3ApplicationMapper.toCloudApplication(app, target)) + .toList(); + } + + @Override + public List getApplicationsByMetadataLabelSelector(String labelSelector) { + String query = applicationsQuery(null); + if (labelSelector != null) { + query = query + "&label_selector=" + labelSelector; + } + return listApplications(query).stream() + .map(app -> V3ApplicationMapper.toCloudApplication(app, target)) + .toList(); + } + + private V3Application findApplicationByName(String applicationName) { + List apps = listApplications(applicationsQuery(applicationName)); + return apps.isEmpty() ? null : apps.get(0); + } + + private List listApplications(String query) { + return cc.list(query, new ParameterizedTypeReference>() { + }); + } + + // Applications in the target space, optionally filtered by exact name. + private String applicationsQuery(String name) { + StringBuilder query = new StringBuilder("/v3/apps?per_page=" + DEFAULT_PAGE_SIZE); + if (target != null && target.getGuid() != null) { + query.append("&space_guids=") + .append(target.getGuid()); + } + if (name != null) { + query.append("&names=") + .append(name); + } + return query.toString(); + } + + @Override + public List getApplicationEvents(String applicationName) { + return eventsOps.getApplicationEvents(applicationName); + } + + @Override + public List getEventsByTarget(UUID uuid) { + return eventsOps.getEventsByTarget(uuid); + } + + @Override + public InstancesInfo getApplicationInstances(CloudApplication app) { + return processesOps.getApplicationInstances(app); + } + + @Override + public InstancesInfo getApplicationInstances(UUID applicationGuid) { + return processesOps.getApplicationInstances(applicationGuid); + } + + @Override + public CloudProcess getApplicationProcess(UUID applicationGuid) { + return processesOps.getApplicationProcess(applicationGuid); + } + + @Override + public List getApplicationRoutes(UUID applicationGuid) { + return routesOps.getApplicationRoutes(applicationGuid); + } + + @Override + public boolean getApplicationSshEnabled(UUID applicationGuid) { + return processesOps.getApplicationSshEnabled(applicationGuid); + } + + @Override + public Map getApplicationFeatures(UUID applicationGuid) { + return processesOps.getApplicationFeatures(applicationGuid); + } + + @Override + public CloudDomain getDefaultDomain() { + return domainsOps.getDefaultDomain(); + } + + @Override + public List getDomains() { + return domainsOps.getDomains(); + } + + @Override + public List getDomainsForOrganization() { + return domainsOps.getDomainsForOrganization(); + } + + @Override + public List getEvents() { + return eventsOps.getEvents(); + } + + @Override + public List getPrivateDomains() { + return domainsOps.getPrivateDomains(); + } + + @Override + public List getRoutes(String domainName) { + return routesOps.getRoutes(domainName); + } + + @Override + public UUID getRequiredServiceInstanceGuid(String name) { + return serviceInstancesOps.getRequiredServiceInstanceGuid(name); + } + + @Override + public CloudServiceInstance getServiceInstance(String serviceInstanceName) { + return serviceInstancesOps.getServiceInstance(serviceInstanceName); + } + + @Override + public CloudServiceInstance getServiceInstance(String serviceInstanceName, boolean required) { + return serviceInstancesOps.getServiceInstance(serviceInstanceName, required); + } + + @Override + public String getServiceInstanceName(UUID serviceInstanceGuid) { + return serviceInstancesOps.getServiceInstanceName(serviceInstanceGuid); + } + + @Override + public CloudServiceInstance getServiceInstanceWithoutAuxiliaryContent(String serviceInstanceName) { + return serviceInstancesOps.getServiceInstanceWithoutAuxiliaryContent(serviceInstanceName); + } + + @Override + public CloudServiceInstance getServiceInstanceWithoutAuxiliaryContent(String serviceInstanceName, boolean required) { + return serviceInstancesOps.getServiceInstanceWithoutAuxiliaryContent(serviceInstanceName, required); + } + + @Override + public CloudServiceBinding getServiceBinding(UUID serviceBindingGuid) { + return serviceBindingsOps.getServiceBinding(serviceBindingGuid); + } + + @Override + public List getServiceAppBindings(UUID serviceInstanceGuid) { + return serviceBindingsOps.getServiceAppBindings(serviceInstanceGuid); + } + + @Override + public List getAppBindings(UUID applicationGuid) { + return serviceBindingsOps.getAppBindings(applicationGuid); + } + + @Override + public List getServiceBindingsForApplication(UUID applicationId, UUID serviceInstanceGuid) { + return serviceBindingsOps.getServiceBindingsForApplication(applicationId, serviceInstanceGuid); + } + + @Override + public CloudServiceBroker getServiceBroker(String name) { + return serviceBrokersOps.getServiceBroker(name); + } + + @Override + public CloudServiceBroker getServiceBroker(String name, boolean required) { + return serviceBrokersOps.getServiceBroker(name, required); + } + + @Override + public List getServiceBrokers() { + return serviceBrokersOps.getServiceBrokers(); + } + + @Override + public CloudServiceKey getServiceKey(String serviceInstanceName, String serviceKeyName) { + return serviceKeysOps.getServiceKey(getServiceInstance(serviceInstanceName), serviceKeyName); + } + + @Override + public List getServiceKeys(String serviceInstanceName) { + return serviceKeysOps.getServiceKeys(getServiceInstance(serviceInstanceName)); + } + + @Override + public List getServiceKeysWithCredentials(String serviceInstanceName) { + return serviceKeysOps.getServiceKeysWithCredentials(getServiceInstance(serviceInstanceName)); + } + + @Override + public List getServiceKeys(CloudServiceInstance serviceInstance) { + return serviceKeysOps.getServiceKeys(serviceInstance); + } + + @Override + public List getServiceKeysWithCredentials(CloudServiceInstance serviceInstance) { + return serviceKeysOps.getServiceKeysWithCredentials(serviceInstance); + } + + @Override + public List getServiceOfferings() { + return serviceOfferingsOps.getServiceOfferings(); + } + + @Override + public List getSharedDomains() { + return domainsOps.getSharedDomains(); + } + + @Override + public CloudStack getStack(String name) { + return stacksOps.getStack(name); + } + + @Override + public CloudStack getStack(String name, boolean required) { + return stacksOps.getStack(name, required); + } + + @Override + public List getStacks() { + return stacksOps.getStacks(); + } + + @Override + public void startApplication(String applicationName) { + UUID guid = getApplicationGuid(applicationName); + cc.getRestClient() + .post() + .uri("/v3/apps/{guid}/actions/start", guid) + .retrieve() + .toBodilessEntity(); + } + + @Override + public void restartApplication(String applicationName) { + stopApplication(applicationName); + startApplication(applicationName); + } + + @Override + public void stopApplication(String applicationName) { + UUID guid = getApplicationGuid(applicationName); + cc.getRestClient() + .post() + .uri("/v3/apps/{guid}/actions/stop", guid) + .retrieve() + .toBodilessEntity(); + } + + @Override + public void rename(String applicationName, String newName) { + UUID guid = getApplicationGuid(applicationName); + cc.getRestClient() + .patch() + .uri("/v3/apps/{guid}", guid) + .body(Map.of("name", newName)) + .retrieve() + .toBodilessEntity(); + } + + @Override + public void updateApplicationInstances(String applicationName, int instances) { + scaleWebProcess(getApplicationGuid(applicationName), Map.of("instances", instances)); + } + + @Override + public void updateApplicationMemory(String applicationName, int memory) { + scaleWebProcess(getApplicationGuid(applicationName), Map.of("memory_in_mb", memory)); + } + + @Override + public void updateApplicationDiskQuota(String applicationName, int disk) { + scaleWebProcess(getApplicationGuid(applicationName), Map.of("disk_in_mb", disk)); + } + + @Override + public void updateApplicationEnv(String applicationName, Map env) { + UUID guid = getApplicationGuid(applicationName); + cc.getRestClient() + .patch() + .uri("/v3/apps/{guid}/environment_variables", guid) + .body(Map.of("var", env)) + .retrieve() + .toBodilessEntity(); + } + + @Override + public void bindDropletToApp(UUID dropletGuid, UUID applicationGuid) { + cc.getRestClient() + .patch() + .uri("/v3/apps/{guid}/relationships/current_droplet", applicationGuid) + .body(Map.of("data", Map.of("guid", dropletGuid.toString()))) + .retrieve() + .toBodilessEntity(); + } + + private void scaleWebProcess(UUID applicationGuid, Map scaleBody) { + cc.getRestClient() + .post() + .uri("/v3/apps/{guid}/processes/web/actions/scale", applicationGuid) + .body(scaleBody) + .retrieve() + .toBodilessEntity(); + } + + @Override + public List unbindServiceInstance(String applicationName, String serviceInstanceName) { + return serviceBindingsOps.unbindServiceInstance(applicationName, serviceInstanceName); + } + + @Override + public List unbindServiceInstance(UUID applicationGuid, UUID serviceInstanceGuid) { + return serviceBindingsOps.unbindServiceInstance(applicationGuid, serviceInstanceGuid); + } + + @Override + public void updateApplicationStaging(String applicationName, Staging staging) { + processesOps.updateApplicationStaging(applicationName, staging); + } + + @Override + public void updateApplicationRoutes(String applicationName, Set routes) { + routesOps.updateApplicationRoutes(applicationName, routes); + } + + @Override + public String updateServiceBroker(CloudServiceBroker serviceBroker) { + return serviceBrokersOps.updateServiceBroker(serviceBroker); + } + + @Override + public void updateServicePlanVisibilityForBroker(String name, ServicePlanVisibility visibility) { + serviceBrokersOps.updateServicePlanVisibilityForBroker(name, visibility); + } + + @Override + public void updateServicePlan(String serviceName, String planName) { + serviceInstancesOps.updateServicePlan(serviceName, planName); + } + + @Override + public void updateServiceParameters(String serviceName, Map parameters) { + serviceInstancesOps.updateServiceParameters(serviceName, parameters); + } + + @Override + public void updateServiceSyslogDrainUrl(String serviceName, String syslogDrainUrl) { + serviceInstancesOps.updateServiceSyslogDrainUrl(serviceName, syslogDrainUrl); + } + + @Override + public void updateServiceTags(String serviceName, List tags) { + serviceInstancesOps.updateServiceTags(serviceName, tags); + } + + @Override + public CloudPackage asyncUploadApplication(String applicationName, Path file, UploadStatusCallback callback, Duration uploadTimeout) { + return packagesOps.asyncUploadApplication(applicationName, file, callback, uploadTimeout); + } + + @Override + public Upload getUploadStatus(UUID packageGuid) { + return packagesOps.getUploadStatus(packageGuid); + } + + @Override + public CloudTask getTask(UUID taskGuid) { + return tasksOps.getTask(taskGuid); + } + + @Override + public List getTasks(String applicationName) { + return tasksOps.getTasks(applicationName); + } + + @Override + public CloudTask runTask(String applicationName, CloudTask task) { + return tasksOps.runTask(applicationName, task); + } + + @Override + public CloudTask cancelTask(UUID taskGuid) { + return tasksOps.cancelTask(taskGuid); + } + + @Override + public CloudBuild createBuild(UUID packageGuid) { + return buildsOps.createBuild(packageGuid); + } + + @Override + public CloudBuild getBuild(UUID packageGuid) { + return buildsOps.getBuild(packageGuid); + } + + @Override + public List getBuildsForApplication(UUID applicationGuid) { + return buildsOps.getBuildsForApplication(applicationGuid); + } + + @Override + public Map getServiceInstanceParameters(UUID guid) { + return serviceInstancesOps.getServiceInstanceParameters(guid); + } + + @Override + public Map getUserProvidedServiceInstanceParameters(UUID guid) { + return serviceInstancesOps.getUserProvidedServiceInstanceParameters(guid); + } + + @Override + public Map getServiceBindingParameters(UUID guid) { + return serviceBindingsOps.getServiceBindingParameters(guid); + } + + @Override + public List getBuildsForPackage(UUID packageGuid) { + return buildsOps.getBuildsForPackage(packageGuid); + } + + @Override + public List getServiceInstancesWithoutAuxiliaryContentByNames(List names) { + return serviceInstancesOps.getServiceInstancesWithoutAuxiliaryContentByNames(names); + } + + @Override + public List getServiceInstancesByMetadataLabelSelector(String labelSelector) { + return serviceInstancesOps.getServiceInstancesByMetadataLabelSelector(labelSelector); + } + + @Override + public List getServiceInstancesWithoutAuxiliaryContentByMetadataLabelSelector(String labelSelector) { + return serviceInstancesOps.getServiceInstancesWithoutAuxiliaryContentByMetadataLabelSelector(labelSelector); + } + + @Override + public void updateApplicationMetadata(UUID guid, Metadata metadata) { + cc.getRestClient() + .patch() + .uri("/v3/apps/{guid}", guid) + .body(Map.of("metadata", Map.of("labels", metadata.getLabels(), "annotations", metadata.getAnnotations()))) + .retrieve() + .toBodilessEntity(); + } + + @Override + public void updateServiceInstanceMetadata(UUID guid, Metadata metadata) { + serviceInstancesOps.updateServiceInstanceMetadata(guid, metadata); + } + + @Override + public void updateServiceBindingMetadata(UUID guid, Metadata metadata) { + serviceBindingsOps.updateServiceBindingMetadata(guid, metadata); + } + + @Override + public DropletInfo getCurrentDropletForApplication(UUID applicationGuid) { + return processesOps.getCurrentDropletForApplication(applicationGuid); + } + + @Override + public CloudPackage getPackage(UUID packageGuid) { + return packagesOps.getPackage(packageGuid); + } + + @Override + public List getPackagesForApplication(UUID applicationGuid) { + return packagesOps.getPackagesForApplication(applicationGuid); + } + + @Override + public Set getUserRolesBySpaceAndUser(UUID spaceGuid, UUID userGuid) { + return rolesOps.getUserRolesBySpaceAndUser(spaceGuid, userGuid); + } + + @Override + public CloudPackage createDockerPackage(UUID applicationGuid, DockerInfo dockerInfo) { + return packagesOps.createDockerPackage(applicationGuid, dockerInfo); + } + + @Override + public CloudAsyncJob getAsyncJob(String jobId) { + return jobsOps.getAsyncJob(jobId); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java new file mode 100644 index 0000000000..de312d1a0b --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java @@ -0,0 +1,191 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.net.URI; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudException; +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Job; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClient; + +/** + * Low-level Cloud Controller v3 access shared by every operation of {@link CloudControllerRestClientV3Impl}. This is the reusable + * machinery of the cf-java-client replacement: + *

    + *
  • typed {@code GET} / list helpers over the configured {@link RestClient} (error mapping is already attached to the RestClient via + * {@link CloudControllerResponseErrorHandler});
  • + *
  • a {@link #list(String, ParameterizedTypeReference) pagination walker} that follows {@code pagination.next.href} across pages;
  • + *
  • an {@link #waitForAsyncJob(String, Duration) async job poller} reproducing the OSS {@code JobV3Util} contract (exponential + * backoff 1s→15s, capped at a timeout, until COMPLETE/FAILED).
  • + *
+ * It deals in the thin {@code resources.*} wire types; mapping those to the {@code domain.Cloud*} model happens in the per-resource + * operations that call this. + */ +public class CloudControllerV3Client { + + private static final Logger LOGGER = LoggerFactory.getLogger(CloudControllerV3Client.class); + + // Mirrors the OSS JobV3Util backoff so async behaviour is unchanged. + private static final Duration JOB_POLL_MIN_INTERVAL = Duration.ofSeconds(1); + private static final Duration JOB_POLL_MAX_INTERVAL = Duration.ofSeconds(15); + private static final Duration DEFAULT_JOB_TIMEOUT = Duration.ofMinutes(5); + + private final RestClient restClient; + + public CloudControllerV3Client(RestClient restClient) { + this.restClient = restClient; + } + + public T get(String uri, Class responseType) { + LOGGER.warn("[cf-v3-poc] GET {}", uri); + return restClient.get() + .uri(uri) + .retrieve() + .body(responseType); + } + + /** + * GET a single resource, returning empty on a 404 rather than throwing — several callers rely on "not found" being a normal, + * non-exceptional outcome (the OSS impl expressed this with {@code onErrorResume} on NOT_FOUND). + */ + public Optional getOptional(String uri, Class responseType) { + try { + return Optional.ofNullable(get(uri, responseType)); + } catch (CloudOperationException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } + throw e; + } + } + + /** + * Walk every page of a v3 list endpoint, following {@code pagination.next.href} until it is {@code null}. The caller supplies the + * fully-formed first-page URI (path + query) and a type reference for the paged {@link V3ListResponse}. + */ + public List list(String firstPageUri, ParameterizedTypeReference> pageType) { + List all = new ArrayList<>(); + String nextUri = firstPageUri; + while (nextUri != null) { + LOGGER.warn("[cf-v3-poc] LIST {}", nextUri); + V3ListResponse page = restClient.get() + .uri(nextUri) + .retrieve() + .body(pageType); + if (page == null) { + break; + } + all.addAll(page.resources()); + nextUri = toRelativeIfAbsolute(page.nextPageHref()); + } + return all; + } + + /** + * Follow the {@code Location} header of a 202-Accepted async response to its job, then poll to completion. + */ + public void followAsyncJob(ResponseEntity acceptedResponse, Duration timeout) { + URI location = acceptedResponse.getHeaders() + .getLocation(); + if (location == null) { + return; // Operation completed synchronously (no job to poll). + } + waitForAsyncJob(extractJobGuid(location.toString()), timeout); + } + + public void waitForAsyncJob(String jobGuid) { + waitForAsyncJob(jobGuid, DEFAULT_JOB_TIMEOUT); + } + + /** + * Poll {@code GET /v3/jobs/{guid}} until the job reaches a terminal state, with exponential backoff bounded by {@code timeout}. + * Throws {@link CloudOperationException} if the job fails, or if it does not complete within the timeout — matching the OSS + * {@code JobV3Util} contract. + */ + public V3Job waitForAsyncJob(String jobGuid, Duration timeout) { + long deadlineNanos = System.nanoTime() + timeout.toNanos(); + Duration interval = JOB_POLL_MIN_INTERVAL; + while (true) { + V3Job job = get("/v3/jobs/" + jobGuid, V3Job.class); + if (job != null && job.isTerminal()) { + if (job.isFailed()) { + throw jobFailed(job); + } + return job; + } + if (System.nanoTime() >= deadlineNanos) { + throw new CloudOperationException(HttpStatus.GATEWAY_TIMEOUT, "Job timeout", + "Job " + jobGuid + " did not complete within " + timeout); + } + sleep(interval); + interval = nextInterval(interval); + } + } + + private static Duration nextInterval(Duration current) { + Duration doubled = current.multipliedBy(2); + return doubled.compareTo(JOB_POLL_MAX_INTERVAL) > 0 ? JOB_POLL_MAX_INTERVAL : doubled; + } + + private static void sleep(Duration interval) { + try { + Thread.sleep(interval.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread() + .interrupt(); + throw new CloudException("Interrupted while polling an async job", e); + } + } + + private static CloudOperationException jobFailed(V3Job job) { + String detail = "Job failed"; + if (job.errors() != null && !job.errors() + .isEmpty()) { + detail = job.errors() + .stream() + .map(V3Job.V3Error::detail) + .reduce((a, b) -> a + "\n" + b) + .orElse(detail); + } + return new CloudOperationException(HttpStatus.UNPROCESSABLE_ENTITY, "Job failed", detail); + } + + private static String extractJobGuid(String location) { + int idx = location.lastIndexOf("/v3/jobs/"); + if (idx < 0) { + return location.substring(location.lastIndexOf('/') + 1); + } + return location.substring(idx + "/v3/jobs/".length()); + } + + // The next.href CF returns is absolute; RestClient has a baseUrl, so strip the origin to a path+query to keep requests baseUrl-relative. + private String toRelativeIfAbsolute(String href) { + if (href == null) { + return null; + } + URI uri = URI.create(href); + if (uri.isAbsolute()) { + String pathAndQuery = uri.getRawPath(); + if (uri.getRawQuery() != null) { + pathAndQuery = pathAndQuery + "?" + uri.getRawQuery(); + } + return pathAndQuery; + } + return href; + } + + public RestClient getRestClient() { + return restClient; + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/DomainsV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/DomainsV3Operations.java new file mode 100644 index 0000000000..f80b4c2ff9 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/DomainsV3Operations.java @@ -0,0 +1,184 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudDomain; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEntity; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Domain; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3DomainMapper; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; + +/** + * CF v3 Domains operations of the in-house {@code CloudControllerRestClient}. Reproduces the HTTP shape, filtering and domain + * mapping of the OSS {@code CloudControllerRestClientImpl} domain methods on top of the shared {@link CloudControllerV3Client} machinery. + * + *

+ * Endpoint map (see {@code docs/cf-java-client-migration/02-endpoint-inventory.md}): + *

+ *
    + *
  • {@code addDomain} → {@code POST /v3/domains} (org-scoped, non-async)
  • + *
  • {@code deleteDomain} → {@code DELETE /v3/domains/{guid}} (async job)
  • + *
  • {@code getDefaultDomain} → {@code GET /v3/organizations/{guid}/domains/default}
  • + *
  • {@code getDomains} / {@code getSharedDomains} / {@code getPrivateDomains} → {@code GET /v3/domains} (paginated, filtered)
  • + *
  • {@code getDomainsForOrganization} → {@code GET /v3/organizations/{guid}/domains} (paginated)
  • + *
+ */ +public class DomainsV3Operations { + + private static final int PER_PAGE = 5000; + // Mirrors the OSS DELETE_JOB_TIMEOUT (Duration.ofMinutes(5)) used for domain/route/service deletes. + private static final Duration DELETE_JOB_TIMEOUT = Duration.ofMinutes(5); + + private static final ParameterizedTypeReference> DOMAIN_LIST_TYPE = new ParameterizedTypeReference<>() { + }; + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public DomainsV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + /** + * Create a private domain in the target organization, but only if a domain with that name does not already exist — matching the OSS + * {@code addDomain}, which no-ops when the domain is already present. + */ + public void addDomain(String domainName) { + assertSpaceProvided("add domain"); + CloudDomain domain = findDomainByName(domainName); + if (domain == null) { + doCreateDomain(domainName); + } + } + + /** + * Delete the (required) domain by name. Reproduces the OSS {@code deleteDomain}: a missing domain throws + * {@code CloudOperationException(NOT_FOUND)}, and the delete follows the async job to completion. + */ + public void deleteDomain(String domainName) { + assertSpaceProvided("delete domain"); + CloudDomain domain = findDomainByName(domainName, true); + doDeleteDomain(domain.getGuid()); + } + + /** + * {@code GET /v3/organizations/{guid}/domains/default} → {@link CloudDomain}. Non-paginated single fetch, mapping the OSS + * {@code getDefaultDomain}. + */ + public CloudDomain getDefaultDomain() { + V3Domain domain = cc.get("/v3/organizations/" + getTargetOrganizationGuid() + "/domains/default", V3Domain.class); + return V3DomainMapper.toCloudDomain(domain); + } + + /** + * All domains visible to the target ({@code GET /v3/domains}). + */ + public List getDomains() { + return getAllDomains().stream() + .map(V3DomainMapper::toCloudDomain) + .collect(Collectors.toList()); + } + + /** + * Shared domains only — those whose {@code relationships.organization.data} is {@code null}, matching the OSS + * {@code getSharedDomainResources} filter. + */ + public List getSharedDomains() { + return getAllDomains().stream() + .filter(domain -> !domain.isPrivate()) + .map(V3DomainMapper::toCloudDomain) + .collect(Collectors.toList()); + } + + /** + * Private domains only — those whose {@code relationships.organization.data} is present, matching the OSS + * {@code getPrivateDomainResources} filter. + */ + public List getPrivateDomains() { + return getAllDomains().stream() + .filter(V3Domain::isPrivate) + .map(V3DomainMapper::toCloudDomain) + .collect(Collectors.toList()); + } + + /** + * Domains available to the target organization ({@code GET /v3/organizations/{guid}/domains}), matching the OSS + * {@code getDomainsForOrganization}. + */ + public List getDomainsForOrganization() { + assertSpaceProvided("access organization domains"); + String uri = "/v3/organizations/" + getTargetOrganizationGuid() + "/domains?per_page=" + PER_PAGE; + return cc.list(uri, DOMAIN_LIST_TYPE) + .stream() + .map(V3DomainMapper::toCloudDomain) + .collect(Collectors.toList()); + } + + private List getAllDomains() { + return cc.list("/v3/domains?per_page=" + PER_PAGE, DOMAIN_LIST_TYPE); + } + + private CloudDomain findDomainByName(String name, boolean required) { + CloudDomain domain = findDomainByName(name); + if (domain == null && required) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Domain " + name + " not found."); + } + return domain; + } + + private CloudDomain findDomainByName(String name) { + String uri = "/v3/domains?names=" + name + "&per_page=" + PER_PAGE; + return cc.list(uri, DOMAIN_LIST_TYPE) + .stream() + .findFirst() + .map(V3DomainMapper::toCloudDomain) + .orElse(null); + } + + private void doCreateDomain(String name) { + cc.getRestClient() + .post() + .uri("/v3/domains") + .body(Map.of("name", name, "relationships", + Map.of("organization", Map.of("data", Map.of("guid", getTargetOrganizationGuid().toString()))))) + .retrieve() + .toBodilessEntity(); + } + + private void doDeleteDomain(UUID guid) { + ResponseEntity response = cc.getRestClient() + .delete() + .uri("/v3/domains/{guid}", guid.toString()) + .retrieve() + .toEntity(Void.class); + cc.followAsyncJob(response, DELETE_JOB_TIMEOUT); + } + + private UUID getTargetOrganizationGuid() { + return getGuid(target.getOrganization()); + } + + private UUID getGuid(CloudEntity entity) { + if (entity == null || entity.getMetadata() == null) { + return null; + } + return entity.getMetadata() + .getGuid(); + } + + private void assertSpaceProvided(String operation) { + Assert.notNull(target, "Unable to " + operation + " without specifying organization and space to use."); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/EventsV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/EventsV3Operations.java new file mode 100644 index 0000000000..1566597540 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/EventsV3Operations.java @@ -0,0 +1,83 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.util.List; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEvent; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Application; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3AuditEvent; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3AuditEventMapper; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; + +/** + * CF v3 audit event operations for the cf-java-client replacement. Reproduces the HTTP shape and domain-mapping of the OSS + * {@code CloudControllerRestClientImpl} event methods: + *
    + *
  • {@code getEvents()} → {@code GET /v3/audit_events} (paginated);
  • + *
  • {@code getEventsByTarget(uuid)} → {@code GET /v3/audit_events?target_guids=} (paginated);
  • + *
  • {@code getApplicationEvents(name)} → resolve the app guid in the target space, then {@code getEventsByTarget}.
  • + *
+ * Audit events are a global resource, so — like the OSS impl — the listings are not filtered by the target space (only by target guid where + * requested). Application-name resolution mirrors the OSS impl's {@code getRequiredApplicationGuid}: it queries the target space and throws + * {@link CloudOperationException} with {@link HttpStatus#NOT_FOUND} when the application does not exist. + */ +public class EventsV3Operations { + + // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). + private static final int DEFAULT_PAGE_SIZE = 5000; + + private static final ParameterizedTypeReference> EVENT_PAGE = new ParameterizedTypeReference<>() { + }; + private static final ParameterizedTypeReference> APPLICATION_PAGE = new ParameterizedTypeReference<>() { + }; + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public EventsV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + public List getEvents() { + return cc.list("/v3/audit_events?per_page=" + DEFAULT_PAGE_SIZE, EVENT_PAGE) + .stream() + .map(V3AuditEventMapper::toCloudEvent) + .toList(); + } + + public List getEventsByTarget(UUID uuid) { + String query = "/v3/audit_events?per_page=" + DEFAULT_PAGE_SIZE + "&target_guids=" + uuid; + return cc.list(query, EVENT_PAGE) + .stream() + .map(V3AuditEventMapper::toCloudEvent) + .toList(); + } + + public List getApplicationEvents(String applicationName) { + UUID applicationGuid = getRequiredApplicationGuid(applicationName); + return getEventsByTarget(applicationGuid); + } + + // Mirrors the OSS impl's getRequiredApplicationGuid: look up the app in the target space and 404 if it is absent. + private UUID getRequiredApplicationGuid(String applicationName) { + StringBuilder query = new StringBuilder("/v3/apps?per_page=" + DEFAULT_PAGE_SIZE); + if (target != null && target.getGuid() != null) { + query.append("&space_guids=") + .append(target.getGuid()); + } + query.append("&names=") + .append(applicationName); + List apps = cc.list(query.toString(), APPLICATION_PAGE); + if (apps.isEmpty()) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + applicationName + " not found."); + } + return UUID.fromString(apps.get(0) + .guid()); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/JobsV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/JobsV3Operations.java new file mode 100644 index 0000000000..b2048dc6b3 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/JobsV3Operations.java @@ -0,0 +1,31 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Job; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3JobMapper; + +/** + * CF v3 Jobs operations of the in-house {@code CloudControllerRestClient}. Reproduces the HTTP shape and domain mapping of the OSS + * {@code CloudControllerRestClientImpl#getAsyncJob} on top of the shared {@link CloudControllerV3Client} machinery. + */ +public class JobsV3Operations { + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public JobsV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + /** + * {@code GET /v3/jobs/{guid}} → {@link CloudAsyncJob}. Non-paginated, synchronous fetch. A missing job yields a + * {@code CloudOperationException(NOT_FOUND)}, matching the OSS {@code fetch(...)} contract (which does not swallow 404s here). + */ + public CloudAsyncJob getAsyncJob(String jobId) { + V3Job job = cc.get("/v3/jobs/" + jobId, V3Job.class); + return V3JobMapper.toCloudAsyncJob(job); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/PackagesV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/PackagesV3Operations.java new file mode 100644 index 0000000000..689e2a9305 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/PackagesV3Operations.java @@ -0,0 +1,212 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.UploadStatusCallback; +import org.cloudfoundry.multiapps.controller.client.facade.domain.BitsData; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudPackage; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.domain.DockerCredentials; +import org.cloudfoundry.multiapps.controller.client.facade.domain.DockerInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ErrorDetails; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableErrorDetails; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableUpload; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Status; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Upload; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Package; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3PackageMapper; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * CF v3 packages operations of the cf-java-client replacement. Reproduces the HTTP shape and domain mapping of the OSS + * {@code CloudControllerRestClientImpl} package methods: + *
    + *
  • {@code getPackage(guid)} → {@code GET /v3/packages/{guid}};
  • + *
  • {@code getPackagesForApplication(appGuid)} → {@code GET /v3/apps/{guid}/packages} (paginated);
  • + *
  • {@code createDockerPackage(appGuid, dockerInfo)} → {@code POST /v3/packages} then re-fetch;
  • + *
  • {@code asyncUploadApplication(...)} → create a bits package ({@code POST /v3/packages}), + * upload the bits multipart ({@code POST /v3/packages/{guid}/upload}), then poll status in a background thread;
  • + *
  • {@code getUploadStatus(guid)} → derived from {@code getPackage(guid)}.
  • + *
+ */ +public class PackagesV3Operations { + + // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). + private static final int DEFAULT_PAGE_SIZE = 5000; + // Mirrors the OSS CloudControllerRestClientImpl.PACKAGE_UPLOAD_JOB_POLLING_PERIOD. + private static final long PACKAGE_UPLOAD_JOB_POLLING_PERIOD = TimeUnit.SECONDS.toMillis(5); + + private static final ParameterizedTypeReference> PACKAGE_PAGE = new ParameterizedTypeReference<>() { + }; + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public PackagesV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + public CloudPackage getPackage(UUID packageGuid) { + V3Package resource = cc.get("/v3/packages/" + packageGuid, V3Package.class); + return resource == null ? null : V3PackageMapper.toCloudPackage(resource); + } + + public List getPackagesForApplication(UUID applicationGuid) { + return cc.list("/v3/apps/" + applicationGuid + "/packages?per_page=" + DEFAULT_PAGE_SIZE, PACKAGE_PAGE) + .stream() + .map(V3PackageMapper::toCloudPackage) + .toList(); + } + + public CloudPackage createDockerPackage(UUID applicationGuid, DockerInfo dockerInfo) { + java.util.Map data = new java.util.HashMap<>(); + data.put("image", dockerInfo.getImage()); + DockerCredentials credentials = dockerInfo.getCredentials(); + if (credentials != null) { + if (credentials.getUsername() != null) { + data.put("username", credentials.getUsername()); + } + if (credentials.getPassword() != null) { + data.put("password", credentials.getPassword()); + } + } + V3Package created = cc.getRestClient() + .post() + .uri("/v3/packages") + .body(java.util.Map.of("type", "docker", "data", data, "relationships", + applicationRelationship(applicationGuid))) + .retrieve() + .body(V3Package.class); + return getPackage(UUID.fromString(created.guid())); + } + + public Upload getUploadStatus(UUID packageGuid) { + CloudPackage cloudPackage = getPackage(packageGuid); + ErrorDetails errorDetails = null; + if (cloudPackage.getType() == CloudPackage.Type.BITS) { + errorDetails = ImmutableErrorDetails.builder() + .description(((BitsData) cloudPackage.getData()).getError()) + .build(); + } + return ImmutableUpload.builder() + .status(cloudPackage.getStatus()) + .errorDetails(errorDetails) + .build(); + } + + public CloudPackage asyncUploadApplication(String applicationName, Path file, UploadStatusCallback callback, Duration uploadTimeout) { + CloudPackage cloudPackage = startUpload(applicationName, file, uploadTimeout); + processAsyncUploadInBackground(cloudPackage, callback); + return cloudPackage; + } + + private CloudPackage startUpload(String applicationName, Path file, Duration uploadTimeout) { + Assert.notNull(applicationName, "AppName must not be null"); + Assert.notNull(file, "File must not be null"); + + UUID applicationGuid = getRequiredApplicationGuid(applicationName); + UUID packageGuid = createBitsPackage(applicationGuid).getGuid(); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("bits", new FileSystemResource(file)); + cc.getRestClient() + .post() + .uri("/v3/packages/{guid}/upload", packageGuid) + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(body) + .retrieve() + .toBodilessEntity(); + + return getPackage(packageGuid); + } + + private CloudPackage createBitsPackage(UUID applicationGuid) { + V3Package created = cc.getRestClient() + .post() + .uri("/v3/packages") + .body(java.util.Map.of("type", "bits", "relationships", applicationRelationship(applicationGuid))) + .retrieve() + .body(V3Package.class); + return getPackage(UUID.fromString(created.guid())); + } + + private static java.util.Map applicationRelationship(UUID applicationGuid) { + return java.util.Map.of("app", java.util.Map.of("data", java.util.Map.of("guid", applicationGuid.toString()))); + } + + private UUID getRequiredApplicationGuid(String applicationName) { + StringBuilder query = new StringBuilder("/v3/apps?per_page=" + DEFAULT_PAGE_SIZE); + if (target != null && target.getGuid() != null) { + query.append("&space_guids=") + .append(target.getGuid()); + } + query.append("&names=") + .append(applicationName); + List apps = cc.list(query.toString(), new ParameterizedTypeReference>() { + }); + if (apps.isEmpty() || apps.get(0) + .guid() == null) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + applicationName + " not found."); + } + return UUID.fromString(apps.get(0) + .guid()); + } + + private void processAsyncUploadInBackground(CloudPackage cloudPackage, UploadStatusCallback callback) { + String threadName = String.format("App upload monitor: %s", cloudPackage.getGuid()); + new Thread(() -> processAsyncUpload(cloudPackage, callback), threadName).start(); + } + + private void processAsyncUpload(CloudPackage cloudPackage, UploadStatusCallback callback) { + while (true) { + Upload upload = getUploadStatus(cloudPackage.getGuid()); + Status uploadStatus = upload.getStatus(); + boolean unsubscribe = callback.onProgress(uploadStatus.toString()); + if (unsubscribe || isUploadReady(uploadStatus)) { + return; + } + if (hasUploadFailed(uploadStatus)) { + callback.onError(upload.getErrorDetails() + .getDescription()); + return; + } + try { + Thread.sleep(PACKAGE_UPLOAD_JOB_POLLING_PERIOD); + } catch (InterruptedException e) { + Thread.currentThread() + .interrupt(); + return; + } + } + } + + private boolean isUploadReady(Status status) { + return status == Status.READY; + } + + private boolean hasUploadFailed(Status status) { + return status == Status.EXPIRED || status == Status.FAILED; + } + + /** + * Minimal wire-model of a v3 app resource, used only to resolve an application's GUID by name when starting an upload. The + * fully-featured app wire model lives in {@code V3Application}; this local record keeps the packages helper self-contained. + */ + @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) + private record V3App(@com.fasterxml.jackson.annotation.JsonProperty("guid") String guid) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ProcessesV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ProcessesV3Operations.java new file mode 100644 index 0000000000..def82d3124 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ProcessesV3Operations.java @@ -0,0 +1,237 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudProcess; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.domain.DropletInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableDropletInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableInstancesInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.InstancesInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Application; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Droplet; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3InstancesInfoMapper; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Process; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ProcessMapper; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; + +/** + * CF v3 process-family operations for the cf-java-client replacement. Reproduces the HTTP shape and domain mapping of the + * OSS {@code CloudControllerRestClientImpl} process/instance/feature/droplet methods: + *
    + *
  • {@code getApplicationProcess(appGuid)} → {@code GET /v3/apps/{guid}/processes/web};
  • + *
  • {@code getApplicationInstances(app)} / {@code getApplicationInstances(appGuid)} → + * {@code GET /v3/apps/{guid}/processes/web/stats} (empty for a non-STARTED app);
  • + *
  • {@code getApplicationSshEnabled(appGuid)} → {@code GET /v3/apps/{guid}/ssh_enabled} (default {@code false});
  • + *
  • {@code getApplicationFeatures(appGuid)} → {@code GET /v3/apps/{guid}/features} (paginated);
  • + *
  • {@code getCurrentDropletForApplication(appGuid)} → {@code GET /v3/apps/{guid}/droplets/current} (404 → + * {@link CloudOperationException});
  • + *
  • {@code updateApplicationStaging(appName, staging)} → {@code PATCH /v3/apps/{guid}} (lifecycle) then update the web process + * ({@code PATCH /v3/apps/{guid}/features/{name}} per feature and {@code PATCH /v3/processes/{guid}}).
  • + *
+ */ +public class ProcessesV3Operations { + + // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). + private static final int DEFAULT_PAGE_SIZE = 5000; + // The "package" link key in the current-droplet response; the package GUID is the last path segment of its href. + private static final String PACKAGE_LINK = "package"; + private static final String WEB_PROCESS_TYPE = "web"; + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public ProcessesV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + public CloudProcess getApplicationProcess(UUID applicationGuid) { + V3Process process = cc.get("/v3/apps/" + applicationGuid + "/processes/" + WEB_PROCESS_TYPE, V3Process.class); + return process == null ? null : V3ProcessMapper.toCloudProcess(process); + } + + public InstancesInfo getApplicationInstances(CloudApplication application) { + if (application.getState() + .equals(CloudApplication.State.STARTED)) { + return findApplicationInstances(application.getGuid()); + } + return ImmutableInstancesInfo.builder() + .instances(Collections.emptyList()) + .build(); + } + + public InstancesInfo getApplicationInstances(UUID applicationGuid) { + return findApplicationInstances(applicationGuid); + } + + private InstancesInfo findApplicationInstances(UUID applicationGuid) { + V3Process.V3ProcessStats stats = cc.get("/v3/apps/" + applicationGuid + "/processes/" + WEB_PROCESS_TYPE + "/stats", + V3Process.V3ProcessStats.class); + return V3InstancesInfoMapper.toInstancesInfo(stats); + } + + public boolean getApplicationSshEnabled(UUID applicationGuid) { + V3Process.V3SshEnabled sshEnabled = cc.get("/v3/apps/" + applicationGuid + "/ssh_enabled", V3Process.V3SshEnabled.class); + if (sshEnabled == null || sshEnabled.enabled() == null) { + return false; + } + return sshEnabled.enabled(); + } + + public Map getApplicationFeatures(UUID applicationGuid) { + List features = cc.list("/v3/apps/" + applicationGuid + "/features?per_page=" + DEFAULT_PAGE_SIZE, + new ParameterizedTypeReference>() { + }); + Map result = new HashMap<>(); + for (V3Process.V3AppFeature feature : features) { + result.put(feature.name(), feature.enabled()); + } + return result; + } + + public DropletInfo getCurrentDropletForApplication(UUID applicationGuid) { + V3Droplet droplet = cc.get("/v3/apps/" + applicationGuid + "/droplets/current", V3Droplet.class); + if (droplet == null) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not found", + "Application with guid " + applicationGuid + " does not have a droplet"); + } + return parseDropletInfo(droplet); + } + + private DropletInfo parseDropletInfo(V3Droplet droplet) { + String packageUrl = droplet.links() + .get(PACKAGE_LINK) + .href(); + if (packageUrl.endsWith("/")) { + packageUrl = packageUrl.substring(0, packageUrl.lastIndexOf("/")); + } + String packageGuid = packageUrl.substring(packageUrl.lastIndexOf("/") + 1); + return ImmutableDropletInfo.builder() + .guid(UUID.fromString(droplet.guid())) + .packageGuid(UUID.fromString(packageGuid)) + .build(); + } + + public void updateApplicationStaging(String applicationName, Staging staging) { + UUID applicationGuid = getRequiredApplicationGuid(applicationName); + cc.getRestClient() + .patch() + .uri("/v3/apps/{guid}", applicationGuid) + .body(Map.of("lifecycle", buildApplicationLifecycle(staging))) + .retrieve() + .toBodilessEntity(); + updateApplicationProcess(applicationGuid, staging); + } + + // Mirrors the OSS impl's buildApplicationLifecycle: Docker if DockerInfo present, otherwise a buildpack (or CNB) lifecycle. + private Map buildApplicationLifecycle(Staging staging) { + if (staging.getDockerInfo() != null) { + return Map.of("type", "docker", "data", Map.of()); + } + String lifecycleType = staging.getLifecycleType() != null ? staging.getLifecycleType() + .name() + .toLowerCase() + : "buildpack"; + if ("cnb".equals(lifecycleType) && (staging.getBuildpacks() == null || staging.getBuildpacks() + .isEmpty())) { + throw new IllegalArgumentException("Buildpacks are required for the CNB lifecycle type"); + } + Map data = new HashMap<>(); + if (staging.getStackName() != null) { + data.put("stack", staging.getStackName()); + } + if (staging.getBuildpacks() != null) { + data.put("buildpacks", staging.getBuildpacks()); + } + return Map.of("type", lifecycleType, "data", data); + } + + // Mirrors the OSS impl's updateApplicationProcess: toggle app features, then PATCH the web process command + health checks. + private void updateApplicationProcess(UUID applicationGuid, Staging staging) { + staging.getAppFeatures() + .forEach((featureName, enabled) -> updateAppFeature(applicationGuid, featureName, enabled)); + V3Process process = cc.get("/v3/apps/" + applicationGuid + "/processes/" + WEB_PROCESS_TYPE, V3Process.class); + Map updateProcessBody = new HashMap<>(); + updateProcessBody.put("command", staging.getCommand()); + if (staging.getHealthCheckType() != null) { + updateProcessBody.put("health_check", buildHealthCheck(staging)); + } + if (staging.getReadinessHealthCheckType() != null) { + updateProcessBody.put("readiness_health_check", buildReadinessHealthCheck(staging)); + } + cc.getRestClient() + .patch() + .uri("/v3/processes/{guid}", process.guid()) + .body(updateProcessBody) + .retrieve() + .toBodilessEntity(); + } + + private void updateAppFeature(UUID applicationGuid, String featureName, boolean enabled) { + cc.getRestClient() + .patch() + .uri("/v3/apps/{guid}/features/{name}", applicationGuid, featureName) + .body(Map.of("enabled", enabled)) + .retrieve() + .toBodilessEntity(); + } + + private Map buildHealthCheck(Staging staging) { + Map data = new HashMap<>(); + putIfNotNull(data, "endpoint", staging.getHealthCheckHttpEndpoint()); + putIfNotNull(data, "timeout", staging.getHealthCheckTimeout()); + putIfNotNull(data, "invocation_timeout", staging.getInvocationTimeout()); + putIfNotNull(data, "interval", staging.getHealthCheckInterval()); + Map healthCheck = new HashMap<>(); + healthCheck.put("type", staging.getHealthCheckType()); + healthCheck.put("data", data); + return healthCheck; + } + + private Map buildReadinessHealthCheck(Staging staging) { + Map data = new HashMap<>(); + putIfNotNull(data, "invocation_timeout", staging.getReadinessHealthCheckInvocationTimeout()); + putIfNotNull(data, "endpoint", staging.getReadinessHealthCheckHttpEndpoint()); + putIfNotNull(data, "interval", staging.getReadinessHealthCheckInterval()); + Map readinessHealthCheck = new HashMap<>(); + readinessHealthCheck.put("type", staging.getReadinessHealthCheckType()); + readinessHealthCheck.put("data", data); + return readinessHealthCheck; + } + + private static void putIfNotNull(Map map, String key, Object value) { + if (value != null) { + map.put(key, value); + } + } + + // Mirrors the OSS impl's getRequiredApplicationGuid: look up the app in the target space and 404 if it is absent. + private UUID getRequiredApplicationGuid(String applicationName) { + StringBuilder query = new StringBuilder("/v3/apps?per_page=" + DEFAULT_PAGE_SIZE); + if (target != null && target.getGuid() != null) { + query.append("&space_guids=") + .append(target.getGuid()); + } + query.append("&names=") + .append(applicationName); + List apps = cc.list(query.toString(), + new ParameterizedTypeReference>() { + }); + if (apps.isEmpty()) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + applicationName + " not found."); + } + return UUID.fromString(apps.get(0) + .guid()); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RolesV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RolesV3Operations.java new file mode 100644 index 0000000000..9a1102b6d6 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RolesV3Operations.java @@ -0,0 +1,46 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.util.EnumSet; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.domain.UserRole; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Role; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3RoleMapper; +import org.springframework.core.ParameterizedTypeReference; + +/** + * CF v3 {@code Roles} operations for the in-house {@link CloudControllerRestClient} replacement. Implements the role-related methods of + * the interface by listing {@code GET /v3/roles} filtered by space and user, and mapping each wire {@link V3Role} to the project's + * {@link UserRole} domain enum via {@link V3RoleMapper}. + * + *

+ * Mirrors the OSS {@code CloudControllerRestClientImpl#getUserRolesBySpaceAndUser}, which lists roles filtered by {@code space_id}, + * {@code user_id} and all role {@code types}, then collects the results into an {@link EnumSet}. + *

+ */ +public class RolesV3Operations { + + private static final ParameterizedTypeReference> ROLE_PAGE = new ParameterizedTypeReference<>() { + }; + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public RolesV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + public Set getUserRolesBySpaceAndUser(UUID spaceGuid, UUID userGuid) { + String uri = "/v3/roles?per_page=5000&space_guids=" + spaceGuid + "&user_guids=" + userGuid; + return cc.list(uri, ROLE_PAGE) + .stream() + .map(V3RoleMapper::toUserRole) + .collect(Collectors.toCollection(() -> EnumSet.noneOf(UserRole.class))); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RoutesV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RoutesV3Operations.java new file mode 100644 index 0000000000..ee9df786f2 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RoutesV3Operations.java @@ -0,0 +1,362 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudRoute; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.domain.RouteDestination; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Domain; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Route; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3RouteMapper; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * CF v3 routes operations of the cf-java-client replacement. Reproduces the HTTP shape, async handling and domain mapping of the + * OSS {@code CloudControllerRestClientImpl} route methods: + *
    + *
  • {@code addRoute(host, domainName, path)} → resolve the domain, then {@code POST /v3/routes} (idempotent: reuse an existing + * route if one already matches);
  • + *
  • {@code deleteRoute(host, domainName, path)} → resolve the route in the target space and {@code DELETE /v3/routes/{guid}} + * (async job), 404 if not found;
  • + *
  • {@code deleteOrphanedRoutes()} → {@code DELETE /v3/spaces/{guid}/routes?unmapped=true} (async job);
  • + *
  • {@code getRoutes(domainName)} → {@code GET /v3/routes?domain_guids=…&space_guids=…} (paginated);
  • + *
  • {@code getApplicationRoutes(appGuid)} → {@code GET /v3/apps/{guid}/routes} (paginated);
  • + *
  • {@code updateApplicationRoutes(appName, routes)} → diff current vs requested destinations, then insert/remove destinations.
  • + *
+ * All listings are scoped to the target space exactly where the OSS impl scopes them. + */ +public class RoutesV3Operations { + + private static final ParameterizedTypeReference> ROUTE_PAGE = new ParameterizedTypeReference<>() { + }; + private static final ParameterizedTypeReference> DOMAIN_PAGE = new ParameterizedTypeReference<>() { + }; + + // CF v3 caps per_page at 5000; matches the other V3 operations. + private static final int DEFAULT_PAGE_SIZE = 5000; + // Mirrors the OSS DELETE_JOB_TIMEOUT. + private static final Duration DELETE_JOB_TIMEOUT = Duration.ofMinutes(5); + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public RoutesV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + public void addRoute(String host, String domainName, String path) { + assertSpaceProvided("add route for domain"); + UUID domainGuid = getRequiredDomainGuid(domainName); + doAddRoute(domainGuid, host, path); + } + + public void deleteRoute(String host, String domainName, String path) { + assertSpaceProvided("delete route for domain"); + UUID routeGuid = getRouteGuid(getRequiredDomainGuid(domainName), host, path); + if (routeGuid == null) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", + "Host " + host + " not found for domain " + domainName + "."); + } + doDeleteRoute(routeGuid); + } + + public void deleteOrphanedRoutes() { + ResponseEntity response = cc.getRestClient() + .delete() + .uri("/v3/spaces/{guid}/routes?unmapped=true", getTargetSpaceGuid()) + .retrieve() + .toEntity(Void.class); + cc.followAsyncJob(response, DELETE_JOB_TIMEOUT); + } + + public List getRoutes(String domainName) { + assertSpaceProvided("get routes for domain"); + UUID domainGuid = getRequiredDomainGuid(domainName); + return findRoutesByDomainGuid(domainGuid); + } + + public List getApplicationRoutes(UUID applicationGuid) { + return listRoutes("/v3/apps/" + applicationGuid + "/routes?per_page=" + DEFAULT_PAGE_SIZE).stream() + .map(route -> V3RouteMapper.toCloudRoute( + route, applicationGuid)) + .toList(); + } + + public void updateApplicationRoutes(String applicationName, Set updatedRoutes) { + UUID applicationGuid = getRequiredApplicationGuid(applicationName); + List appRoutes = listRoutes( + "/v3/apps/" + applicationGuid + "/routes?per_page=" + DEFAULT_PAGE_SIZE).stream() + .map(V3RouteMapper::toCloudRoute) + .toList(); + Set outdatedRoutes = getOutdatedRoutes(applicationGuid, appRoutes, updatedRoutes); + Set newRoutes = getNewRoutes(applicationGuid, appRoutes, updatedRoutes); + removeRoutes(outdatedRoutes, applicationGuid); + addRoutes(newRoutes, applicationGuid); + } + + // --- route listing helpers ------------------------------------------------------------------- + + private List findRoutesByDomainGuid(UUID domainGuid) { + StringBuilder query = new StringBuilder("/v3/routes?per_page=" + DEFAULT_PAGE_SIZE); + query.append("&domain_guids=") + .append(domainGuid); + query.append("&space_guids=") + .append(getTargetSpaceGuid()); + return listRoutes(query.toString()).stream() + .map(V3RouteMapper::toCloudRoute) + .toList(); + } + + private UUID getRouteGuid(UUID domainGuid, String host, String path) { + StringBuilder query = new StringBuilder("/v3/routes?per_page=" + DEFAULT_PAGE_SIZE); + query.append("&domain_guids=") + .append(domainGuid); + query.append("&space_guids=") + .append(getTargetSpaceGuid()); + if (host != null) { + query.append("&hosts=") + .append(host); + } + if (path != null) { + query.append("&paths=") + .append(path); + } + List routes = listRoutes(query.toString()); + if (CollectionUtils.isEmpty(routes)) { + return null; + } + return UUID.fromString(routes.get(0) + .guid()); + } + + private List listRoutes(String query) { + return cc.list(query, ROUTE_PAGE); + } + + // --- add / bind / remove --------------------------------------------------------------------- + + private void addRoutes(Set routes, UUID applicationGuid) { + Map domains = getDomainsFromRoutes(routes); + for (CloudRoute route : routes) { + validateDomainForRoute(route, domains); + UUID domainGuid = domains.get(route.getDomain() + .getName()); + UUID routeGuid = getOrAddRoute(domainGuid, route.getHost(), route.getPath()); + bindRoute(routeGuid, applicationGuid, route.getRequestedProtocol()); + } + } + + private void removeRoutes(Set routes, UUID applicationGuid) { + for (CloudRoute route : routes) { + for (RouteDestination destination : route.getDestinations()) { + if (destination.getApplicationGuid() + .equals(applicationGuid)) { + unbindRoute(route.getGuid(), destination.getGuid()); + } + } + } + } + + private UUID getOrAddRoute(UUID domainGuid, String host, String path) { + UUID routeGuid = getRouteGuid(domainGuid, host, path); + if (routeGuid == null) { + routeGuid = doAddRoute(domainGuid, host, path); + } + return routeGuid; + } + + private UUID doAddRoute(UUID domainGuid, String host, String path) { + assertSpaceProvided("add route"); + V3Route created = cc.getRestClient() + .post() + .uri("/v3/routes") + .body(Map.of("host", host == null ? "" : host, "path", path == null ? "" : path, "relationships", + Map.of("domain", toOneRelationship(domainGuid), "space", + toOneRelationship(getTargetSpaceGuid())))) + .retrieve() + .body(V3Route.class); + return UUID.fromString(created.guid()); + } + + private void doDeleteRoute(UUID guid) { + ResponseEntity response = cc.getRestClient() + .delete() + .uri("/v3/routes/{guid}", guid) + .retrieve() + .toEntity(Void.class); + cc.followAsyncJob(response, DELETE_JOB_TIMEOUT); + } + + private void bindRoute(UUID routeGuid, UUID applicationGuid, String protocol) { + cc.getRestClient() + .post() + .uri("/v3/routes/{guid}/destinations", routeGuid) + .body(Map.of("destinations", List.of(createDestination(applicationGuid, protocol)))) + .retrieve() + .toBodilessEntity(); + } + + private void unbindRoute(UUID routeGuid, UUID destinationGuid) { + cc.getRestClient() + .delete() + .uri("/v3/routes/{routeGuid}/destinations/{destinationGuid}", routeGuid, destinationGuid) + .retrieve() + .toBodilessEntity(); + } + + private Map createDestination(UUID applicationGuid, String protocol) { + if (protocol == null) { + return Map.of("app", Map.of("guid", applicationGuid.toString())); + } + return Map.of("app", Map.of("guid", applicationGuid.toString()), "protocol", protocol); + } + + private static Map toOneRelationship(UUID guid) { + return Map.of("data", Map.of("guid", guid.toString())); + } + + // --- diffing (mirror of the OSS impl) -------------------------------------------------------- + + private Set getOutdatedRoutes(UUID applicationGuid, List currentRoutes, Set updatedRoutes) { + return currentRoutes.stream() + .filter(currentRoute -> isRouteOutdated(applicationGuid, currentRoute, updatedRoutes)) + .collect(Collectors.toSet()); + } + + private boolean isRouteOutdated(UUID applicationGuid, CloudRoute currentRoute, Set updatedRoutes) { + Optional updatedRoute = findRoute(currentRoute.getUrl(), updatedRoutes); + if (updatedRoute.isEmpty()) { + return true; + } + return isProtocolChanged(applicationGuid, currentRoute, updatedRoute.get()); + } + + private Set getNewRoutes(UUID applicationGuid, List currentRoutes, Set updatedRoutes) { + return updatedRoutes.stream() + .filter(updatedRoute -> isRouteUpdated(applicationGuid, updatedRoute, currentRoutes)) + .collect(Collectors.toSet()); + } + + private boolean isRouteUpdated(UUID applicationGuid, CloudRoute updatedRoute, List currentRoutes) { + Optional currentRoute = findRoute(updatedRoute.getUrl(), currentRoutes); + if (currentRoute.isEmpty()) { + return true; + } + return isProtocolChanged(applicationGuid, currentRoute.get(), updatedRoute); + } + + private Optional findRoute(String url, java.util.Collection routes) { + return routes.stream() + .filter(route -> Objects.equals(url, route.getUrl())) + .findFirst(); + } + + private boolean isProtocolChanged(UUID applicationGuid, CloudRoute currentRoute, CloudRoute updatedRoute) { + if (updatedRoute.getRequestedProtocol() == null) { + return false; + } + return currentRoute.getDestinations() + .stream() + .filter(routeDestination -> Objects.equals(routeDestination.getApplicationGuid(), applicationGuid)) + .noneMatch(routeDestination -> Objects.equals(routeDestination.getProtocol(), + updatedRoute.getRequestedProtocol())); + } + + private void validateDomainForRoute(CloudRoute route, Map existingDomains) { + String domain = route.getDomain() + .getName(); + if (!StringUtils.hasLength(domain) || !existingDomains.containsKey(domain)) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), + "Domain '" + domain + "' not found for URI " + route.getUrl()); + } + } + + // --- domain resolution (minimal inline lookups; the Domains group owns the full wire model) -- + + private UUID getRequiredDomainGuid(String name) { + UUID guid = findDomainGuidByName(name); + if (guid == null) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Domain " + name + " not found."); + } + return guid; + } + + private UUID findDomainGuidByName(String name) { + List domains = cc.list("/v3/domains?names=" + name + "&per_page=" + DEFAULT_PAGE_SIZE, DOMAIN_PAGE); + if (CollectionUtils.isEmpty(domains)) { + return null; + } + return UUID.fromString(domains.get(0) + .guid()); + } + + private Map getDomainsFromRoutes(Set routes) { + Set domainNames = routes.stream() + .map(route -> route.getDomain() + .getName()) + .filter(StringUtils::hasLength) + .collect(Collectors.toSet()); + if (domainNames.isEmpty()) { + return Map.of(); + } + String names = String.join(",", domainNames); + return cc.list("/v3/domains?names=" + names + "&per_page=" + DEFAULT_PAGE_SIZE, DOMAIN_PAGE) + .stream() + .collect(Collectors.toMap(V3Domain::name, domain -> UUID.fromString(domain.guid()))); + } + + // --- application resolution ------------------------------------------------------------------ + + private UUID getRequiredApplicationGuid(String applicationName) { + StringBuilder query = new StringBuilder("/v3/apps?per_page=" + DEFAULT_PAGE_SIZE); + query.append("&names=") + .append(applicationName); + if (target != null && target.getGuid() != null) { + query.append("&space_guids=") + .append(target.getGuid()); + } + List apps = cc.list(query.toString(), new ParameterizedTypeReference>() { + }); + if (CollectionUtils.isEmpty(apps)) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + applicationName + " not found."); + } + return UUID.fromString(apps.get(0) + .guid()); + } + + private UUID getTargetSpaceGuid() { + return target.getGuid(); + } + + private void assertSpaceProvided(String operation) { + if (target == null) { + throw new IllegalArgumentException("Unable to " + operation + " without specifying organization and space to use."); + } + } + + /** + * Minimal wire-model of a CF v3 application resource — only the {@code guid} needed to resolve an application by name. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + private record V3AppRef(@JsonProperty("guid") String guid) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBindingsV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBindingsV3Operations.java new file mode 100644 index 0000000000..25e04c2831 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBindingsV3Operations.java @@ -0,0 +1,269 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBinding; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Application; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceBinding; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceBindingMapper; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.CollectionUtils; + +/** + * CF v3 service credential bindings (application bindings) operations of the in-house {@code CloudControllerRestClient}. + * Reproduces the HTTP shape, filtering, async handling and domain mapping of the OSS {@code CloudControllerRestClientImpl} binding + * methods on top of the shared {@link CloudControllerV3Client} machinery. + * + *

+ * Endpoint map (see {@code docs/cf-java-client-migration/02-endpoint-inventory.md}): + *

+ *
    + *
  • {@code bindServiceInstance} → {@code POST /v3/service_credential_bindings} (type=app, async job)
  • + *
  • {@code unbindServiceInstance} / {@code deleteServiceBinding} → {@code DELETE /v3/service_credential_bindings/{guid}} (async + * job)
  • + *
  • {@code getServiceBinding} → {@code GET /v3/service_credential_bindings?guids={guid}} (single)
  • + *
  • {@code getServiceAppBindings} → {@code GET /v3/service_credential_bindings?service_instance_guids=…&type=app} (paginated)
  • + *
  • {@code getAppBindings} → {@code GET /v3/service_credential_bindings?app_guids=…} (paginated)
  • + *
  • {@code getServiceBindingsForApplication} → {@code GET /v3/service_credential_bindings?app_guids=…&service_instance_guids=…} + * (paginated)
  • + *
  • {@code getServiceBindingParameters} → {@code GET /v3/service_credential_bindings/{guid}/parameters}
  • + *
  • {@code updateServiceBindingMetadata} → {@code PATCH /v3/service_credential_bindings/{guid}}
  • + *
+ * + *

+ * As in the OSS impl, the async create/delete operations do not block on the job: they return the job GUID (extracted from the + * {@code Location} header of the 202 response) so the caller can poll it. This matches + * {@code delegate.serviceBindingsV3().create(...).getJobId()} / {@code .delete(...).block()} which yield the job id without waiting. + *

+ */ +public class ServiceBindingsV3Operations { + + // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). + private static final int PER_PAGE = 5000; + + private static final ParameterizedTypeReference> BINDING_LIST_TYPE = new ParameterizedTypeReference<>() { + }; + private static final ParameterizedTypeReference> APPLICATION_LIST_TYPE = new ParameterizedTypeReference<>() { + }; + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public ServiceBindingsV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + public Optional bindServiceInstance(String bindingName, String applicationName, String serviceInstanceName) { + return bindServiceInstance(bindingName, applicationName, serviceInstanceName, null); + } + + public Optional bindServiceInstance(String bindingName, String applicationName, String serviceInstanceName, + Map parameters) { + UUID applicationGuid = getRequiredApplicationGuid(applicationName); + UUID serviceInstanceGuid = getRequiredServiceInstanceGuid(serviceInstanceName); + + Map body = new HashMap<>(); + body.put("name", bindingName); + body.put("type", "app"); + body.put("relationships", Map.of("app", toOneRelationship(applicationGuid), "service_instance", + toOneRelationship(serviceInstanceGuid))); + if (!CollectionUtils.isEmpty(parameters)) { + body.put("parameters", parameters); + } + + ResponseEntity response = cc.getRestClient() + .post() + .uri("/v3/service_credential_bindings") + .body(body) + .retrieve() + .toBodilessEntity(); + return extractJobGuid(response); + } + + public List unbindServiceInstance(String applicationName, String serviceInstanceName) { + UUID applicationGuid = getRequiredApplicationGuid(applicationName); + UUID serviceInstanceGuid = getRequiredServiceInstanceGuid(serviceInstanceName); + return doUnbindServiceInstance(applicationGuid, serviceInstanceGuid); + } + + public List unbindServiceInstance(UUID applicationGuid, UUID serviceInstanceGuid) { + return doUnbindServiceInstance(applicationGuid, serviceInstanceGuid); + } + + public Optional deleteServiceBinding(UUID bindingGuid) { + return deleteSingleServiceBinding(bindingGuid); + } + + public CloudServiceBinding getServiceBinding(UUID serviceBindingGuid) { + String uri = "/v3/service_credential_bindings?guids=" + serviceBindingGuid + "&per_page=" + PER_PAGE; + return cc.list(uri, BINDING_LIST_TYPE) + .stream() + .findFirst() + .map(V3ServiceBindingMapper::toCloudServiceBinding) + .orElse(null); + } + + public List getServiceAppBindings(UUID serviceInstanceGuid) { + String uri = "/v3/service_credential_bindings?service_instance_guids=" + serviceInstanceGuid + "&type=app&per_page=" + PER_PAGE; + return listBindings(uri); + } + + public List getAppBindings(UUID applicationGuid) { + String uri = "/v3/service_credential_bindings?app_guids=" + applicationGuid + "&per_page=" + PER_PAGE; + return listBindings(uri); + } + + public List getServiceBindingsForApplication(UUID applicationId, UUID serviceInstanceGuid) { + String uri = "/v3/service_credential_bindings?app_guids=" + applicationId + "&service_instance_guids=" + serviceInstanceGuid + + "&per_page=" + PER_PAGE; + return listBindings(uri); + } + + public Map getServiceBindingParameters(UUID guid) { + @SuppressWarnings("unchecked") + Map parameters = cc.get("/v3/service_credential_bindings/" + guid + "/parameters", Map.class); + return parameters; + } + + public void updateServiceBindingMetadata(UUID guid, Metadata metadata) { + Map metadataBody = new HashMap<>(); + metadataBody.put("labels", metadata.getLabels()); + metadataBody.put("annotations", metadata.getAnnotations()); + cc.getRestClient() + .patch() + .uri("/v3/service_credential_bindings/{guid}", guid) + .body(Map.of("metadata", metadataBody)) + .retrieve() + .toBodilessEntity(); + } + + private List listBindings(String uri) { + return cc.list(uri, BINDING_LIST_TYPE) + .stream() + .map(V3ServiceBindingMapper::toCloudServiceBinding) + .toList(); + } + + // Mirrors the OSS doUnbindServiceInstance: resolve the app<->service bindings and 404 when none exist, then delete each. + private List doUnbindServiceInstance(UUID applicationGuid, UUID serviceInstanceGuid) { + List serviceBindingGuids = getServiceBindingGuids(applicationGuid, serviceInstanceGuid); + if (serviceBindingGuids.isEmpty()) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", + "Service binding between service with GUID " + serviceInstanceGuid + + " and application with GUID " + applicationGuid + " not found."); + } + return doDeleteServiceBindings(serviceBindingGuids); + } + + private List getServiceBindingGuids(UUID applicationGuid, UUID serviceInstanceGuid) { + String uri = "/v3/service_credential_bindings?app_guids=" + applicationGuid + "&service_instance_guids=" + serviceInstanceGuid + + "&per_page=" + PER_PAGE; + return cc.list(uri, BINDING_LIST_TYPE) + .stream() + .map(binding -> UUID.fromString(binding.guid())) + .toList(); + } + + private List doDeleteServiceBindings(List guids) { + List jobIds = new ArrayList<>(); + List errors = new ArrayList<>(); + for (UUID guid : guids) { + try { + deleteSingleServiceBinding(guid).ifPresent(jobIds::add); + } catch (CloudOperationException e) { + errors.add(e); + } + } + throwOnErrors(errors); + return jobIds; + } + + private Optional deleteSingleServiceBinding(UUID guid) { + ResponseEntity response = cc.getRestClient() + .delete() + .uri("/v3/service_credential_bindings/{guid}", guid) + .retrieve() + .toBodilessEntity(); + return extractJobGuid(response); + } + + private void throwOnErrors(List errors) { + if (errors.isEmpty()) { + return; + } + CloudOperationException first = errors.get(0); + errors.subList(1, errors.size()) + .forEach(first::addSuppressed); + throw first; + } + + // Extract the job GUID from the Location header of a 202-Accepted response (path ends in /v3/jobs/{guid}); empty if synchronous. + private Optional extractJobGuid(ResponseEntity response) { + URI location = response.getHeaders() + .getLocation(); + if (location == null) { + return Optional.empty(); + } + String path = location.getPath(); + return Optional.of(path.substring(path.lastIndexOf('/') + 1)); + } + + private Map toOneRelationship(UUID guid) { + return Map.of("data", Map.of("guid", guid.toString())); + } + + // Mirrors the OSS getRequiredApplicationGuid: look up the app in the target space and 404 if it is absent. + private UUID getRequiredApplicationGuid(String applicationName) { + StringBuilder query = new StringBuilder("/v3/apps?per_page=" + PER_PAGE); + if (target != null && target.getGuid() != null) { + query.append("&space_guids=") + .append(target.getGuid()); + } + query.append("&names=") + .append(applicationName); + List apps = cc.list(query.toString(), APPLICATION_LIST_TYPE); + if (apps.isEmpty()) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + applicationName + " not found."); + } + return UUID.fromString(apps.get(0) + .guid()); + } + + // Mirrors the OSS getRequiredServiceInstanceGuid: look up the service instance in the target space and 404 if it is absent. + private UUID getRequiredServiceInstanceGuid(String name) { + StringBuilder query = new StringBuilder("/v3/service_instances?per_page=" + PER_PAGE); + if (target != null && target.getGuid() != null) { + query.append("&space_guids=") + .append(target.getGuid()); + } + query.append("&names=") + .append(name); + List instances = cc.list(query.toString(), + new ParameterizedTypeReference>() { + }); + if (instances.isEmpty()) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service instance " + name + " not found."); + } + return UUID.fromString(instances.get(0) + .guid()); + } + + // Minimal wire model to resolve a service instance GUID by name without depending on the (separately-owned) ServiceInstances group. + @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) + private record V3ServiceInstanceRef(@com.fasterxml.jackson.annotation.JsonProperty("guid") String guid) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBrokersV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBrokersV3Operations.java new file mode 100644 index 0000000000..8a7acff08a --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBrokersV3Operations.java @@ -0,0 +1,228 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBroker; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServicePlanVisibility; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceBroker; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceBrokerMapper; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * CF v3 service broker operations of the in-house {@code CloudControllerRestClient}. Reproduces the HTTP shape, filtering, + * async job handling and domain mapping of the OSS {@code CloudControllerRestClientImpl} service-broker methods on top of the shared + * {@link CloudControllerV3Client} machinery. + * + *

+ * Endpoint map (see {@code docs/cf-java-client-migration/02-endpoint-inventory.md}): + *

+ *
    + *
  • {@code createServiceBroker} → {@code POST /v3/service_brokers} (async job; returns the job GUID)
  • + *
  • {@code deleteServiceBroker} → {@code DELETE /v3/service_brokers/{guid}} (async job; returns the job GUID)
  • + *
  • {@code getServiceBroker(name[, required])} → {@code GET /v3/service_brokers?names=} (paginated, first match)
  • + *
  • {@code getServiceBrokers} → {@code GET /v3/service_brokers} (paginated)
  • + *
  • {@code updateServiceBroker} → {@code PATCH /v3/service_brokers/{guid}} (async job; returns the job GUID or {@code null})
  • + *
  • {@code updateServicePlanVisibilityForBroker} → per plan {@code PATCH /v3/service_plans/{guid}/visibility}
  • + *
+ * + *

+ * As in the OSS impl, {@code createServiceBroker}/{@code deleteServiceBroker}/{@code updateServiceBroker} return the async job GUID + * without waiting for the job to complete — the caller polls the job separately. + *

+ */ +public class ServiceBrokersV3Operations { + + private static final int PER_PAGE = 5000; + + private static final ParameterizedTypeReference> BROKER_PAGE = new ParameterizedTypeReference<>() { + }; + private static final ParameterizedTypeReference> OFFERING_PAGE = new ParameterizedTypeReference<>() { + }; + private static final ParameterizedTypeReference> PLAN_PAGE = new ParameterizedTypeReference<>() { + }; + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public ServiceBrokersV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + public String createServiceBroker(CloudServiceBroker serviceBroker) { + Assert.notNull(serviceBroker, "Service broker must not be null."); + + Map body = new HashMap<>(); + body.put("name", serviceBroker.getName()); + body.put("url", serviceBroker.getUrl()); + body.put("authentication", basicAuthentication(serviceBroker)); + if (serviceBroker.getSpaceGuid() != null) { + body.put("relationships", Map.of("space", Map.of("data", Map.of("guid", serviceBroker.getSpaceGuid())))); + } + + ResponseEntity response = cc.getRestClient() + .post() + .uri("/v3/service_brokers") + .body(body) + .retrieve() + .toBodilessEntity(); + return extractJobGuid(response); + } + + public String deleteServiceBroker(String name) { + CloudServiceBroker broker = getServiceBroker(name); + UUID guid = broker.getMetadata() + .getGuid(); + ResponseEntity response = cc.getRestClient() + .delete() + .uri("/v3/service_brokers/{guid}", guid.toString()) + .retrieve() + .toBodilessEntity(); + return extractJobGuid(response); + } + + public CloudServiceBroker getServiceBroker(String name) { + return getServiceBroker(name, true); + } + + public CloudServiceBroker getServiceBroker(String name, boolean required) { + CloudServiceBroker serviceBroker = findServiceBrokerByName(name); + if (serviceBroker == null && required) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service broker " + name + " not found."); + } + return serviceBroker; + } + + public List getServiceBrokers() { + return cc.list("/v3/service_brokers?per_page=" + PER_PAGE, BROKER_PAGE) + .stream() + .map(V3ServiceBrokerMapper::toCloudServiceBroker) + .toList(); + } + + public String updateServiceBroker(CloudServiceBroker serviceBroker) { + Assert.notNull(serviceBroker, "Service broker must not be null."); + + CloudServiceBroker existingBroker = getServiceBroker(serviceBroker.getName()); + UUID brokerGuid = existingBroker.getMetadata() + .getGuid(); + + Map body = Map.of("name", serviceBroker.getName(), "url", serviceBroker.getUrl(), "authentication", + basicAuthentication(serviceBroker)); + + ResponseEntity response = cc.getRestClient() + .patch() + .uri("/v3/service_brokers/{guid}", brokerGuid.toString()) + .body(body) + .retrieve() + .toBodilessEntity(); + // A synchronous update returns 200 with no Location header (no job); an async update returns 202 with a job GUID. + return extractJobGuid(response); + } + + public void updateServicePlanVisibilityForBroker(String name, ServicePlanVisibility visibility) { + CloudServiceBroker broker = getServiceBroker(name); + UUID brokerGuid = broker.getMetadata() + .getGuid(); + for (UUID servicePlanGuid : findServicePlanGuidsByBrokerGuid(brokerGuid)) { + updateServicePlanVisibility(servicePlanGuid, visibility); + } + } + + private CloudServiceBroker findServiceBrokerByName(String name) { + return cc.list("/v3/service_brokers?names=" + name + "&per_page=" + PER_PAGE, BROKER_PAGE) + .stream() + .findFirst() + .map(V3ServiceBrokerMapper::toCloudServiceBroker) + .orElse(null); + } + + private List findServicePlanGuidsByBrokerGuid(UUID brokerGuid) { + return findServiceOfferingGuidsByBrokerGuid(brokerGuid).stream() + .flatMap(offeringGuid -> findServicePlanGuidsByOfferingGuid( + offeringGuid).stream()) + .toList(); + } + + private List findServiceOfferingGuidsByBrokerGuid(UUID brokerGuid) { + String uri = "/v3/service_offerings?service_broker_guids=" + brokerGuid + "&space_guids=" + getTargetSpaceGuid() + "&per_page=" + + PER_PAGE; + return cc.list(uri, OFFERING_PAGE) + .stream() + .map(ServiceOfferingRef::guid) + .map(UUID::fromString) + .toList(); + } + + private List findServicePlanGuidsByOfferingGuid(UUID serviceOfferingGuid) { + String uri = "/v3/service_plans?service_offering_guids=" + serviceOfferingGuid + "&per_page=" + PER_PAGE; + return cc.list(uri, PLAN_PAGE) + .stream() + .map(ServicePlanRef::guid) + .map(UUID::fromString) + .toList(); + } + + private void updateServicePlanVisibility(UUID servicePlanGuid, ServicePlanVisibility visibility) { + // ServicePlanVisibility.toString() yields the lowercase CF visibility type (public/admin/organization), + // matching the OSS Visibility.from(visibility.toString()) mapping. + cc.getRestClient() + .patch() + .uri("/v3/service_plans/{guid}/visibility", servicePlanGuid.toString()) + .body(Map.of("type", visibility.toString())) + .retrieve() + .toBodilessEntity(); + } + + private static Map basicAuthentication(CloudServiceBroker serviceBroker) { + return Map.of("type", "basic", "credentials", + Map.of("username", nullToEmpty(serviceBroker.getUsername()), "password", nullToEmpty(serviceBroker.getPassword()))); + } + + private static String nullToEmpty(String value) { + return value == null ? "" : value; + } + + private String getTargetSpaceGuid() { + return target.getMetadata() + .getGuid() + .toString(); + } + + // A 202-Accepted async response carries the job in its Location header (/v3/jobs/{guid}); a synchronous 200 has none. + private static String extractJobGuid(ResponseEntity response) { + URI location = response.getHeaders() + .getLocation(); + if (location == null) { + return null; + } + String value = location.toString(); + int idx = value.lastIndexOf("/v3/jobs/"); + if (idx < 0) { + return value.substring(value.lastIndexOf('/') + 1); + } + return value.substring(idx + "/v3/jobs/".length()); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record ServiceOfferingRef(@JsonProperty("guid") String guid) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record ServicePlanRef(@JsonProperty("guid") String guid) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java new file mode 100644 index 0000000000..1361ba9fb4 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java @@ -0,0 +1,442 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEntity; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceInstance; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceInstanceMapper; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceOffering; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServicePlan; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +/** + * CF v3 service instance operations of the in-house {@code CloudControllerRestClient}. Reproduces the HTTP shape, filtering, + * space-scoping and domain-mapping of the OSS {@code CloudControllerRestClientImpl} service-instance methods on top of the shared + * {@link CloudControllerV3Client} machinery. + * + *

+ * Endpoint map (see {@code docs/cf-java-client-migration/02-endpoint-inventory.md}): + *

+ *
    + *
  • {@code createServiceInstance} → {@code POST /v3/service_instances} (type=managed)
  • + *
  • {@code createUserProvidedServiceInstance} → {@code POST /v3/service_instances} (type=user-provided)
  • + *
  • {@code deleteServiceInstance} → {@code DELETE /v3/service_instances/{guid}}
  • + *
  • {@code getServiceInstance} / {@code getRequiredServiceInstanceGuid} / {@code getServiceInstanceWithoutAuxiliaryContent} → + * {@code GET /v3/service_instances?space_guids=…&names=…} (paginated)
  • + *
  • {@code getServiceInstanceName} → {@code GET /v3/service_instances/{guid}}
  • + *
  • {@code getServiceInstanceParameters} → {@code GET /v3/service_instances/{guid}/parameters}
  • + *
  • {@code getUserProvidedServiceInstanceParameters} → {@code GET /v3/service_instances/{guid}/credentials}
  • + *
  • {@code getServiceInstancesWithoutAuxiliaryContentByNames} → {@code GET /v3/service_instances?space_guids=…&names=…} (batched)
  • + *
  • {@code getServiceInstancesByMetadataLabelSelector} (+ WithoutAuxiliaryContent) → + * {@code GET /v3/service_instances?space_guids=…&label_selector=…} (paginated)
  • + *
  • {@code updateServicePlan} / {@code updateServiceParameters} / {@code updateServiceTags} / {@code updateServiceSyslogDrainUrl} / + * {@code updateServiceInstanceMetadata} → {@code PATCH /v3/service_instances/{guid}}
  • + *
+ * + *

+ * Async note: for managed instances CF returns 202 for create/update/delete, but the OSS impl does not follow the job — it returns + * as soon as the request is accepted. This helper preserves that behaviour (no {@code followAsyncJob} on these calls). + *

+ */ +public class ServiceInstancesV3Operations { + + // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). + private static final int PER_PAGE = 5000; + // Mirrors the OSS MAX_CHAR_LENGTH_FOR_PARAMS_IN_REQUEST used to batch the "byNames" lookups. + private static final int MAX_CHAR_LENGTH_FOR_PARAMS_IN_REQUEST = 4000; + + private static final ParameterizedTypeReference> SERVICE_INSTANCE_LIST_TYPE = new ParameterizedTypeReference<>() { + }; + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public ServiceInstancesV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + public void createServiceInstance(CloudServiceInstance serviceInstance) { + assertSpaceProvided("create service instance"); + Assert.notNull(serviceInstance, "Service instance must not be null."); + UUID servicePlanGuid = findPlanGuidForService(serviceInstance, serviceInstance.getPlan()); + + Map body = new HashMap<>(); + body.put("type", "managed"); + body.put("name", serviceInstance.getName()); + putIfNotNull(body, "metadata", toMetadataMap(serviceInstance.getV3Metadata())); + putIfNotNull(body, "tags", serviceInstance.getTags()); + putIfNotNull(body, "parameters", serviceInstance.getCredentials()); + body.put("relationships", Map.of("service_plan", toOneRelationship(servicePlanGuid.toString()), "space", + toOneRelationship(getTargetSpaceGuid().toString()))); + + cc.getRestClient() + .post() + .uri("/v3/service_instances") + .body(body) + .retrieve() + .toBodilessEntity(); + } + + public void createUserProvidedServiceInstance(CloudServiceInstance serviceInstance) { + assertSpaceProvided("create service instance"); + Assert.notNull(serviceInstance, "Service instance must not be null."); + String syslogDrainUrl = hasText(serviceInstance.getSyslogDrainUrl()) ? serviceInstance.getSyslogDrainUrl() : ""; + + Map body = new HashMap<>(); + body.put("type", "user-provided"); + body.put("name", serviceInstance.getName()); + putIfNotNull(body, "metadata", toMetadataMap(serviceInstance.getV3Metadata())); + putIfNotNull(body, "credentials", serviceInstance.getCredentials()); + body.put("syslog_drain_url", syslogDrainUrl); + putIfNotNull(body, "tags", serviceInstance.getTags()); + body.put("relationships", Map.of("space", toOneRelationship(getTargetSpaceGuid().toString()))); + + cc.getRestClient() + .post() + .uri("/v3/service_instances") + .body(body) + .retrieve() + .toBodilessEntity(); + } + + public void deleteServiceInstance(String serviceInstanceName) { + CloudServiceInstance serviceInstance = getServiceInstanceWithoutAuxiliaryContent(serviceInstanceName); + doDeleteServiceInstance(serviceInstance.getGuid()); + } + + public void deleteServiceInstance(CloudServiceInstance serviceInstance) { + doDeleteServiceInstance(serviceInstance.getGuid()); + } + + public UUID getRequiredServiceInstanceGuid(String name) { + V3ServiceInstance resource = findServiceInstanceResourceByName(name); + if (resource == null) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service instance " + name + " not found."); + } + return UUID.fromString(resource.guid()); + } + + public CloudServiceInstance getServiceInstance(String serviceInstanceName) { + return getServiceInstance(serviceInstanceName, true); + } + + public CloudServiceInstance getServiceInstance(String serviceInstanceName, boolean required) { + CloudServiceInstance serviceInstance = findServiceInstanceByName(serviceInstanceName); + return getServiceInstanceIfRequired(serviceInstanceName, serviceInstance, required); + } + + public String getServiceInstanceName(UUID serviceInstanceGuid) { + V3ServiceInstance serviceInstance = cc.get("/v3/service_instances/" + serviceInstanceGuid, V3ServiceInstance.class); + return serviceInstance == null ? null : serviceInstance.name(); + } + + public CloudServiceInstance getServiceInstanceWithoutAuxiliaryContent(String serviceInstanceName) { + return getServiceInstanceWithoutAuxiliaryContent(serviceInstanceName, true); + } + + public CloudServiceInstance getServiceInstanceWithoutAuxiliaryContent(String serviceInstanceName, boolean required) { + V3ServiceInstance resource = findServiceInstanceResourceByName(serviceInstanceName); + CloudServiceInstance serviceInstance = resource == null ? null : V3ServiceInstanceMapper.toCloudServiceInstance(resource, null, + null); + return getServiceInstanceIfRequired(serviceInstanceName, serviceInstance, required); + } + + public Map getServiceInstanceParameters(UUID guid) { + return getParameters("/v3/service_instances/" + guid + "/parameters"); + } + + public Map getUserProvidedServiceInstanceParameters(UUID guid) { + return getParameters("/v3/service_instances/" + guid + "/credentials"); + } + + public List getServiceInstancesWithoutAuxiliaryContentByNames(List names) { + List allServiceInstances = new ArrayList<>(); + for (List batch : toBatches(names, MAX_CHAR_LENGTH_FOR_PARAMS_IN_REQUEST)) { + String uri = "/v3/service_instances?per_page=" + PER_PAGE + "&space_guids=" + getTargetSpaceGuid() + "&names=" + + String.join(",", batch); + cc.list(uri, SERVICE_INSTANCE_LIST_TYPE) + .stream() + .map(V3ServiceInstanceMapper::toCloudServiceInstanceWithoutAuxiliaryContent) + .forEach(allServiceInstances::add); + } + return allServiceInstances; + } + + public List getServiceInstancesByMetadataLabelSelector(String labelSelector) { + String uri = "/v3/service_instances?per_page=" + PER_PAGE + "&space_guids=" + getTargetSpaceGuid() + "&label_selector=" + + labelSelector; + return cc.list(uri, SERVICE_INSTANCE_LIST_TYPE) + .stream() + .map(this::mapWithAuxiliaryContent) + .collect(Collectors.toList()); + } + + public List getServiceInstancesWithoutAuxiliaryContentByMetadataLabelSelector(String labelSelector) { + String uri = "/v3/service_instances?per_page=" + PER_PAGE + "&space_guids=" + getTargetSpaceGuid() + "&label_selector=" + + labelSelector; + return cc.list(uri, SERVICE_INSTANCE_LIST_TYPE) + .stream() + .map(resource -> V3ServiceInstanceMapper.toCloudServiceInstance(resource, null, null)) + .collect(Collectors.toList()); + } + + public void updateServicePlan(String serviceName, String planName) { + CloudServiceInstance service = getServiceInstance(serviceName); + if (service.isUserProvided()) { + return; + } + UUID planGuid = findPlanGuidForService(service, planName); + patchServiceInstance(service.getGuid(), Map.of("relationships", Map.of("service_plan", toOneRelationship(planGuid.toString())))); + } + + public void updateServiceParameters(String serviceName, Map parameters) { + CloudServiceInstance service = getServiceInstanceWithoutAuxiliaryContent(serviceName); + String key = service.isUserProvided() ? "credentials" : "parameters"; + patchServiceInstance(service.getGuid(), Map.of(key, parameters)); + } + + public void updateServiceTags(String serviceName, List tags) { + UUID serviceInstanceGuid = getRequiredServiceInstanceGuid(serviceName); + patchServiceInstance(serviceInstanceGuid, Map.of("tags", tags)); + } + + public void updateServiceSyslogDrainUrl(String serviceName, String syslogDrainUrl) { + CloudServiceInstance service = getServiceInstanceWithoutAuxiliaryContent(serviceName); + if (!service.isUserProvided()) { + return; + } + String updatedSyslogDrain = hasText(syslogDrainUrl) ? syslogDrainUrl : ""; + patchServiceInstance(service.getGuid(), Map.of("syslog_drain_url", updatedSyslogDrain)); + } + + public void updateServiceInstanceMetadata(UUID guid, Metadata metadata) { + Map metadataMap = toMetadataMap(metadata); + patchServiceInstance(guid, Map.of("metadata", metadataMap == null ? Map.of() : metadataMap)); + } + + private CloudServiceInstance getServiceInstanceIfRequired(String serviceInstanceName, CloudServiceInstance serviceInstance, + boolean required) { + if (serviceInstance == null && required) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service instance " + serviceInstanceName + " not found."); + } + return serviceInstance; + } + + // Mirrors the OSS findServiceInstanceByName -> zipWithAuxiliaryServiceInstanceContent: for managed instances the plan and offering + // names are resolved and included; user-provided instances are mapped as-is. + private CloudServiceInstance findServiceInstanceByName(String name) { + V3ServiceInstance resource = findServiceInstanceResourceByName(name); + if (resource == null) { + return null; + } + return mapWithAuxiliaryContent(resource); + } + + private CloudServiceInstance mapWithAuxiliaryContent(V3ServiceInstance resource) { + if (isUserProvided(resource)) { + return V3ServiceInstanceMapper.toCloudServiceInstance(resource, null, null); + } + String servicePlanGuid = servicePlanGuidOf(resource); + if (servicePlanGuid == null) { + return V3ServiceInstanceMapper.toCloudServiceInstance(resource, null, null); + } + ServicePlanNames names = resolvePlanAndOfferingNames(servicePlanGuid); + return V3ServiceInstanceMapper.toCloudServiceInstance(resource, names.planName(), names.offeringName()); + } + + private V3ServiceInstance findServiceInstanceResourceByName(String name) { + String uri = "/v3/service_instances?per_page=" + PER_PAGE + "&space_guids=" + getTargetSpaceGuid() + "&names=" + name; + return cc.list(uri, SERVICE_INSTANCE_LIST_TYPE) + .stream() + .findFirst() + .orElse(null); + } + + private boolean isUserProvided(V3ServiceInstance resource) { + return "user-provided".equals(resource.type()); + } + + private String servicePlanGuidOf(V3ServiceInstance resource) { + if (resource.relationships() == null || resource.relationships() + .servicePlan() == null + || resource.relationships() + .servicePlan() + .data() == null) { + return null; + } + return resource.relationships() + .servicePlan() + .data() + .guid(); + } + + // Mirrors the OSS getServicePlanResource + getServiceOffering zip: resolve the plan's own name and its offering's name. + private ServicePlanNames resolvePlanAndOfferingNames(String servicePlanGuid) { + V3ServicePlan plan = cc.get("/v3/service_plans/" + servicePlanGuid, V3ServicePlan.class); + if (plan == null) { + return new ServicePlanNames(null, null); + } + String offeringName = null; + String offeringGuid = offeringGuidOf(plan); + if (offeringGuid != null) { + V3ServiceOffering offering = cc.get("/v3/service_offerings/" + offeringGuid, V3ServiceOffering.class); + offeringName = offering == null ? null : offering.name(); + } + return new ServicePlanNames(plan.name(), offeringName); + } + + private String offeringGuidOf(V3ServicePlan plan) { + if (plan.relationships() == null || plan.relationships() + .serviceOffering() == null + || plan.relationships() + .serviceOffering() + .data() == null) { + return null; + } + return plan.relationships() + .serviceOffering() + .data() + .guid(); + } + + // Mirrors the OSS findPlanForService: list the offerings for the service's label (optionally scoped by broker), then find the plan by + // name; a missing plan throws CloudOperationException(NOT_FOUND). + private UUID findPlanGuidForService(CloudServiceInstance service, String planName) { + List offeringGuids = findServiceOfferingGuids(service.getLabel(), service.getBroker()); + for (String offeringGuid : offeringGuids) { + String uri = "/v3/service_plans?per_page=" + PER_PAGE + "&service_offering_guids=" + offeringGuid + "&names=" + planName; + V3ServicePlan plan = cc.list(uri, new ParameterizedTypeReference>() { + }) + .stream() + .findFirst() + .orElse(null); + if (plan != null) { + return UUID.fromString(plan.guid()); + } + } + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service plan " + planName + " not found."); + } + + private List findServiceOfferingGuids(String label, String broker) { + StringBuilder uri = new StringBuilder("/v3/service_offerings?per_page=").append(PER_PAGE) + .append("&space_guids=") + .append(getTargetSpaceGuid()) + .append("&names=") + .append(label); + if (hasText(broker)) { + uri.append("&service_broker_names=") + .append(broker); + } + return cc.list(uri.toString(), new ParameterizedTypeReference>() { + }) + .stream() + .map(V3ServiceOffering::guid) + .collect(Collectors.toList()); + } + + private void doDeleteServiceInstance(UUID serviceInstanceGuid) { + // OSS parity: the delete is issued and the response consumed, but the async job is NOT followed. + cc.getRestClient() + .delete() + .uri("/v3/service_instances/{guid}", serviceInstanceGuid.toString()) + .retrieve() + .toBodilessEntity(); + } + + private void patchServiceInstance(UUID guid, Map body) { + // OSS parity: the update is issued and the response consumed, but the async job is NOT followed. + cc.getRestClient() + .patch() + .uri("/v3/service_instances/{guid}", guid.toString()) + .body(body) + .retrieve() + .toBodilessEntity(); + } + + @SuppressWarnings("unchecked") + private Map getParameters(String uri) { + return cc.get(uri, Map.class); + } + + private Map toOneRelationship(String guid) { + return Map.of("data", Map.of("guid", guid)); + } + + private Map toMetadataMap(Metadata metadata) { + if (metadata == null) { + return null; + } + Map map = new HashMap<>(); + map.put("labels", metadata.getLabels() == null ? Map.of() : metadata.getLabels()); + map.put("annotations", metadata.getAnnotations() == null ? Map.of() : metadata.getAnnotations()); + return map; + } + + private static void putIfNotNull(Map body, String key, Object value) { + if (value != null) { + body.put(key, value); + } + } + + private static boolean hasText(String value) { + return value != null && !value.isBlank(); + } + + // Mirrors the OSS toBatches: split a large collection into batches bounded by the total character length of the elements. + private List> toBatches(Collection largeList, int maxCharLength) { + if (largeList.isEmpty()) { + return List.of(); + } + List> batches = new ArrayList<>(); + int currentBatchLength = 0; + int currentBatchIndex = 0; + batches.add(new ArrayList<>()); + for (T element : largeList) { + int elementLength = element.toString() + .length(); + if (elementLength + currentBatchLength >= maxCharLength) { + batches.add(new ArrayList<>()); + currentBatchIndex++; + currentBatchLength = 0; + } + batches.get(currentBatchIndex) + .add(element); + currentBatchLength += elementLength; + } + return batches; + } + + private UUID getTargetSpaceGuid() { + return getGuid(target); + } + + private UUID getGuid(CloudEntity entity) { + if (entity == null || entity.getMetadata() == null) { + return null; + } + return entity.getMetadata() + .getGuid(); + } + + private void assertSpaceProvided(String operation) { + Assert.notNull(target, "Unable to " + operation + " without specifying organization and space to use."); + } + + private record ServicePlanNames(String planName, String offeringName) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceKeysV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceKeysV3Operations.java new file mode 100644 index 0000000000..720db7bc3f --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceKeysV3Operations.java @@ -0,0 +1,188 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.net.URI; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType; +import org.cloudfoundry.multiapps.controller.client.facade.Messages; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceBinding; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceKeyDetails; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceKeyMapper; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.ResponseEntity; + +/** + * CF v3 service key operations for the cf-java-client replacement. A service key is a service credential binding of + * {@code type=key}, so all endpoints target {@code /v3/service_credential_bindings}. Reproduces the HTTP shape and domain-mapping of the + * OSS {@code CloudControllerRestClientImpl} service-key methods: + *
    + *
  • create → {@code POST /v3/service_credential_bindings} (type=key) — returns a job (202);
  • + *
  • list → {@code GET /v3/service_credential_bindings?type=key&service_instance_guids={guid}[&names={name}]} (paginated);
  • + *
  • credentials → {@code GET /v3/service_credential_bindings/{guid}/details}.
  • + *
+ * + *

+ * The interface methods that accept a {@code serviceInstanceName} require the owning {@link CloudServiceInstance} (for its GUID and to + * embed in the resulting {@link CloudServiceKey}). Mirroring the OSS impl — which first calls {@code getServiceInstance(name)} + * — these operations take the already-resolved {@link CloudServiceInstance}; the integrator resolves it in + * {@code CloudControllerRestClientV3Impl} before delegating. + *

+ */ +public class ServiceKeysV3Operations { + + // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). + private static final int DEFAULT_PAGE_SIZE = 5000; + // Mirrors the OSS BINDING_OPERATIONS_TIMEOUT used when synchronously creating (and fetching) a service key. + private static final Duration BINDING_OPERATIONS_TIMEOUT = Duration.ofMinutes(5); + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public ServiceKeysV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + // --- create ----------------------------------------------------------------------------------------------------------------- + + public CloudServiceKey createAndFetchServiceKey(CloudServiceKey keyModel, CloudServiceInstance serviceInstance) { + ResponseEntity accepted = postServiceKey(keyModel.getName(), keyModel.getCredentials(), keyModel.getV3Metadata(), + serviceInstance); + cc.followAsyncJob(accepted, BINDING_OPERATIONS_TIMEOUT); + V3ServiceBinding created = getServiceKeyResourceByNameAndServiceInstanceGuid(keyModel.getName(), serviceInstance.getGuid()); + if (created == null) { + return null; + } + Map credentials = getServiceKeyCredentials(created.guid()); + return V3ServiceKeyMapper.toCloudServiceKey(created, serviceInstance, credentials); + } + + public Optional createServiceKey(CloudServiceKey keyModel, CloudServiceInstance serviceInstance) { + ResponseEntity accepted = postServiceKey(keyModel.getName(), keyModel.getCredentials(), keyModel.getV3Metadata(), + serviceInstance); + return extractJobGuid(accepted); + } + + public Optional createServiceKey(CloudServiceInstance serviceInstance, String serviceKeyName, Map parameters) { + ResponseEntity accepted = postServiceKey(serviceKeyName, parameters, null, serviceInstance); + return extractJobGuid(accepted); + } + + // --- read ------------------------------------------------------------------------------------------------------------------- + + public CloudServiceKey getServiceKey(CloudServiceInstance serviceInstance, String serviceKeyName) { + V3ServiceBinding key = getServiceKeyResourceByNameAndServiceInstanceGuid(serviceKeyName, serviceInstance.getGuid()); + if (key == null) { + return null; + } + Map credentials = getServiceKeyCredentials(key.guid()); + return V3ServiceKeyMapper.toCloudServiceKey(key, serviceInstance, credentials); + } + + public List getServiceKeys(CloudServiceInstance serviceInstance) { + return listServiceKeyResources(serviceInstance.getGuid()).stream() + .map(key -> V3ServiceKeyMapper.toCloudServiceKey(key, serviceInstance)) + .toList(); + } + + public List getServiceKeysWithCredentials(CloudServiceInstance serviceInstance) { + return listServiceKeyResources(serviceInstance.getGuid()).stream() + .map(key -> V3ServiceKeyMapper.toCloudServiceKey(key, serviceInstance, + getServiceKeyCredentials( + key.guid()))) + .toList(); + } + + // --- helpers ---------------------------------------------------------------------------------------------------------------- + + private ResponseEntity postServiceKey(String name, Map parameters, Metadata metadata, + CloudServiceInstance serviceInstance) { + if (serviceInstance.getType() != ServiceInstanceType.MANAGED) { + throw new IllegalArgumentException( + String.format(Messages.CANT_CREATE_SERVICE_KEY_FOR_USER_PROVIDED_SERVICE, serviceInstance.getName())); + } + return cc.getRestClient() + .post() + .uri("/v3/service_credential_bindings") + .body(buildCreateServiceKeyBody(name, parameters, metadata, serviceInstance.getGuid())) + .retrieve() + .toBodilessEntity(); + } + + private Map buildCreateServiceKeyBody(String name, Map parameters, Metadata metadata, + UUID serviceInstanceGuid) { + Map body = new HashMap<>(); + body.put("type", "key"); + body.put("name", name); + body.put("relationships", Map.of("service_instance", Map.of("data", Map.of("guid", serviceInstanceGuid.toString())))); + if (parameters != null && !parameters.isEmpty()) { + body.put("parameters", parameters); + } + Map metadataBody = buildMetadataBody(metadata); + if (metadataBody != null) { + body.put("metadata", metadataBody); + } + return body; + } + + private Map buildMetadataBody(Metadata metadata) { + if (metadata == null) { + return null; + } + Map metadataBody = new HashMap<>(); + if (metadata.getLabels() != null) { + metadataBody.put("labels", metadata.getLabels()); + } + if (metadata.getAnnotations() != null) { + metadataBody.put("annotations", metadata.getAnnotations()); + } + return metadataBody.isEmpty() ? null : metadataBody; + } + + private V3ServiceBinding getServiceKeyResourceByNameAndServiceInstanceGuid(String name, UUID serviceInstanceGuid) { + String query = "/v3/service_credential_bindings?per_page=" + DEFAULT_PAGE_SIZE + "&type=key&service_instance_guids=" + + serviceInstanceGuid + "&names=" + name; + List keys = cc.list(query, new ParameterizedTypeReference>() { + }); + return keys.isEmpty() ? null : keys.get(0); + } + + private List listServiceKeyResources(UUID serviceInstanceGuid) { + String query = "/v3/service_credential_bindings?per_page=" + DEFAULT_PAGE_SIZE + "&type=key&service_instance_guids=" + + serviceInstanceGuid; + return cc.list(query, new ParameterizedTypeReference>() { + }); + } + + // CF v3 returns 404 when fetching the details of a service key whose creation failed; treat that as empty credentials, matching the OSS impl. + private Map getServiceKeyCredentials(String keyGuid) { + Optional details = cc.getOptional("/v3/service_credential_bindings/" + keyGuid + "/details", + V3ServiceKeyDetails.class); + return details.map(V3ServiceKeyDetails::credentials) + .orElse(Collections.emptyMap()); + } + + private Optional extractJobGuid(ResponseEntity accepted) { + URI location = accepted.getHeaders() + .getLocation(); + if (location == null) { + return Optional.empty(); + } + String href = location.toString(); + int idx = href.lastIndexOf("/v3/jobs/"); + String jobGuid = idx < 0 ? href.substring(href.lastIndexOf('/') + 1) : href.substring(idx + "/v3/jobs/".length()); + return Optional.of(jobGuid); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceOfferingsV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceOfferingsV3Operations.java new file mode 100644 index 0000000000..9f5672110d --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceOfferingsV3Operations.java @@ -0,0 +1,82 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.util.List; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceOffering; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServicePlan; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceOffering; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceOfferingMapper; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServicePlan; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServicePlanMapper; +import org.springframework.core.ParameterizedTypeReference; + +/** + * CF v3 service offerings operations of the in-house {@code CloudControllerRestClient}. Reproduces the HTTP shape, filtering and + * per-offering plan fan-out of the OSS {@code CloudControllerRestClientImpl#getServiceOfferings} on top of the shared + * {@link CloudControllerV3Client} machinery. + * + *

+ * Endpoint map (see {@code docs/cf-java-client-migration/02-endpoint-inventory.md}): + *

+ *
    + *
  • list offerings → {@code GET /v3/service_offerings?space_guids=} (paginated);
  • + *
  • resolve each offering's plans → {@code GET /v3/service_plans?service_offering_guids=} (paginated).
  • + *
+ * + *

+ * This mirrors the OSS {@code getServiceResources()} + {@code zipWithAuxiliaryServiceOfferingContent(...)} flow: the offerings are scoped + * to the target space, and each offering is enriched with the plans belonging to it before being mapped to the domain object. + *

+ */ +public class ServiceOfferingsV3Operations { + + // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). + private static final int PER_PAGE = 5000; + + private static final ParameterizedTypeReference> OFFERING_LIST_TYPE = new ParameterizedTypeReference<>() { + }; + private static final ParameterizedTypeReference> PLAN_LIST_TYPE = new ParameterizedTypeReference<>() { + }; + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public ServiceOfferingsV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + public List getServiceOfferings() { + return getServiceOfferingResources().stream() + .map(this::toCloudServiceOfferingWithPlans) + .toList(); + } + + // Mirrors the OSS getServiceResources(): list the target space's offerings (space_guids filter), walking all pages. + private List getServiceOfferingResources() { + StringBuilder query = new StringBuilder("/v3/service_offerings?per_page=" + PER_PAGE); + if (target != null && target.getGuid() != null) { + query.append("&space_guids=") + .append(target.getGuid()); + } + return cc.list(query.toString(), OFFERING_LIST_TYPE); + } + + // Mirrors the OSS zipWithAuxiliaryServiceOfferingContent(...): fetch the offering's plans, then map to the domain object. + private CloudServiceOffering toCloudServiceOfferingWithPlans(V3ServiceOffering serviceOffering) { + List servicePlans = getServicePlans(serviceOffering.guid()); + return V3ServiceOfferingMapper.toCloudServiceOffering(serviceOffering, servicePlans); + } + + // Mirrors the OSS getServicePlanResourcesByServiceOfferingGuid(...): list the plans belonging to the given offering, walking all pages. + private List getServicePlans(String serviceOfferingGuid) { + String uri = "/v3/service_plans?service_offering_guids=" + serviceOfferingGuid + "&per_page=" + PER_PAGE; + return cc.list(uri, PLAN_LIST_TYPE) + .stream() + .map(V3ServicePlanMapper::toCloudServicePlan) + .toList(); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServicePlansV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServicePlansV3Operations.java new file mode 100644 index 0000000000..d3e2fd116d --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServicePlansV3Operations.java @@ -0,0 +1,122 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServicePlanVisibility; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServicePlan; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; + +/** + * CF v3 service plans operations of the in-house {@code CloudControllerRestClient}. Reproduces the HTTP shape, filtering and + * broker-scoped fan-out of the OSS {@code CloudControllerRestClientImpl#updateServicePlanVisibilityForBroker} on top of the shared + * {@link CloudControllerV3Client} machinery. + * + *

+ * Endpoint map (see {@code docs/cf-java-client-migration/02-endpoint-inventory.md}): + *

+ *
    + *
  • resolve broker by name → {@code GET /v3/service_brokers?names=} (404 when absent, as the broker is required);
  • + *
  • resolve the broker's offerings → {@code GET /v3/service_offerings?service_broker_guids=&space_guids=};
  • + *
  • resolve each offering's plans → {@code GET /v3/service_plans?service_offering_guids=};
  • + *
  • update visibility → {@code PATCH /v3/service_plans/{guid}/visibility} with body {@code { "type": "" }}.
  • + *
+ * + *

+ * The {@code visibility} body value is the lowercase CF visibility token ({@code public|admin|organization}); this matches the OSS impl, + * which passes {@code Visibility.from(visibility.toString())} where {@link ServicePlanVisibility#toString()} lower-cases the enum name. + *

+ */ +public class ServicePlansV3Operations { + + // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). + private static final int PER_PAGE = 5000; + + private static final ParameterizedTypeReference> BROKER_LIST_TYPE = new ParameterizedTypeReference<>() { + }; + private static final ParameterizedTypeReference> OFFERING_LIST_TYPE = new ParameterizedTypeReference<>() { + }; + private static final ParameterizedTypeReference> PLAN_LIST_TYPE = new ParameterizedTypeReference<>() { + }; + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public ServicePlansV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + public void updateServicePlanVisibilityForBroker(String name, ServicePlanVisibility visibility) { + UUID brokerGuid = getRequiredServiceBrokerGuid(name); + List servicePlanGuids = findServicePlanGuidsByBrokerGuid(brokerGuid); + for (UUID servicePlanGuid : servicePlanGuids) { + updateServicePlanVisibility(servicePlanGuid, visibility); + } + } + + // Mirrors the OSS getServiceBroker(name, true): find the broker by name in the (global) broker list and 404 if it is absent. + private UUID getRequiredServiceBrokerGuid(String name) { + String uri = "/v3/service_brokers?names=" + name + "&per_page=" + PER_PAGE; + return cc.list(uri, BROKER_LIST_TYPE) + .stream() + .findFirst() + .map(broker -> UUID.fromString(broker.guid())) + .orElseThrow(() -> new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", + "Service broker " + name + " not found.")); + } + + // Mirrors the OSS findServicePlansByBrokerGuid: list the broker's offerings (scoped to the target space), then each offering's plans. + private List findServicePlanGuidsByBrokerGuid(UUID brokerGuid) { + List offeringGuids = findServiceOfferingGuidsByBrokerGuid(brokerGuid); + if (offeringGuids.isEmpty()) { + return List.of(); + } + String offeringGuidsFilter = offeringGuids.stream() + .map(UUID::toString) + .reduce((a, b) -> a + "," + b) + .orElse(""); + String uri = "/v3/service_plans?service_offering_guids=" + offeringGuidsFilter + "&per_page=" + PER_PAGE; + return cc.list(uri, PLAN_LIST_TYPE) + .stream() + .map(plan -> UUID.fromString(plan.guid())) + .toList(); + } + + private List findServiceOfferingGuidsByBrokerGuid(UUID brokerGuid) { + StringBuilder query = new StringBuilder("/v3/service_offerings?service_broker_guids=" + brokerGuid + "&per_page=" + PER_PAGE); + if (target != null && target.getGuid() != null) { + query.append("&space_guids=") + .append(target.getGuid()); + } + return cc.list(query.toString(), OFFERING_LIST_TYPE) + .stream() + .map(offering -> UUID.fromString(offering.guid())) + .toList(); + } + + // Mirrors the OSS updateServicePlanVisibility(UUID, ServicePlanVisibility): PATCH the plan's visibility with the lowercase CF token. + private void updateServicePlanVisibility(UUID servicePlanGuid, ServicePlanVisibility visibility) { + cc.getRestClient() + .patch() + .uri("/v3/service_plans/{guid}/visibility", servicePlanGuid) + .body(Map.of("type", visibility.toString())) + .retrieve() + .toBodilessEntity(); + } + + // Minimal wire models to resolve GUIDs by name/relationship without depending on the (separately-owned) broker/offering groups. + @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) + private record V3ServiceBrokerRef(@com.fasterxml.jackson.annotation.JsonProperty("guid") String guid) { + } + + @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) + private record V3ServiceOfferingRef(@com.fasterxml.jackson.annotation.JsonProperty("guid") String guid) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/StacksV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/StacksV3Operations.java new file mode 100644 index 0000000000..18e8537756 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/StacksV3Operations.java @@ -0,0 +1,63 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.util.List; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudStack; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Stack; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3StackMapper; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; + +/** + * CF v3 stacks operations of the cf-java-client replacement. Reproduces the HTTP shape and domain mapping of the OSS + * {@code CloudControllerRestClientImpl} stack methods: + *
    + *
  • {@code getStacks()} → {@code GET /v3/stacks} (paginated);
  • + *
  • {@code getStack(name[, required])} → {@code GET /v3/stacks?names=}, taking the first match.
  • + *
+ * Stacks are a global resource, so — like the OSS impl — the listing is not filtered by the target space. + */ +public class StacksV3Operations { + + private static final ParameterizedTypeReference> STACK_PAGE = new ParameterizedTypeReference<>() { + }; + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public StacksV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + public CloudStack getStack(String name) { + return getStack(name, true); + } + + public CloudStack getStack(String name, boolean required) { + CloudStack stack = findStackByName(name); + if (stack == null && required) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Stack " + name + " not found."); + } + return stack; + } + + public List getStacks() { + return cc.list("/v3/stacks?per_page=5000", STACK_PAGE) + .stream() + .map(V3StackMapper::toCloudStack) + .toList(); + } + + private CloudStack findStackByName(String name) { + return cc.list("/v3/stacks?names=" + name + "&per_page=5000", STACK_PAGE) + .stream() + .map(V3StackMapper::toCloudStack) + .findFirst() + .orElse(null); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/TasksV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/TasksV3Operations.java new file mode 100644 index 0000000000..0fd5155e8d --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/TasksV3Operations.java @@ -0,0 +1,112 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudTask; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Application; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Task; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3TaskMapper; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; + +/** + * CF v3 task operations for the cf-java-client replacement. Reproduces the HTTP shape and domain-mapping of the OSS + * {@code CloudControllerRestClientImpl} task methods: + *
    + *
  • {@code getTask} → {@code GET /v3/tasks/{guid}}
  • + *
  • {@code getTasks} → {@code GET /v3/tasks?app_guids={appGuid}} (paginated)
  • + *
  • {@code runTask} → {@code POST /v3/apps/{appGuid}/tasks}
  • + *
  • {@code cancelTask} → {@code POST /v3/tasks/{guid}/actions/cancel}
  • + *
+ * Application-name resolution mirrors the OSS impl's {@code getRequiredApplicationGuid}: it queries the target space and throws + * {@link CloudOperationException} with {@link HttpStatus#NOT_FOUND} when the application does not exist. + */ +public class TasksV3Operations { + + // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). + private static final int DEFAULT_PAGE_SIZE = 5000; + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public TasksV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + public CloudTask getTask(UUID taskGuid) { + V3Task task = cc.get("/v3/tasks/" + taskGuid, V3Task.class); + return task == null ? null : V3TaskMapper.toCloudTask(task); + } + + public List getTasks(String applicationName) { + UUID applicationGuid = getRequiredApplicationGuid(applicationName); + String query = "/v3/tasks?per_page=" + DEFAULT_PAGE_SIZE + "&app_guids=" + applicationGuid; + List tasks = cc.list(query, new ParameterizedTypeReference>() { + }); + return tasks.stream() + .map(V3TaskMapper::toCloudTask) + .toList(); + } + + public CloudTask runTask(String applicationName, CloudTask task) { + UUID applicationGuid = getRequiredApplicationGuid(applicationName); + V3Task created = cc.getRestClient() + .post() + .uri("/v3/apps/{guid}/tasks", applicationGuid) + .body(buildCreateTaskBody(task)) + .retrieve() + .body(V3Task.class); + return created == null ? null : V3TaskMapper.toCloudTask(created); + } + + public CloudTask cancelTask(UUID taskGuid) { + V3Task cancelled = cc.getRestClient() + .post() + .uri("/v3/tasks/{guid}/actions/cancel", taskGuid) + .retrieve() + .body(V3Task.class); + return cancelled == null ? null : V3TaskMapper.toCloudTask(cancelled); + } + + private Map buildCreateTaskBody(CloudTask task) { + Map body = new HashMap<>(); + body.put("command", task.getCommand()); + body.put("name", task.getName()); + CloudTask.Limits limits = task.getLimits(); + if (limits != null) { + if (limits.getMemory() != null) { + body.put("memory_in_mb", limits.getMemory()); + } + if (limits.getDisk() != null) { + body.put("disk_in_mb", limits.getDisk()); + } + } + return body; + } + + // Mirrors the OSS impl's getRequiredApplicationGuid: look up the app in the target space and 404 if it is absent. + private UUID getRequiredApplicationGuid(String applicationName) { + StringBuilder query = new StringBuilder("/v3/apps?per_page=" + DEFAULT_PAGE_SIZE); + if (target != null && target.getGuid() != null) { + query.append("&space_guids=") + .append(target.getGuid()); + } + query.append("&names=") + .append(applicationName); + List apps = cc.list(query.toString(), new ParameterizedTypeReference>() { + }); + if (apps.isEmpty()) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + applicationName + " not found."); + } + return UUID.fromString(apps.get(0) + .guid()); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Application.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Application.java new file mode 100644 index 0000000000..2eab011a70 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Application.java @@ -0,0 +1,53 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 application resource ({@code GET/POST /v3/apps}). + * + *
+ * { "guid": "...", "name": "...", "state": "STARTED|STOPPED", "created_at": "...", "updated_at": "...",
+ *   "lifecycle": { "type": "buildpack|docker|cnb", "data": { "buildpacks": [...], "stack": "..." } },
+ *   "metadata": { "labels": {...}, "annotations": {...} },
+ *   "relationships": { "space": { "data": { "guid": "..." } } } }
+ * 
+ */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3Application(@JsonProperty("guid") String guid, @JsonProperty("name") String name, @JsonProperty("state") String state, + @JsonProperty("created_at") String createdAt, @JsonProperty("updated_at") String updatedAt, + @JsonProperty("lifecycle") V3Lifecycle lifecycle, @JsonProperty("metadata") V3Metadata metadata, + @JsonProperty("relationships") V3Relationships relationships) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Lifecycle(@JsonProperty("type") String type, @JsonProperty("data") V3LifecycleData data) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3LifecycleData(@JsonProperty("buildpacks") List buildpacks, @JsonProperty("stack") String stack) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Relationships(@JsonProperty("space") V3ToOneRelationship space) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3ToOneRelationship(@JsonProperty("data") V3RelationshipData data) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3RelationshipData(@JsonProperty("guid") String guid) { + } + + /** + * The CF v3 app environment variables response ({@code GET /v3/apps/{guid}/environment_variables}): + * {@code { "var": { "KEY": "VALUE", ... } }}. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3EnvironmentVariables(@JsonProperty("var") Map var) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ApplicationMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ApplicationMapper.java new file mode 100644 index 0000000000..ae4de63eb3 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ApplicationMapper.java @@ -0,0 +1,59 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.Collections; +import java.util.Map; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; +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.ImmutableLifecycle; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Lifecycle; +import org.cloudfoundry.multiapps.controller.client.facade.domain.LifecycleType; + +/** + * Maps the {@link V3Application} wire model to the project's {@link CloudApplication} domain object. Mirrors the OSS + * {@code RawCloudApplication} adapter field-for-field, so both client implementations yield identical domain objects. + */ +public final class V3ApplicationMapper { + + private static final String BUILDPACKS = "buildpacks"; + private static final String STACK = "stack"; + + private V3ApplicationMapper() { + } + + public static CloudApplication toCloudApplication(V3Application app, CloudSpace space) { + return ImmutableCloudApplication.builder() + .metadata(V3ResourceMappers.parseMetadata(app.guid(), app.createdAt(), app.updatedAt())) + .v3Metadata(V3ResourceMappers.toV3Metadata(app.metadata())) + .name(app.name()) + .state(parseState(app.state())) + .lifecycle(parseLifecycle(app.lifecycle())) + .space(space) + .build(); + } + + private static CloudApplication.State parseState(String state) { + return state == null ? null : CloudApplication.State.valueOf(state); + } + + private static Lifecycle parseLifecycle(V3Application.V3Lifecycle lifecycle) { + if (lifecycle == null || lifecycle.type() == null) { + return null; + } + return ImmutableLifecycle.builder() + .type(LifecycleType.valueOf(lifecycle.type() + .toUpperCase())) + .data(extractLifecycleData(lifecycle.data())) + .build(); + } + + private static Map extractLifecycleData(V3Application.V3LifecycleData data) { + if (data == null) { + return Collections.emptyMap(); + } + return Map.of(BUILDPACKS, data.buildpacks() == null ? Collections.emptyList() : data.buildpacks(), + STACK, data.stack() == null ? "" : data.stack()); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEvent.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEvent.java new file mode 100644 index 0000000000..de24e0babc --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEvent.java @@ -0,0 +1,30 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 audit event resource ({@code GET /v3/audit_events}). + * + *
+ * { "guid": "...", "created_at": "...", "updated_at": "...", "type": "audit.app.update",
+ *   "actor":  { "guid": "...", "type": "user",        "name": "..." },
+ *   "target": { "guid": "...", "type": "app",         "name": "..." },
+ *   "data": { ... }, "space": {...}, "organization": {...} }
+ * 
+ * + * Only the fields the domain {@code CloudEvent} needs are mapped; everything else is ignored so CF can evolve the payload without breaking + * us. Mirrors the CF v3 JSON consumed by the OSS {@code AuditEventResource} adapter (whose {@code actor}/{@code target} carry + * {@code guid}/{@code name}/{@code type}). + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3AuditEvent(@JsonProperty("guid") String guid, @JsonProperty("created_at") String createdAt, + @JsonProperty("updated_at") String updatedAt, @JsonProperty("type") String type, + @JsonProperty("actor") V3Participant actor, @JsonProperty("target") V3Participant target) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Participant(@JsonProperty("guid") String guid, @JsonProperty("type") String type, + @JsonProperty("name") String name) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEventMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEventMapper.java new file mode 100644 index 0000000000..ede9fc9837 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEventMapper.java @@ -0,0 +1,40 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEvent; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEvent.Participant; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudEvent; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudEvent.ImmutableParticipant; + +/** + * Maps the {@link V3AuditEvent} wire model to the project's {@link CloudEvent} domain object. Mirrors the OSS {@code RawCloudEvent} adapter + * field-for-field, so both client implementations yield identical domain objects. + *

+ * As in the OSS adapter, a {@code null} actor/target still yields an empty (non-{@code null}) {@link Participant}. + */ +public final class V3AuditEventMapper { + + private V3AuditEventMapper() { + } + + public static CloudEvent toCloudEvent(V3AuditEvent event) { + return ImmutableCloudEvent.builder() + .metadata(V3ResourceMappers.parseMetadata(event.guid(), event.createdAt(), event.updatedAt())) + .target(parseParticipant(event.target())) + .actor(parseParticipant(event.actor())) + .type(event.type()) + .build(); + } + + private static Participant parseParticipant(V3AuditEvent.V3Participant participant) { + if (participant == null) { + return ImmutableParticipant.builder() + .build(); + } + return ImmutableParticipant.builder() + .guid(V3ResourceMappers.parseNullableGuid(participant.guid())) + .name(participant.name()) + .type(participant.type()) + .build(); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Build.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Build.java new file mode 100644 index 0000000000..15fd5ec357 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Build.java @@ -0,0 +1,35 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 build resource ({@code GET/POST /v3/builds}, {@code GET /v3/apps/{guid}/builds}). + * + *

+ * { "guid": "...", "created_at": "...", "updated_at": "...",
+ *   "state": "STAGING|STAGED|FAILED", "error": "...",
+ *   "created_by": { "guid": "...", "name": "..." },
+ *   "package": { "guid": "..." },
+ *   "droplet": { "guid": "..." } }
+ * 
+ */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3Build(@JsonProperty("guid") String guid, @JsonProperty("created_at") String createdAt, + @JsonProperty("updated_at") String updatedAt, @JsonProperty("state") String state, + @JsonProperty("error") String error, @JsonProperty("created_by") V3CreatedBy createdBy, + @JsonProperty("package") V3PackageReference inputPackage, @JsonProperty("droplet") V3DropletReference droplet) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3CreatedBy(@JsonProperty("guid") String guid, @JsonProperty("name") String name) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3PackageReference(@JsonProperty("guid") String guid) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3DropletReference(@JsonProperty("guid") String guid) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3BuildMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3BuildMapper.java new file mode 100644 index 0000000000..c40417b701 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3BuildMapper.java @@ -0,0 +1,60 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudBuild; +import org.cloudfoundry.multiapps.controller.client.facade.domain.DropletInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudBuild; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudBuild.ImmutableCreatedBy; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudBuild.ImmutablePackageInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableDropletInfo; + +/** + * Maps the {@link V3Build} wire model to the project's {@link CloudBuild} domain object. Mirrors the OSS {@code RawCloudBuild} adapter + * field-for-field, so both client implementations yield identical domain objects. + */ +public final class V3BuildMapper { + + private V3BuildMapper() { + } + + public static CloudBuild toCloudBuild(V3Build build) { + return ImmutableCloudBuild.builder() + .metadata(V3ResourceMappers.parseMetadata(build.guid(), build.createdAt(), build.updatedAt())) + .createdBy(parseCreatedBy(build.createdBy())) + .packageInfo(parsePackageInfo(build.inputPackage())) + .dropletInfo(parseDropletInfo(build.droplet())) + .state(parseState(build.state())) + .error(build.error()) + .build(); + } + + private static CloudBuild.CreatedBy parseCreatedBy(V3Build.V3CreatedBy createdBy) { + if (createdBy == null) { + return null; + } + return ImmutableCreatedBy.builder() + .guid(V3ResourceMappers.parseNullableGuid(createdBy.guid())) + .name(createdBy.name()) + .build(); + } + + private static CloudBuild.PackageInfo parsePackageInfo(V3Build.V3PackageReference inputPackage) { + if (inputPackage == null) { + return null; + } + return ImmutablePackageInfo.of(V3ResourceMappers.parseNullableGuid(inputPackage.guid())); + } + + private static DropletInfo parseDropletInfo(V3Build.V3DropletReference droplet) { + if (droplet == null || droplet.guid() == null) { + return null; + } + return ImmutableDropletInfo.builder() + .guid(V3ResourceMappers.parseNullableGuid(droplet.guid())) + .build(); + } + + private static CloudBuild.State parseState(String state) { + return state == null ? null : CloudBuild.State.valueOf(state); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Domain.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Domain.java new file mode 100644 index 0000000000..8bc9403d99 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Domain.java @@ -0,0 +1,45 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 domain resource ({@code GET/POST /v3/domains}, {@code GET /v3/organizations/{guid}/domains}, + * {@code GET /v3/organizations/{guid}/domains/default}). + * + *
+ * { "guid": "...", "name": "example.com", "created_at": "...", "updated_at": "...",
+ *   "metadata": { "labels": {...}, "annotations": {...} },
+ *   "relationships": { "organization": { "data": { "guid": "..." } | null } } }
+ * 
+ * + * The {@code relationships.organization.data} discriminates shared domains (data == null) from private domains (data != null); this + * mirrors the filtering the OSS impl applied on {@code DomainResource.getRelationships().getOrganization().getData()}. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3Domain(@JsonProperty("guid") String guid, @JsonProperty("name") String name, + @JsonProperty("created_at") String createdAt, @JsonProperty("updated_at") String updatedAt, + @JsonProperty("metadata") V3Metadata metadata, @JsonProperty("relationships") V3DomainRelationships relationships) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3DomainRelationships(@JsonProperty("organization") V3ToOneRelationship organization) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3ToOneRelationship(@JsonProperty("data") V3RelationshipData data) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3RelationshipData(@JsonProperty("guid") String guid) { + } + + /** + * True when this domain is scoped to an organization (a private domain), i.e. its {@code relationships.organization.data} is present. + * A shared domain has {@code data == null}. Mirrors the OSS {@code getPrivateDomainResources}/{@code getSharedDomainResources} filters. + */ + public boolean isPrivate() { + return relationships != null && relationships.organization() != null && relationships.organization() + .data() != null; + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3DomainMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3DomainMapper.java new file mode 100644 index 0000000000..98e1c96bb8 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3DomainMapper.java @@ -0,0 +1,22 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudDomain; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudDomain; + +/** + * Maps the {@link V3Domain} wire model to the project's {@link CloudDomain} domain object. Mirrors the OSS {@code RawCloudDomain} adapter + * field-for-field (metadata from the resource envelope + {@code name}), so both client implementations yield identical domain objects. + */ +public final class V3DomainMapper { + + private V3DomainMapper() { + } + + public static CloudDomain toCloudDomain(V3Domain domain) { + return ImmutableCloudDomain.builder() + .metadata(V3ResourceMappers.parseMetadata(domain.guid(), domain.createdAt(), domain.updatedAt())) + .name(domain.name()) + .build(); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Droplet.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Droplet.java new file mode 100644 index 0000000000..5281407ddd --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Droplet.java @@ -0,0 +1,24 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of the CF v3 current-droplet response ({@code GET /v3/apps/{guid}/droplets/current}): + * + *
+ * { "guid": "...", "links": { "package": { "href": "https://.../v3/packages/{guid}" }, ... } }
+ * 
+ * + * The client only needs the droplet {@code guid} and the {@code package} link, from which the package GUID is derived. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3Droplet(@JsonProperty("guid") String guid, @JsonProperty("links") Map links) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Link(@JsonProperty("href") String href) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3InstancesInfoMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3InstancesInfoMapper.java new file mode 100644 index 0000000000..f111e8acc6 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3InstancesInfoMapper.java @@ -0,0 +1,49 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableInstanceInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableInstancesInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.InstanceInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.InstanceState; +import org.cloudfoundry.multiapps.controller.client.facade.domain.InstancesInfo; + +/** + * Maps the {@link V3Process.V3ProcessStats} wire model to the project's {@link InstancesInfo} domain object. Mirrors the OSS + * {@code RawInstancesInfo} adapter field-for-field, so both client implementations yield identical domain objects. + *

+ * The instance state string comes from CF ({@code GET /v3/apps/{guid}/processes/web/stats}) and is resolved via + * {@link InstanceState#valueOfWithDefault(String)} (which falls back to {@link InstanceState#UNKNOWN} for unrecognised values), so no OSS + * leaf type is involved. + */ +public final class V3InstancesInfoMapper { + + private V3InstancesInfoMapper() { + } + + public static InstancesInfo toInstancesInfo(V3Process.V3ProcessStats processStats) { + return ImmutableInstancesInfo.builder() + .instances(parseProcessStatistics(processStats.resources())) + .build(); + } + + private static List parseProcessStatistics(List stats) { + if (stats == null) { + return Collections.emptyList(); + } + return stats.stream() + .map(V3InstancesInfoMapper::parseProcessStatistic) + .collect(Collectors.toList()); + } + + private static InstanceInfo parseProcessStatistic(V3Process.V3ProcessStatsResource statsResource) { + return ImmutableInstanceInfo.builder() + .index(statsResource.index()) + .state(InstanceState.valueOfWithDefault(statsResource.state())) + .isRoutable(Boolean.parseBoolean(statsResource.routable())) + .build(); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Job.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Job.java new file mode 100644 index 0000000000..84c55b5be2 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Job.java @@ -0,0 +1,45 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a Cloud Controller v3 job resource ({@code GET /v3/jobs/{guid}}). + * + *

+ * { "guid": "...", "created_at": "...", "updated_at": "...", "state": "PROCESSING|POLLING|COMPLETE|FAILED", "operation": "...",
+ *   "errors": [ { "detail": "...", "title": "...", "code": N } ], "warnings": [ { "detail": "..." } ] }
+ * 
+ * + * The async job poller in {@link org.cloudfoundry.multiapps.controller.client.facade.rest.CloudControllerV3Client} polls this until + * {@code state} reaches a terminal value (COMPLETE / FAILED), mirroring the OSS {@code JobV3Util} behaviour. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3Job(@JsonProperty("guid") String guid, @JsonProperty("created_at") String createdAt, + @JsonProperty("updated_at") String updatedAt, @JsonProperty("state") String state, + @JsonProperty("operation") String operation, @JsonProperty("errors") List errors, + @JsonProperty("warnings") List warnings) { + + public boolean isComplete() { + return "COMPLETE".equals(state); + } + + public boolean isFailed() { + return "FAILED".equals(state); + } + + public boolean isTerminal() { + return isComplete() || isFailed(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Error(@JsonProperty("detail") String detail, @JsonProperty("title") String title, @JsonProperty("code") Integer code) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Warning(@JsonProperty("detail") String detail) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3JobMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3JobMapper.java new file mode 100644 index 0000000000..89a153a5e4 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3JobMapper.java @@ -0,0 +1,57 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.List; +import java.util.stream.Collectors; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudAsyncJob; +import org.cloudfoundry.multiapps.controller.client.facade.domain.JobState; + +/** + * Maps the {@link V3Job} wire model to the project's {@link CloudAsyncJob} domain object. Mirrors the OSS {@code RawCloudAsyncJob} adapter + * field-for-field, so both client implementations yield identical domain objects. + */ +public final class V3JobMapper { + + private V3JobMapper() { + } + + public static CloudAsyncJob toCloudAsyncJob(V3Job job) { + return ImmutableCloudAsyncJob.builder() + .metadata(V3ResourceMappers.parseMetadata(job.guid(), job.createdAt(), job.updatedAt())) + .state(parseState(job.state())) + .operation(job.operation()) + .warnings(getWarnings(job)) + .errors(getErrors(job)) + .build(); + } + + private static JobState parseState(String state) { + return state == null ? null : JobState.valueOf(state.toUpperCase()); + } + + private static String getWarnings(V3Job job) { + List warnings = job.warnings(); + if (warnings == null) { + return ""; + } + return warnings.stream() + .map(V3Job.V3Warning::detail) + .collect(Collectors.joining(",")); + } + + private static String getErrors(V3Job job) { + List errors = job.errors(); + if (errors == null) { + return ""; + } + return errors.stream() + .map(V3JobMapper::joinErrorDetails) + .collect(Collectors.joining(",")); + } + + private static String joinErrorDetails(V3Job.V3Error error) { + return String.join(" ", String.valueOf(error.code()), error.title(), error.detail()); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ListResponse.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ListResponse.java new file mode 100644 index 0000000000..43e91ce3a5 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ListResponse.java @@ -0,0 +1,49 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a Cloud Controller v3 list response envelope: + * + *
+ * {
+ *   "pagination": { "total_results": N, "next": { "href": "..." } | null, ... },
+ *   "resources":  [ ... ]
+ * }
+ * 
+ * + * Only the fields the client actually needs are mapped; everything else is ignored so CF can evolve the payload without breaking us. + * The {@link org.cloudfoundry.multiapps.controller.client.facade.rest.CloudControllerV3Client pagination walker} follows + * {@code pagination.next.href} until it is {@code null}, accumulating {@link #resources()} from every page. + * + * @param the wire type of a single resource in this list + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3ListResponse(@JsonProperty("pagination") Pagination pagination, @JsonProperty("resources") List resources) { + + public List resources() { + return resources == null ? Collections.emptyList() : resources; + } + + public String nextPageHref() { + if (pagination == null || pagination.next() == null) { + return null; + } + return pagination.next() + .href(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record Pagination(@JsonProperty("total_results") Integer totalResults, @JsonProperty("total_pages") Integer totalPages, + @JsonProperty("next") Link next, @JsonProperty("previous") Link previous) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record Link(@JsonProperty("href") String href) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Metadata.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Metadata.java new file mode 100644 index 0000000000..ef50b44d5c --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Metadata.java @@ -0,0 +1,24 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.Collections; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of the CF v3 {@code metadata} object (labels + annotations) attached to most resources. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3Metadata(@JsonProperty("labels") Map labels, + @JsonProperty("annotations") Map annotations) { + + public Map labels() { + return labels == null ? Collections.emptyMap() : labels; + } + + public Map annotations() { + return annotations == null ? Collections.emptyMap() : annotations; + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Package.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Package.java new file mode 100644 index 0000000000..c67ff92e0b --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Package.java @@ -0,0 +1,36 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 package resource ({@code GET/POST /v3/packages}, {@code GET /v3/apps/{guid}/packages}). + * + *
+ * { "guid": "...", "type": "bits|docker", "state": "AWAITING_UPLOAD|PROCESSING_UPLOAD|READY|FAILED|COPYING|EXPIRED",
+ *   "created_at": "...", "updated_at": "...",
+ *   "data": {
+ *     // bits:
+ *     "checksum": { "type": "sha256", "value": "..." }, "error": "...",
+ *     // docker:
+ *     "image": "...", "username": "...", "password": "..."
+ *   },
+ *   "relationships": { "app": { "data": { "guid": "..." } } } }
+ * 
+ */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3Package(@JsonProperty("guid") String guid, @JsonProperty("type") String type, @JsonProperty("state") String state, + @JsonProperty("created_at") String createdAt, @JsonProperty("updated_at") String updatedAt, + @JsonProperty("data") V3PackageData data) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3PackageData(@JsonProperty("checksum") V3Checksum checksum, @JsonProperty("error") String error, + @JsonProperty("image") String image, @JsonProperty("username") String username, + @JsonProperty("password") String password) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Checksum(@JsonProperty("type") String type, @JsonProperty("value") String value) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3PackageMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3PackageMapper.java new file mode 100644 index 0000000000..4f62f4b347 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3PackageMapper.java @@ -0,0 +1,76 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudPackage; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableBitsData; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudPackage; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableDockerData; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Status; + +/** + * Maps the {@link V3Package} wire model to the project's {@link CloudPackage} domain object. Mirrors the OSS {@code RawCloudPackage} + * adapter field-for-field, so both client implementations yield identical domain objects. + */ +public final class V3PackageMapper { + + private V3PackageMapper() { + } + + public static CloudPackage toCloudPackage(V3Package resource) { + return ImmutableCloudPackage.builder() + .metadata(V3ResourceMappers.parseMetadata(resource.guid(), resource.createdAt(), + resource.updatedAt())) + .status(parseStatus(resource)) + .data(parseData(resource)) + .type(parseType(resource)) + .build(); + } + + private static Status parseStatus(V3Package resource) { + return resource.state() == null ? null : Status.valueOf(resource.state()); + } + + private static CloudPackage.Type parseType(V3Package resource) { + return resource.type() == null ? null : CloudPackage.Type.from(resource.type()); + } + + private static CloudPackage.PackageData parseData(V3Package resource) { + if (resource.type() != null && CloudPackage.Type.from(resource.type()) == CloudPackage.Type.BITS) { + return parseBitsData(resource.data()); + } + return parseDockerData(resource.data()); + } + + private static CloudPackage.PackageData parseBitsData(V3Package.V3PackageData data) { + if (data == null) { + return ImmutableBitsData.builder() + .build(); + } + return ImmutableBitsData.builder() + .checksum(parseBitsChecksum(data.checksum())) + .error(data.error()) + .build(); + } + + private static org.cloudfoundry.multiapps.controller.client.facade.domain.BitsData.Checksum + parseBitsChecksum(V3Package.V3Checksum checksum) { + if (checksum == null) { + return null; + } + return ImmutableBitsData.ImmutableChecksum.builder() + .algorithm(checksum.type()) + .value(checksum.value()) + .build(); + } + + private static CloudPackage.PackageData parseDockerData(V3Package.V3PackageData data) { + if (data == null) { + return null; + } + return ImmutableDockerData.builder() + .image(data.image()) + .username(data.username()) + .password(data.password()) + .build(); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Process.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Process.java new file mode 100644 index 0000000000..263a4d29dc --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Process.java @@ -0,0 +1,71 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-models for the CF v3 process family of endpoints consumed by the client: + *
    + *
  • {@link V3Process} — {@code GET /v3/apps/{guid}/processes/web} (also {@code GET /v3/processes/{guid}});
  • + *
  • {@link V3ProcessStats} — {@code GET /v3/apps/{guid}/processes/web/stats};
  • + *
  • {@link V3AppFeatures} / {@link V3AppFeature} — {@code GET /v3/apps/{guid}/features};
  • + *
  • {@link V3SshEnabled} — {@code GET /v3/apps/{guid}/ssh_enabled}.
  • + *
+ * + *
+ * // process
+ * { "guid": "...", "command": "...", "instances": 1, "memory_in_mb": 256, "disk_in_mb": 1024,
+ *   "health_check": { "type": "port|http|process", "data": { "timeout": N, "invocation_timeout": N, "endpoint": "...", "interval": N } },
+ *   "readiness_health_check": { "type": "process|http|port", "data": { "invocation_timeout": N, "endpoint": "...", "interval": N } } }
+ * 
+ * + * Every record ignores unknown fields so CF can evolve the payload without breaking us. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3Process(@JsonProperty("guid") String guid, @JsonProperty("command") String command, + @JsonProperty("instances") Integer instances, @JsonProperty("memory_in_mb") Integer memoryInMb, + @JsonProperty("disk_in_mb") Integer diskInMb, @JsonProperty("health_check") V3HealthCheck healthCheck, + @JsonProperty("readiness_health_check") V3HealthCheck readinessHealthCheck) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3HealthCheck(@JsonProperty("type") String type, @JsonProperty("data") V3HealthCheckData data) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3HealthCheckData(@JsonProperty("timeout") Integer timeout, + @JsonProperty("invocation_timeout") Integer invocationTimeout, + @JsonProperty("endpoint") String endpoint, @JsonProperty("interval") Integer interval) { + } + + /** + * The CF v3 process statistics response ({@code GET /v3/apps/{guid}/processes/web/stats}): + * {@code { "resources": [ { "index": 0, "state": "RUNNING", "routable": true }, ... ] }}. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3ProcessStats(@JsonProperty("resources") List resources) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3ProcessStatsResource(@JsonProperty("index") Integer index, @JsonProperty("state") String state, + @JsonProperty("routable") String routable) { + } + + /** + * The CF v3 app features response ({@code GET /v3/apps/{guid}/features}): + * {@code { "resources": [ { "name": "ssh", "enabled": true }, ... ] }}. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3AppFeature(@JsonProperty("name") String name, @JsonProperty("enabled") Boolean enabled) { + } + + /** + * The CF v3 app SSH-enabled response ({@code GET /v3/apps/{guid}/ssh_enabled}): + * {@code { "enabled": true, "reason": "..." }}. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3SshEnabled(@JsonProperty("enabled") Boolean enabled, @JsonProperty("reason") String reason) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ProcessMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ProcessMapper.java new file mode 100644 index 0000000000..94de49472a --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ProcessMapper.java @@ -0,0 +1,60 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudProcess; +import org.cloudfoundry.multiapps.controller.client.facade.domain.HealthCheckType; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudProcess; + +/** + * Maps the {@link V3Process} wire model to the project's {@link CloudProcess} domain object. Mirrors the OSS + * {@code RawCloudProcess} adapter field-for-field, so both client implementations yield identical domain objects. + */ +public final class V3ProcessMapper { + + private V3ProcessMapper() { + } + + public static CloudProcess toCloudProcess(V3Process process) { + V3Process.V3HealthCheck healthCheck = process.healthCheck(); + V3Process.V3HealthCheck readinessHealthCheck = process.readinessHealthCheck(); + + Integer healthCheckTimeout = null; + String healthCheckHttpEndpoint = null; + Integer healthCheckInvocationTimeout = null; + Integer healthCheckInterval = null; + if (healthCheck != null && healthCheck.data() != null) { + V3Process.V3HealthCheckData healthCheckData = healthCheck.data(); + healthCheckTimeout = healthCheckData.timeout(); + healthCheckInvocationTimeout = healthCheckData.invocationTimeout(); + healthCheckHttpEndpoint = healthCheckData.endpoint(); + healthCheckInterval = healthCheckData.interval(); + } + + Integer readinessHealthCheckInvocationTimeout = null; + String readinessHealthCheckHttpEndpoint = null; + Integer readinessHealthCheckInterval = null; + if (readinessHealthCheck != null && readinessHealthCheck.data() != null) { + V3Process.V3HealthCheckData readinessHealthCheckData = readinessHealthCheck.data(); + readinessHealthCheckInvocationTimeout = readinessHealthCheckData.invocationTimeout(); + readinessHealthCheckHttpEndpoint = readinessHealthCheckData.endpoint(); + readinessHealthCheckInterval = readinessHealthCheckData.interval(); + } + + return ImmutableCloudProcess.builder() + .command(process.command()) + .instances(process.instances()) + .memoryInMb(process.memoryInMb()) + .diskInMb(process.diskInMb()) + .healthCheckType(HealthCheckType.valueOf(healthCheck.type() + .toUpperCase())) + .healthCheckHttpEndpoint(healthCheckHttpEndpoint) + .healthCheckTimeout(healthCheckTimeout) + .healthCheckInvocationTimeout(healthCheckInvocationTimeout) + .healthCheckInterval(healthCheckInterval) + .readinessHealthCheckType(readinessHealthCheck == null ? null : readinessHealthCheck.type()) + .readinessHealthCheckHttpEndpoint(readinessHealthCheckHttpEndpoint) + .readinessHealthCheckInvocationTimeout(readinessHealthCheckInvocationTimeout) + .readinessHealthCheckInterval(readinessHealthCheckInterval) + .build(); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ResourceMappers.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ResourceMappers.java new file mode 100644 index 0000000000..e5fc872a2b --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ResourceMappers.java @@ -0,0 +1,73 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudMetadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helpers shared by all CF v3 wire-model mappers: parse the resource envelope fields ({@code guid}, {@code created_at}, + * {@code updated_at}) into the project's {@link CloudMetadata}, and parse GUIDs/dates leniently — mirroring the behaviour of the OSS + * {@code RawCloudEntity} so the two client implementations produce identical domain objects. + */ +public final class V3ResourceMappers { + + private static final Logger LOGGER = LoggerFactory.getLogger(V3ResourceMappers.class); + + private V3ResourceMappers() { + } + + public static CloudMetadata parseMetadata(String guid, String createdAt, String updatedAt) { + return ImmutableCloudMetadata.builder() + .guid(parseNullableGuid(guid)) + .createdAt(parseNullableDate(createdAt)) + .updatedAt(parseNullableDate(updatedAt)) + .build(); + } + + /** + * Build the project's v3 {@link Metadata} (labels/annotations) from the wire representation. Kept as the OSS leaf type for now; it + * is re-homed to a project-owned type in the leaf-type migration step. + */ + public static Metadata toV3Metadata(V3Metadata metadata) { + if (metadata == null) { + return null; + } + return Metadata.builder() + .labels(metadata.labels()) + .annotations(metadata.annotations()) + .build(); + } + + public static UUID parseNullableGuid(String guid) { + if (guid == null) { + return null; + } + try { + return UUID.fromString(guid); + } catch (IllegalArgumentException e) { + LOGGER.warn("Could not parse GUID string: \"{}\"", guid, e); + return null; + } + } + + public static LocalDateTime parseNullableDate(String date) { + if (date == null) { + return null; + } + try { + // CF v3 timestamps are ISO-8601 with offset, e.g. 2026-08-04T10:15:30Z + return java.time.OffsetDateTime.parse(date) + .toLocalDateTime(); + } catch (DateTimeParseException e) { + LOGGER.warn("Could not parse date string: \"{}\"", date, e); + return null; + } + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Role.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Role.java new file mode 100644 index 0000000000..f280bc0267 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Role.java @@ -0,0 +1,22 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 role resource ({@code GET /v3/roles}). + * + *
+ * { "guid": "...", "created_at": "...", "updated_at": "...",
+ *   "type": "organization_auditor|organization_billing_manager|organization_manager|organization_user|space_auditor|space_developer|space_manager",
+ *   "relationships": { "user": { "data": { "guid": "..." } }, "space": { "data": { "guid": "..." } | null },
+ *                      "organization": { "data": { "guid": "..." } | null } } }
+ * 
+ * + * Only {@code type} is consumed by the mapper (mirroring the OSS {@code RawUserRole}); the remaining fields are documented for + * completeness and ignored so CF can evolve the payload without breaking us. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3Role(@JsonProperty("guid") String guid, @JsonProperty("created_at") String createdAt, + @JsonProperty("updated_at") String updatedAt, @JsonProperty("type") String type) { +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RoleMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RoleMapper.java new file mode 100644 index 0000000000..5b75520137 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RoleMapper.java @@ -0,0 +1,18 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.UserRole; + +/** + * Maps the {@link V3Role} wire model to the project's {@link UserRole} domain enum. Passes the raw CF v3 role {@code type} wire value + * (e.g. {@code space_developer}) straight to {@link UserRole#fromRoleType(String)}, so this mapper depends on no OSS types. + */ +public final class V3RoleMapper { + + private V3RoleMapper() { + } + + public static UserRole toUserRole(V3Role role) { + return UserRole.fromRoleType(role.type()); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Route.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Route.java new file mode 100644 index 0000000000..6cc6e7f991 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Route.java @@ -0,0 +1,57 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 route resource ({@code GET/POST /v3/routes}, {@code GET /v3/apps/{guid}/routes}). + * + *
+ * { "guid": "...", "host": "...", "path": "...", "port": 8080, "url": "host.domain/path",
+ *   "created_at": "...", "updated_at": "...",
+ *   "destinations": [ { "guid": "...", "app": { "guid": "..." }, "port": 8080, "weight": 1, "protocol": "http1" } ],
+ *   "metadata": { "labels": {...}, "annotations": {...} },
+ *   "relationships": { "space": { "data": { "guid": "..." } }, "domain": { "data": { "guid": "..." } } } }
+ * 
+ * + * Mirrors exactly the fields the OSS {@code RawCloudRoute} adapter consumes from {@code org.cloudfoundry.client.v3.routes.Route}, so + * both client implementations yield identical {@code CloudRoute} domain objects. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3Route(@JsonProperty("guid") String guid, @JsonProperty("host") String host, @JsonProperty("path") String path, + @JsonProperty("port") Integer port, @JsonProperty("url") String url, + @JsonProperty("created_at") String createdAt, @JsonProperty("updated_at") String updatedAt, + @JsonProperty("destinations") List destinations, @JsonProperty("metadata") V3Metadata metadata, + @JsonProperty("relationships") V3RouteRelationships relationships) { + + public List destinations() { + return destinations == null ? Collections.emptyList() : destinations; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Destination(@JsonProperty("guid") String guid, @JsonProperty("app") V3DestinationApp app, + @JsonProperty("port") Integer port, @JsonProperty("weight") Integer weight, + @JsonProperty("protocol") String protocol) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3DestinationApp(@JsonProperty("guid") String guid) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3RouteRelationships(@JsonProperty("space") V3ToOneRelationship space, + @JsonProperty("domain") V3ToOneRelationship domain) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3ToOneRelationship(@JsonProperty("data") V3RelationshipData data) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3RelationshipData(@JsonProperty("guid") String guid) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RouteMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RouteMapper.java new file mode 100644 index 0000000000..b33b856d80 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RouteMapper.java @@ -0,0 +1,104 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudRoute; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudDomain; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudRoute; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableRouteDestination; +import org.cloudfoundry.multiapps.controller.client.facade.domain.RouteDestination; + +/** + * Maps the {@link V3Route} wire model to the project's {@link CloudRoute} domain object. Mirrors the OSS {@code RawCloudRoute} adapter + * field-for-field (including the domain-name derivation from the route URL and the requested-protocol lookup by application GUID), so + * both client implementations yield identical domain objects. + */ +public final class V3RouteMapper { + + private V3RouteMapper() { + } + + /** + * Maps a route without an application context (equivalent to OSS {@code ImmutableRawCloudRoute.of(route)}): the requested protocol is + * always {@code null}. + */ + public static CloudRoute toCloudRoute(V3Route route) { + return toCloudRoute(route, null); + } + + /** + * Maps a route in the context of a specific application (equivalent to OSS + * {@code ImmutableRawCloudRoute.builder().route(route).applicationGuid(applicationGuid).build()}): the requested protocol is taken + * from the destination that points at {@code applicationGuid}. + */ + public static CloudRoute toCloudRoute(V3Route route, UUID applicationGuid) { + List destinations = mapDestinations(route); + String domainGuid = route.relationships() + .domain() + .data() + .guid(); + return ImmutableCloudRoute.builder() + .metadata(V3ResourceMappers.parseMetadata(route.guid(), route.createdAt(), route.updatedAt())) + .appsUsingRoute(route.destinations() + .size()) + .host(route.host()) + .port(route.port()) + .domain(ImmutableCloudDomain.builder() + .name(computeDomain(route)) + .metadata(ImmutableCloudMetadata.of(UUID.fromString(domainGuid))) + .build()) + .path(route.path()) + .url(route.url()) + .destinations(destinations) + .requestedProtocol(computeRequestedProtocol(destinations, applicationGuid)) + .build(); + } + + private static String computeDomain(V3Route route) { + String domain = route.url(); + String host = route.host() == null ? "" : route.host(); + String path = route.path() == null ? "" : route.path(); + if (!host.isEmpty()) { + domain = domain.substring(host.length() + 1); + } + if (!path.isEmpty()) { + domain = domain.substring(0, domain.indexOf('/')); + } + if (route.port() != null) { + domain = domain.substring(0, domain.indexOf(':')); + } + return domain; + } + + private static List mapDestinations(V3Route route) { + return route.destinations() + .stream() + .map(destination -> ImmutableRouteDestination.builder() + .metadata(ImmutableCloudMetadata.builder() + .guid(UUID.fromString(destination.guid())) + .build()) + .applicationGuid(UUID.fromString(destination.app() + .guid())) + .port(destination.port()) + .weight(destination.weight()) + .protocol(destination.protocol()) + .build()) + .collect(Collectors.toList()); + } + + private static String computeRequestedProtocol(List destinations, UUID applicationGuid) { + if (applicationGuid == null) { + return null; + } + return destinations.stream() + .filter(destination -> Objects.equals(destination.getApplicationGuid(), applicationGuid)) + .findFirst() + .map(RouteDestination::getProtocol) + .orElse(null); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBinding.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBinding.java new file mode 100644 index 0000000000..9c24571233 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBinding.java @@ -0,0 +1,46 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 service credential binding resource ({@code GET/POST/PATCH/DELETE + * /v3/service_credential_bindings}). A binding is either of {@code type=app} (an application bound to a service instance) or + * {@code type=key} (a service key); the {@code relationships.app} is absent for keys. + * + *
+ * { "guid": "...", "name": "...", "type": "app|key", "created_at": "...", "updated_at": "...",
+ *   "last_operation": { "type": "create|delete", "state": "initial|in progress|succeeded|failed",
+ *                       "description": "...", "created_at": "...", "updated_at": "..." },
+ *   "metadata": { "labels": {...}, "annotations": {...} },
+ *   "relationships": { "app": { "data": { "guid": "..." } | null },
+ *                      "service_instance": { "data": { "guid": "..." } } } }
+ * 
+ */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3ServiceBinding(@JsonProperty("guid") String guid, @JsonProperty("name") String name, @JsonProperty("type") String type, + @JsonProperty("created_at") String createdAt, @JsonProperty("updated_at") String updatedAt, + @JsonProperty("last_operation") V3LastOperation lastOperation, + @JsonProperty("metadata") V3Metadata metadata, + @JsonProperty("relationships") V3Relationships relationships) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3LastOperation(@JsonProperty("type") String type, @JsonProperty("state") String state, + @JsonProperty("description") String description, @JsonProperty("created_at") String createdAt, + @JsonProperty("updated_at") String updatedAt) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Relationships(@JsonProperty("app") V3ToOneRelationship application, + @JsonProperty("service_instance") V3ToOneRelationship serviceInstance) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3ToOneRelationship(@JsonProperty("data") V3RelationshipData data) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3RelationshipData(@JsonProperty("guid") String guid) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBindingMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBindingMapper.java new file mode 100644 index 0000000000..8a06576d65 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBindingMapper.java @@ -0,0 +1,71 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBinding; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceBinding; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableServiceCredentialBindingOperation; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceCredentialBindingOperation; + +/** + * Maps the {@link V3ServiceBinding} wire model to the project's {@link CloudServiceBinding} domain object. Mirrors the OSS + * {@code RawCloudServiceBinding} adapter field-for-field, so both client implementations yield identical domain objects: + *
    + *
  • {@code applicationGuid} — nullable; from {@code relationships.app.data.guid} (absent for {@code type=key} bindings);
  • + *
  • {@code serviceInstanceGuid} — required; from {@code relationships.service_instance.data.guid};
  • + *
  • {@code serviceBindingOperation} — from {@code last_operation}, mirroring {@link ServiceCredentialBindingOperation#from}.
  • + *
+ */ +public final class V3ServiceBindingMapper { + + private V3ServiceBindingMapper() { + } + + public static CloudServiceBinding toCloudServiceBinding(V3ServiceBinding binding) { + return ImmutableCloudServiceBinding.builder() + .metadata(V3ResourceMappers.parseMetadata(binding.guid(), binding.createdAt(), + binding.updatedAt())) + .applicationGuid(parseApplicationGuid(binding)) + .serviceInstanceGuid(parseServiceInstanceGuid(binding)) + .serviceBindingOperation(parseServiceBindingOperation(binding.lastOperation())) + .build(); + } + + private static UUID parseApplicationGuid(V3ServiceBinding binding) { + V3ServiceBinding.V3ToOneRelationship application = binding.relationships() == null ? null + : binding.relationships() + .application(); + if (application == null || application.data() == null) { + return null; + } + return V3ResourceMappers.parseNullableGuid(application.data() + .guid()); + } + + private static UUID parseServiceInstanceGuid(V3ServiceBinding binding) { + return UUID.fromString(binding.relationships() + .serviceInstance() + .data() + .guid()); + } + + private static ServiceCredentialBindingOperation parseServiceBindingOperation(V3ServiceBinding.V3LastOperation lastOperation) { + if (lastOperation == null) { + return null; + } + return ImmutableServiceCredentialBindingOperation.builder() + .type(ServiceCredentialBindingOperation.Type.fromString(lastOperation.type())) + .state(ServiceCredentialBindingOperation.State.fromString(lastOperation.state())) + .description(lastOperation.description()) + .createdAt(parseDate(lastOperation.createdAt())) + .updatedAt(parseDate(lastOperation.updatedAt())) + .build(); + } + + private static LocalDateTime parseDate(String date) { + return date == null ? null : LocalDateTime.parse(date, DateTimeFormatter.ISO_DATE_TIME); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBroker.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBroker.java new file mode 100644 index 0000000000..4be2e36a9b --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBroker.java @@ -0,0 +1,35 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 service broker resource ({@code GET/POST/PATCH /v3/service_brokers}). + * + *
+ * { "guid": "...", "name": "...", "url": "...", "created_at": "...", "updated_at": "...",
+ *   "metadata": { "labels": {...}, "annotations": {...} },
+ *   "relationships": { "space": { "data": { "guid": "..." } | null } } }
+ * 
+ * + * Only the fields the client maps into {@code CloudServiceBroker} are declared; everything else is ignored. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3ServiceBroker(@JsonProperty("guid") String guid, @JsonProperty("name") String name, @JsonProperty("url") String url, + @JsonProperty("created_at") String createdAt, @JsonProperty("updated_at") String updatedAt, + @JsonProperty("metadata") V3Metadata metadata, + @JsonProperty("relationships") V3Relationships relationships) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Relationships(@JsonProperty("space") V3ToOneRelationship space) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3ToOneRelationship(@JsonProperty("data") V3RelationshipData data) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3RelationshipData(@JsonProperty("guid") String guid) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBrokerMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBrokerMapper.java new file mode 100644 index 0000000000..34d1bab18d --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBrokerMapper.java @@ -0,0 +1,40 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.Optional; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBroker; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceBroker; + +/** + * Maps the {@link V3ServiceBroker} wire model to the project's {@link CloudServiceBroker} domain object. Mirrors the OSS + * {@code RawCloudServiceBroker} adapter field-for-field, so both client implementations yield identical domain objects. + * + *

+ * As in the OSS adapter, {@code username}/{@code password} are write-only credentials that CF never returns, so they are not mapped; + * only {@code name}, {@code url} and the (optional) space GUID from {@code relationships.space.data.guid} are populated. + *

+ */ +public final class V3ServiceBrokerMapper { + + private V3ServiceBrokerMapper() { + } + + public static CloudServiceBroker toCloudServiceBroker(V3ServiceBroker serviceBroker) { + return ImmutableCloudServiceBroker.builder() + .metadata(V3ResourceMappers.parseMetadata(serviceBroker.guid(), serviceBroker.createdAt(), + serviceBroker.updatedAt())) + .name(serviceBroker.name()) + .url(serviceBroker.url()) + .spaceGuid(getSpaceGuid(serviceBroker)) + .build(); + } + + private static String getSpaceGuid(V3ServiceBroker serviceBroker) { + return Optional.ofNullable(serviceBroker.relationships()) + .map(V3ServiceBroker.V3Relationships::space) + .map(V3ServiceBroker.V3ToOneRelationship::data) + .map(V3ServiceBroker.V3RelationshipData::guid) + .orElse(null); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstance.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstance.java new file mode 100644 index 0000000000..a220b724b3 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstance.java @@ -0,0 +1,47 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 service instance resource ({@code GET/POST/PATCH /v3/service_instances}). + * + *
+ * { "guid": "...", "name": "...", "type": "managed|user-provided",
+ *   "created_at": "...", "updated_at": "...",
+ *   "tags": [...], "syslog_drain_url": "...",
+ *   "last_operation": { "type": "create|update|delete", "state": "succeeded|failed|in progress|initial", "description": "..." },
+ *   "metadata": { "labels": {...}, "annotations": {...} },
+ *   "relationships": { "space": { "data": { "guid": "..." } }, "service_plan": { "data": { "guid": "..." } } } }
+ * 
+ * + * Only the fields the client needs are mapped; everything else is ignored so CF can evolve the payload without breaking us. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3ServiceInstance(@JsonProperty("guid") String guid, @JsonProperty("name") String name, @JsonProperty("type") String type, + @JsonProperty("created_at") String createdAt, @JsonProperty("updated_at") String updatedAt, + @JsonProperty("tags") List tags, @JsonProperty("syslog_drain_url") String syslogDrainUrl, + @JsonProperty("last_operation") V3LastOperation lastOperation, @JsonProperty("metadata") V3Metadata metadata, + @JsonProperty("relationships") V3Relationships relationships) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3LastOperation(@JsonProperty("type") String type, @JsonProperty("state") String state, + @JsonProperty("description") String description) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Relationships(@JsonProperty("space") V3ToOneRelationship space, + @JsonProperty("service_plan") V3ToOneRelationship servicePlan) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3ToOneRelationship(@JsonProperty("data") V3RelationshipData data) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3RelationshipData(@JsonProperty("guid") String guid) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstanceMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstanceMapper.java new file mode 100644 index 0000000000..45b6e73b51 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstanceMapper.java @@ -0,0 +1,88 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceInstance; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceOperation; + +/** + * Maps the {@link V3ServiceInstance} wire model to the project's {@link CloudServiceInstance} domain object. Mirrors the OSS + * {@code RawCloudServiceInstance} and {@code RawV3CloudServiceInstance} adapters field-for-field, so both client implementations yield + * identical domain objects. + * + *

+ * The OSS {@code RawCloudServiceInstance} derives {@code plan} and {@code label} from auxiliary {@code ServicePlan} / + * {@code ServiceOffering} resources it is zipped with; those names are resolved by the operations helper and passed in here. + * {@code RawV3CloudServiceInstance} (used for the {@code byNames} batch lookups) maps only the base fields with no auxiliary content. + *

+ */ +public final class V3ServiceInstanceMapper { + + private V3ServiceInstanceMapper() { + } + + /** + * Full mapping mirroring the OSS {@code RawCloudServiceInstance.derive()}: base fields plus the resolved plan/offering names. + */ + public static CloudServiceInstance toCloudServiceInstance(V3ServiceInstance serviceInstance, String planName, String labelName) { + return ImmutableCloudServiceInstance.builder() + .metadata(V3ResourceMappers.parseMetadata(serviceInstance.guid(), + serviceInstance.createdAt(), + serviceInstance.updatedAt())) + .v3Metadata(V3ResourceMappers.toV3Metadata(serviceInstance.metadata())) + .name(serviceInstance.name()) + .plan(planName) + .label(labelName) + .type(parseType(serviceInstance.type())) + .tags(serviceInstance.tags() == null ? java.util.Collections.emptyList() + : serviceInstance.tags()) + .lastOperation(parseLastOperation(serviceInstance.lastOperation())) + .syslogDrainUrl(serviceInstance.syslogDrainUrl()) + .build(); + } + + /** + * Base-only mapping mirroring the OSS {@code RawV3CloudServiceInstance.derive()}: metadata, v3Metadata, name and tags (used by the + * {@code byNames} batch lookups, which never resolve plan/offering and leave {@code type} unset). + */ + public static CloudServiceInstance toCloudServiceInstanceWithoutAuxiliaryContent(V3ServiceInstance serviceInstance) { + return ImmutableCloudServiceInstance.builder() + .metadata(V3ResourceMappers.parseMetadata(serviceInstance.guid(), + serviceInstance.createdAt(), + serviceInstance.updatedAt())) + .v3Metadata(V3ResourceMappers.toV3Metadata(serviceInstance.metadata())) + .name(serviceInstance.name()) + .tags(serviceInstance.tags() == null ? java.util.Collections.emptyList() + : serviceInstance.tags()) + .build(); + } + + /** + * Map the CF v3 {@code type} JSON value ({@code managed} / {@code user-provided}) to the project's {@link ServiceInstanceType}. + */ + private static ServiceInstanceType parseType(String type) { + if (type == null) { + return null; + } + for (ServiceInstanceType serviceInstanceType : ServiceInstanceType.values()) { + if (serviceInstanceType.getValue() + .equals(type)) { + return serviceInstanceType; + } + } + return null; + } + + /** + * Mirrors {@code ServiceOperation.fromLastOperation(LastOperation)}: a {@code null} operation, type or state yields {@code null}. + */ + private static ServiceOperation parseLastOperation(V3ServiceInstance.V3LastOperation lastOperation) { + if (lastOperation == null || lastOperation.type() == null || lastOperation.state() == null) { + return null; + } + ServiceOperation.Type type = ServiceOperation.Type.fromString(lastOperation.type()); + ServiceOperation.State state = ServiceOperation.State.fromString(lastOperation.state()); + return new ServiceOperation(type, lastOperation.description(), state); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyDetails.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyDetails.java new file mode 100644 index 0000000000..d844dfaa51 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyDetails.java @@ -0,0 +1,28 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.Collections; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of the CF v3 service credential binding details response + * ({@code GET /v3/service_credential_bindings/{guid}/details}), which is where the actual {@code credentials} of a service key live. + * + *
+ * { "credentials": { ... }, "syslog_drain_url": "...", "volume_mounts": [ ... ] }
+ * 
+ * + * Mirrors the OSS impl's {@code serviceBindingsV3().getDetails(...)} call. The CF v3 API returns a 404 when fetching the details of a + * service key whose creation failed; callers translate that into empty credentials (see {@code ServiceKeysV3Operations}). + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3ServiceKeyDetails(@JsonProperty("credentials") Map credentials, + @JsonProperty("syslog_drain_url") String syslogDrainUrl) { + + public Map credentials() { + return credentials == null ? Collections.emptyMap() : credentials; + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyMapper.java new file mode 100644 index 0000000000..bc617456c8 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyMapper.java @@ -0,0 +1,59 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.Map; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceKey; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableServiceCredentialBindingOperation; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceCredentialBindingOperation; + +/** + * Maps a CF v3 service credential binding of {@code type=key} ({@link V3ServiceBinding}) to the project's {@link CloudServiceKey} domain + * object. Mirrors the OSS {@code RawCloudServiceKey#derive()} adapter field-for-field, so both client implementations yield identical + * domain objects: + *
    + *
  • {@code metadata} — from the binding resource envelope ({@code guid}, {@code created_at}, {@code updated_at});
  • + *
  • {@code v3Metadata} — labels/annotations from {@code metadata};
  • + *
  • {@code name} — the service key name;
  • + *
  • {@code credentials} — nullable; fetched separately from the binding details endpoint (absent when listing without + * credentials);
  • + *
  • {@code serviceInstance} — the owning (already-derived) {@link CloudServiceInstance};
  • + *
  • {@code serviceKeyOperation} — from {@code last_operation}, mirroring {@link ServiceCredentialBindingOperation#from}.
  • + *
+ */ +public final class V3ServiceKeyMapper { + + private V3ServiceKeyMapper() { + } + + public static CloudServiceKey toCloudServiceKey(V3ServiceBinding key, CloudServiceInstance serviceInstance) { + return toCloudServiceKey(key, serviceInstance, null); + } + + public static CloudServiceKey toCloudServiceKey(V3ServiceBinding key, CloudServiceInstance serviceInstance, + Map credentials) { + return ImmutableCloudServiceKey.builder() + .metadata(V3ResourceMappers.parseMetadata(key.guid(), key.createdAt(), key.updatedAt())) + .v3Metadata(V3ResourceMappers.toV3Metadata(key.metadata())) + .name(key.name()) + .credentials(credentials) + .serviceInstance(serviceInstance) + .serviceKeyOperation(parseServiceKeyOperation(key.lastOperation())) + .build(); + } + + private static ServiceCredentialBindingOperation parseServiceKeyOperation(V3ServiceBinding.V3LastOperation lastOperation) { + if (lastOperation == null) { + return null; + } + return ImmutableServiceCredentialBindingOperation.builder() + .type(ServiceCredentialBindingOperation.Type.fromString(lastOperation.type())) + .state(ServiceCredentialBindingOperation.State.fromString(lastOperation.state())) + .description(lastOperation.description()) + .createdAt(V3ResourceMappers.parseNullableDate(lastOperation.createdAt())) + .updatedAt(V3ResourceMappers.parseNullableDate(lastOperation.updatedAt())) + .build(); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceOffering.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceOffering.java new file mode 100644 index 0000000000..f0e64df11f --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceOffering.java @@ -0,0 +1,54 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 service offering resource ({@code GET /v3/service_offerings}). + * + *
+ * { "guid": "...", "name": "...", "description": "...", "available": true, "shareable": true,
+ *   "documentation_url": "...", "created_at": "...", "updated_at": "...",
+ *   "broker_catalog": { "id": "...", "metadata": {...}, "features": { "bindable": true, ... } },
+ *   "relationships": { "service_broker": { "data": { "guid": "..." } } },
+ *   "metadata": { "labels": {...}, "annotations": {...} } }
+ * 
+ */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3ServiceOffering(@JsonProperty("guid") String guid, @JsonProperty("name") String name, + @JsonProperty("description") String description, @JsonProperty("available") Boolean available, + @JsonProperty("shareable") Boolean shareable, + @JsonProperty("documentation_url") String documentationUrl, + @JsonProperty("created_at") String createdAt, @JsonProperty("updated_at") String updatedAt, + @JsonProperty("broker_catalog") V3BrokerCatalog brokerCatalog, + @JsonProperty("relationships") V3Relationships relationships, + @JsonProperty("metadata") V3Metadata metadata) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3BrokerCatalog(@JsonProperty("id") String id, @JsonProperty("metadata") Map metadata, + @JsonProperty("features") V3Features features) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Features(@JsonProperty("bindable") Boolean bindable, + @JsonProperty("allow_context_updates") Boolean allowContextUpdates, + @JsonProperty("bindings_retrievable") Boolean bindingsRetrievable, + @JsonProperty("instances_retrievable") Boolean instancesRetrievable, + @JsonProperty("plan_updateable") Boolean planUpdateable) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Relationships(@JsonProperty("service_broker") V3ToOneRelationship serviceBroker) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3ToOneRelationship(@JsonProperty("data") V3RelationshipData data) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3RelationshipData(@JsonProperty("guid") String guid) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceOfferingMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceOfferingMapper.java new file mode 100644 index 0000000000..7393d9a97e --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceOfferingMapper.java @@ -0,0 +1,64 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.List; +import java.util.Map; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceOffering; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServicePlan; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceOffering; + +/** + * Maps the {@link V3ServiceOffering} wire model (plus the service plans fetched separately for it) to the project's + * {@link CloudServiceOffering} domain object. Mirrors the OSS {@code RawCloudServiceOffering} adapter field-for-field, so both client + * implementations yield identical domain objects. + */ +public final class V3ServiceOfferingMapper { + + private V3ServiceOfferingMapper() { + } + + public static CloudServiceOffering toCloudServiceOffering(V3ServiceOffering serviceOffering, List servicePlans) { + return ImmutableCloudServiceOffering.builder() + .metadata(V3ResourceMappers.parseMetadata(serviceOffering.guid(), + serviceOffering.createdAt(), + serviceOffering.updatedAt())) + .name(serviceOffering.name()) + .isAvailable(serviceOffering.available()) + .isBindable(extractBindable(serviceOffering.brokerCatalog())) + .description(serviceOffering.description()) + .isShareable(serviceOffering.shareable()) + .extra(extractBrokerCatalogMetadata(serviceOffering.brokerCatalog())) + .docUrl(serviceOffering.documentationUrl()) + .brokerId(extractBrokerId(serviceOffering.relationships())) + .uniqueId(extractBrokerCatalogId(serviceOffering.brokerCatalog())) + .servicePlans(servicePlans) + .build(); + } + + private static Boolean extractBindable(V3ServiceOffering.V3BrokerCatalog brokerCatalog) { + if (brokerCatalog == null || brokerCatalog.features() == null) { + return null; + } + return brokerCatalog.features() + .bindable(); + } + + private static Map extractBrokerCatalogMetadata(V3ServiceOffering.V3BrokerCatalog brokerCatalog) { + return brokerCatalog == null ? null : brokerCatalog.metadata(); + } + + private static String extractBrokerCatalogId(V3ServiceOffering.V3BrokerCatalog brokerCatalog) { + return brokerCatalog == null ? null : brokerCatalog.id(); + } + + private static String extractBrokerId(V3ServiceOffering.V3Relationships relationships) { + if (relationships == null || relationships.serviceBroker() == null || relationships.serviceBroker() + .data() == null) { + return null; + } + return relationships.serviceBroker() + .data() + .guid(); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServicePlan.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServicePlan.java new file mode 100644 index 0000000000..f7da724b1c --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServicePlan.java @@ -0,0 +1,43 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 service plan resource ({@code GET /v3/service_plans}). + * + *
+ * { "guid": "...", "name": "...", "description": "...", "free": true|false,
+ *   "visibility_type": "public|admin|organization|space",
+ *   "created_at": "...", "updated_at": "...",
+ *   "broker_catalog": { "id": "...", "metadata": { ... } },
+ *   "relationships": { "service_offering": { "data": { "guid": "..." } } },
+ *   "metadata": { "labels": {...}, "annotations": {...} } }
+ * 
+ */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3ServicePlan(@JsonProperty("guid") String guid, @JsonProperty("name") String name, + @JsonProperty("description") String description, @JsonProperty("free") Boolean free, + @JsonProperty("visibility_type") String visibilityType, @JsonProperty("created_at") String createdAt, + @JsonProperty("updated_at") String updatedAt, @JsonProperty("broker_catalog") V3BrokerCatalog brokerCatalog, + @JsonProperty("relationships") V3Relationships relationships, @JsonProperty("metadata") V3Metadata metadata) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3BrokerCatalog(@JsonProperty("id") String id, @JsonProperty("metadata") Map metadata) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Relationships(@JsonProperty("service_offering") V3ToOneRelationship serviceOffering) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3ToOneRelationship(@JsonProperty("data") V3RelationshipData data) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3RelationshipData(@JsonProperty("guid") String guid) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServicePlanMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServicePlanMapper.java new file mode 100644 index 0000000000..4a7c197e67 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServicePlanMapper.java @@ -0,0 +1,55 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServicePlan; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServicePlan; + +/** + * Maps the {@link V3ServicePlan} wire model to the project's {@link CloudServicePlan} domain object. Mirrors the OSS + * {@code RawCloudServicePlan} adapter field-for-field, so both client implementations yield identical domain objects. + */ +public final class V3ServicePlanMapper { + + // CF v3 visibility_type value that maps to the domain's isPublic flag (see RawCloudServicePlan#derive). + private static final String PUBLIC_VISIBILITY = "public"; + + private V3ServicePlanMapper() { + } + + public static CloudServicePlan toCloudServicePlan(V3ServicePlan servicePlan) { + return ImmutableCloudServicePlan.builder() + .metadata(V3ResourceMappers.parseMetadata(servicePlan.guid(), servicePlan.createdAt(), + servicePlan.updatedAt())) + .v3Metadata(V3ResourceMappers.toV3Metadata(servicePlan.metadata())) + .name(servicePlan.name()) + .description(servicePlan.description()) + .extra(extractBrokerCatalogMetadata(servicePlan.brokerCatalog())) + .uniqueId(extractBrokerCatalogId(servicePlan.brokerCatalog())) + .serviceOfferingId(extractServiceOfferingId(servicePlan.relationships())) + .isPublic(isPublic(servicePlan.visibilityType())) + .isFree(servicePlan.free()) + .build(); + } + + private static java.util.Map extractBrokerCatalogMetadata(V3ServicePlan.V3BrokerCatalog brokerCatalog) { + return brokerCatalog == null ? null : brokerCatalog.metadata(); + } + + private static String extractBrokerCatalogId(V3ServicePlan.V3BrokerCatalog brokerCatalog) { + return brokerCatalog == null ? null : brokerCatalog.id(); + } + + private static String extractServiceOfferingId(V3ServicePlan.V3Relationships relationships) { + if (relationships == null || relationships.serviceOffering() == null || relationships.serviceOffering() + .data() == null) { + return null; + } + return relationships.serviceOffering() + .data() + .guid(); + } + + private static Boolean isPublic(String visibilityType) { + return visibilityType == null ? null : PUBLIC_VISIBILITY.equals(visibilityType); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Stack.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Stack.java new file mode 100644 index 0000000000..d1ba56ffe3 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Stack.java @@ -0,0 +1,20 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 stack resource ({@code GET /v3/stacks}). + * + *
+ * { "guid": "...", "name": "cflinuxfs4", "description": "...",
+ *   "created_at": "...", "updated_at": "...",
+ *   "metadata": { "labels": {...}, "annotations": {...} } }
+ * 
+ */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3Stack(@JsonProperty("guid") String guid, @JsonProperty("name") String name, + @JsonProperty("description") String description, @JsonProperty("created_at") String createdAt, + @JsonProperty("updated_at") String updatedAt, @JsonProperty("metadata") V3Metadata metadata) { + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3StackMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3StackMapper.java new file mode 100644 index 0000000000..9d4c7c84d9 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3StackMapper.java @@ -0,0 +1,23 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudStack; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudStack; + +/** + * Maps the {@link V3Stack} wire model to the project's {@link CloudStack} domain object. Mirrors the OSS + * {@code RawCloudStack} adapter field-for-field, so both client implementations yield identical domain objects. + */ +public final class V3StackMapper { + + private V3StackMapper() { + } + + public static CloudStack toCloudStack(V3Stack stack) { + return ImmutableCloudStack.builder() + .metadata(V3ResourceMappers.parseMetadata(stack.guid(), stack.createdAt(), stack.updatedAt())) + .name(stack.name()) + .description(stack.description()) + .build(); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Task.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Task.java new file mode 100644 index 0000000000..bddbd82ec6 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Task.java @@ -0,0 +1,28 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 task resource ({@code GET/POST /v3/tasks}, {@code POST /v3/apps/{guid}/tasks}). + * + *
+ * { "guid": "...", "name": "...", "command": "...", "state": "PENDING|RUNNING|SUCCEEDED|CANCELING|FAILED",
+ *   "memory_in_mb": 256, "disk_in_mb": 1024,
+ *   "result": { "failure_reason": "..." | null },
+ *   "created_at": "...", "updated_at": "...",
+ *   "metadata": { "labels": {...}, "annotations": {...} } }
+ * 
+ */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3Task(@JsonProperty("guid") String guid, @JsonProperty("name") String name, @JsonProperty("command") String command, + @JsonProperty("state") String state, @JsonProperty("memory_in_mb") Integer memoryInMb, + @JsonProperty("disk_in_mb") Integer diskInMb, @JsonProperty("result") V3Result result, + @JsonProperty("created_at") String createdAt, @JsonProperty("updated_at") String updatedAt, + @JsonProperty("metadata") V3Metadata metadata) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Result(@JsonProperty("failure_reason") String failureReason) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3TaskMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3TaskMapper.java new file mode 100644 index 0000000000..1341ea722e --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3TaskMapper.java @@ -0,0 +1,45 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudTask; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudTask; + +/** + * Maps the {@link V3Task} wire model to the project's {@link CloudTask} domain object. Mirrors the OSS {@code RawCloudTask} adapter + * field-for-field, so both client implementations yield identical domain objects. + */ +public final class V3TaskMapper { + + private V3TaskMapper() { + } + + public static CloudTask toCloudTask(V3Task task) { + return ImmutableCloudTask.builder() + .metadata(V3ResourceMappers.parseMetadata(task.guid(), task.createdAt(), task.updatedAt())) + .name(task.name()) + .command(task.command()) + .limits(parseLimits(task)) + .result(parseResult(task)) + .state(parseState(task.state())) + .build(); + } + + private static CloudTask.Result parseResult(V3Task task) { + if (task.result() == null) { + return null; + } + return ImmutableCloudTask.ImmutableResult.of(task.result() + .failureReason()); + } + + private static CloudTask.Limits parseLimits(V3Task task) { + return ImmutableCloudTask.ImmutableLimits.builder() + .disk(task.diskInMb()) + .memory(task.memoryInMb()) + .build(); + } + + private static CloudTask.State parseState(String state) { + return state == null ? null : CloudTask.State.valueOf(state); + } + +} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/ApplicationsCloudControllerClientIntegrationTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/ApplicationsCloudControllerClientIntegrationTest.java index 4cc72cbed1..70cc2867b7 100644 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/ApplicationsCloudControllerClientIntegrationTest.java +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/ApplicationsCloudControllerClientIntegrationTest.java @@ -1,7 +1,7 @@ package org.cloudfoundry.multiapps.controller.client.facade; -import org.cloudfoundry.client.v3.Metadata; -import org.cloudfoundry.client.v3.processes.HealthCheckType; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.HealthCheckType; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudBuild; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudMetadata; @@ -171,7 +171,7 @@ void updateApplicationHealthcheckType() { .healthCheck( org.cloudfoundry.client.v3.processes.HealthCheck.builder() .type( - HealthCheckType.PROCESS) + org.cloudfoundry.client.v3.processes.HealthCheckType.PROCESS) .build()) .build()) .block(); diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/ServicesCloudControllerClientIntegrationTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/ServicesCloudControllerClientIntegrationTest.java index c0de6f9023..0cc22fe179 100644 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/ServicesCloudControllerClientIntegrationTest.java +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/ServicesCloudControllerClientIntegrationTest.java @@ -23,8 +23,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.jobs.JobState; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; +import org.cloudfoundry.multiapps.controller.client.facade.domain.JobState; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplicationTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplicationTest.java index 514fe959c6..e864ff93a9 100644 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplicationTest.java +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplicationTest.java @@ -49,7 +49,7 @@ void testDeriveForCnbApp() { private static CloudApplication buildApplication(org.cloudfoundry.multiapps.controller.client.facade.domain.Lifecycle lifecycle) { return ImmutableCloudApplication.builder() .metadata(RawCloudEntityTest.EXPECTED_METADATA_V3) - .v3Metadata(RawCloudEntityTest.V3_METADATA) + .v3Metadata(RawCloudEntityTest.V3_METADATA_DOMAIN) .name(RawCloudEntityTest.NAME) .state(EXPECTED_STATE) .lifecycle(lifecycle) diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJobTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJobTest.java index 1340e590ff..41407fb6b6 100644 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJobTest.java +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJobTest.java @@ -34,7 +34,7 @@ void testDerive() { private CloudAsyncJob buildExpectedTask() { return ImmutableCloudAsyncJob.builder() .metadata(RawCloudEntityTest.EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE) - .state(JobState.FAILED) + .state(org.cloudfoundry.multiapps.controller.client.facade.domain.JobState.FAILED) .operation(OPERATION) .warnings(WARNINGS) .errors(ERRORS) diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntityTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntityTest.java index 51b1fb5007..8e0e74876d 100644 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntityTest.java +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntityTest.java @@ -44,6 +44,13 @@ class RawCloudEntityTest { .labels(V3_LABELS) .build(); + // Domain-typed equivalent of V3_METADATA, for building expected domain objects (CloudEntity.getV3Metadata() is domain.Metadata). + static final org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata V3_METADATA_DOMAIN = + org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata.builder() + .annotations(V3_ANNOTATIONS) + .labels(V3_LABELS) + .build(); + static final CloudMetadata EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE = ImmutableCloudMetadata.builder() .guid(GUID) .createdAt(CREATED_AT) diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBindingTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBindingTest.java index 41d468060a..b2d1600aa8 100644 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBindingTest.java +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBindingTest.java @@ -31,7 +31,10 @@ private static CloudServiceBinding buildExpectedServiceBinding() { .metadata(RawCloudEntityTest.EXPECTED_METADATA_V3) .applicationGuid(APPLICATION_GUID) .serviceInstanceGuid(SERVICE_INSTANCE_GUID) - .serviceBindingOperation(ServiceCredentialBindingOperation.from(LAST_OPERATION)) + .serviceBindingOperation(ServiceCredentialBindingOperation.from( + new org.cloudfoundry.multiapps.controller.client.facade.domain.LastOperation( + LAST_OPERATION.getType(), LAST_OPERATION.getState(), LAST_OPERATION.getDescription(), + LAST_OPERATION.getCreatedAt(), LAST_OPERATION.getUpdatedAt()))) .build(); } diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstanceTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstanceTest.java index 9645cca49a..1c41c4f866 100644 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstanceTest.java +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstanceTest.java @@ -8,7 +8,7 @@ import org.cloudfoundry.client.v3.LastOperation; import org.cloudfoundry.client.v3.MaintenanceInfo; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.client.v3.Relationship; import org.cloudfoundry.client.v3.ToOneRelationship; import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceRelationships; @@ -58,7 +58,7 @@ private CloudServiceInstance buildExpectedServiceInstance() { .guid(UUID.fromString(SERVICE_INSTANCE_ID)) .build()) .name(SERVICE_INSTANCE_NAME) - .v3Metadata(Metadata.builder() + .v3Metadata(org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata.builder() .annotations(METADATA_ANNOTATIONS) .labels(METADATA_LABELS) .build()) @@ -90,7 +90,7 @@ private ServiceInstanceResource buildServiceInstanceResource() { .description(MAINTENANCE_INFO_DESCRIPTION) .version(MAINTENANCE_INFO_VERSION) .build()) - .metadata(Metadata.builder() + .metadata(org.cloudfoundry.client.v3.Metadata.builder() .annotations(METADATA_ANNOTATIONS) .labels(METADATA_LABELS) .build()) diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKeyTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKeyTest.java index e8c49dc920..4a8a69f161 100644 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKeyTest.java +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKeyTest.java @@ -34,11 +34,14 @@ void testDerive() { private static CloudServiceKey buildExpectedServiceKey() { return ImmutableCloudServiceKey.builder() .metadata(RawCloudEntityTest.EXPECTED_METADATA_V3) - .v3Metadata(RawCloudEntityTest.V3_METADATA) + .v3Metadata(RawCloudEntityTest.V3_METADATA_DOMAIN) .name(NAME) .credentials(CREDENTIALS) .serviceInstance(SERVICE_INSTANCE) - .serviceKeyOperation(ServiceCredentialBindingOperation.from(LAST_OPERATION)) + .serviceKeyOperation(ServiceCredentialBindingOperation.from( + new org.cloudfoundry.multiapps.controller.client.facade.domain.LastOperation( + LAST_OPERATION.getType(), LAST_OPERATION.getState(), LAST_OPERATION.getDescription(), + LAST_OPERATION.getCreatedAt(), LAST_OPERATION.getUpdatedAt()))) .build(); } diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceTest.java index 8a4fec8ef0..1eaf5f98e8 100644 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceTest.java +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceTest.java @@ -46,7 +46,7 @@ private static CloudServiceInstance buildExpectedService() { .name(NAME) .plan(PLAN_NAME) .label(OFFERING_NAME) - .type(ServiceInstanceType.MANAGED) + .type(org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType.MANAGED) .tags(TAGS) .lastOperation(new ServiceOperation(Type.CREATE, "", State.IN_PROGRESS)) .build(); @@ -56,7 +56,7 @@ private static CloudServiceInstance buildExpectedUserProvidedService() { return ImmutableCloudServiceInstance.builder() .metadata(RawCloudEntityTest.EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE) .name(NAME) - .type(ServiceInstanceType.USER_PROVIDED) + .type(org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType.USER_PROVIDED) .tags(TAGS) .lastOperation(new ServiceOperation(Type.CREATE, "", State.SUCCEEDED)) .build(); diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CloudEntityResourceMapper.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CloudEntityResourceMapper.java index 8d01fe7eca..228cfaca71 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CloudEntityResourceMapper.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CloudEntityResourceMapper.java @@ -11,8 +11,8 @@ import java.util.Optional; import java.util.UUID; -import org.cloudfoundry.client.v3.Metadata; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudMetadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClient.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClient.java index 977ddae39b..d9e6a303f3 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClient.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClient.java @@ -7,7 +7,7 @@ import java.util.UUID; import java.util.stream.Collectors; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType; import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadataLabels; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/metadata/processor/MtaMetadataParser.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/metadata/processor/MtaMetadataParser.java index f9503d54ee..290ed3b8ae 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/metadata/processor/MtaMetadataParser.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/metadata/processor/MtaMetadataParser.java @@ -7,7 +7,7 @@ import jakarta.inject.Inject; import jakarta.inject.Named; import org.apache.commons.lang3.StringUtils; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEntity; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/metadata/util/MtaMetadataUtil.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/metadata/util/MtaMetadataUtil.java index 59b674ab9e..4d6d69572c 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/metadata/util/MtaMetadataUtil.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/metadata/util/MtaMetadataUtil.java @@ -4,7 +4,7 @@ import java.util.Set; import org.apache.commons.lang3.StringUtils; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEntity; import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadataAnnotations; import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadataLabels; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ApplicationMetadataBuilder.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ApplicationMetadataBuilder.java index 44f9dee90b..aea8248cab 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ApplicationMetadataBuilder.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ApplicationMetadataBuilder.java @@ -5,7 +5,7 @@ import java.util.TreeMap; import java.util.stream.Collectors; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.common.util.JsonUtil; import org.cloudfoundry.multiapps.common.util.MapUtil; import org.cloudfoundry.multiapps.controller.core.Constants; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/MtaMetadataBuilder.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/MtaMetadataBuilder.java index 9b9a0d18db..a392eafb31 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/MtaMetadataBuilder.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/MtaMetadataBuilder.java @@ -1,8 +1,8 @@ package org.cloudfoundry.multiapps.controller.core.cf.v2; import org.apache.commons.lang3.StringUtils; -import org.cloudfoundry.client.v3.Metadata; -import org.cloudfoundry.client.v3.Metadata.Builder; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata.Builder; import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadataAnnotations; import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadataLabels; import org.cloudfoundry.multiapps.controller.core.cf.metadata.util.MtaMetadataUtil; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServiceKeyMetadataBuilder.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServiceKeyMetadataBuilder.java index ae13c007af..e9ccf2fcc0 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServiceKeyMetadataBuilder.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServiceKeyMetadataBuilder.java @@ -1,7 +1,7 @@ package org.cloudfoundry.multiapps.controller.core.cf.v2; -import org.cloudfoundry.client.v3.Metadata; -import org.cloudfoundry.client.v3.Metadata.Builder; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata.Builder; import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadataLabels; import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServiceKeysCloudModelBuilder.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServiceKeysCloudModelBuilder.java index ef5cb4a984..1ad059b028 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServiceKeysCloudModelBuilder.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServiceKeysCloudModelBuilder.java @@ -6,8 +6,8 @@ import java.util.Map; import java.util.stream.Collectors; -import org.cloudfoundry.client.v3.Metadata; import org.cloudfoundry.multiapps.common.ContentException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceKey; import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudServiceInstanceExtended; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServiceMetadataBuilder.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServiceMetadataBuilder.java index 2bbc6befb9..f80768cd03 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServiceMetadataBuilder.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServiceMetadataBuilder.java @@ -3,8 +3,8 @@ import java.util.Map; import java.util.TreeMap; -import org.cloudfoundry.client.v3.Metadata; -import org.cloudfoundry.client.v3.Metadata.Builder; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata.Builder; import org.cloudfoundry.multiapps.common.util.JsonUtil; import org.cloudfoundry.multiapps.common.util.MapUtil; import org.cloudfoundry.multiapps.controller.core.Constants; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServicesCloudModelBuilder.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServicesCloudModelBuilder.java index 90abef06be..70e33a2be6 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServicesCloudModelBuilder.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServicesCloudModelBuilder.java @@ -7,7 +7,7 @@ import java.util.Objects; import java.util.TreeMap; import java.util.stream.Collectors; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType; import org.cloudfoundry.multiapps.common.ContentException; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudServiceInstanceExtended; diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClientTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClientTest.java index 670dcfaec3..ffddd0f832 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClientTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/clients/CustomServiceKeysClientTest.java @@ -6,8 +6,8 @@ import java.util.List; import java.util.UUID; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; import org.cloudfoundry.multiapps.controller.core.model.DeployedMtaService; import org.cloudfoundry.multiapps.controller.core.model.DeployedMtaServiceKey; diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/detect/DeployedMtaDetectorTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/detect/DeployedMtaDetectorTest.java index e61ff34873..8d5230fb38 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/detect/DeployedMtaDetectorTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/detect/DeployedMtaDetectorTest.java @@ -9,7 +9,7 @@ import java.util.stream.Stream; import com.fasterxml.jackson.core.type.TypeReference; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.common.test.TestUtil; import org.cloudfoundry.multiapps.common.test.Tester; import org.cloudfoundry.multiapps.common.test.Tester.Expectation; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/client/LoggingCloudControllerClient.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/client/LoggingCloudControllerClient.java index afb3fbdce1..60f0909f3c 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/client/LoggingCloudControllerClient.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/client/LoggingCloudControllerClient.java @@ -8,7 +8,6 @@ import java.util.Set; import java.util.UUID; -import org.cloudfoundry.client.v3.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.ApplicationServicesUpdateCallback; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; import org.cloudfoundry.multiapps.controller.client.facade.ServiceBindingOperationCallback; @@ -32,6 +31,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.DockerInfo; import org.cloudfoundry.multiapps.controller.client.facade.domain.DropletInfo; import org.cloudfoundry.multiapps.controller.client.facade.domain.InstancesInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.ServicePlanVisibility; import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; import org.cloudfoundry.multiapps.controller.client.facade.domain.Upload; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/BuildApplicationDeployModelStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/BuildApplicationDeployModelStep.java index dd6569a455..1bcfd82f39 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/BuildApplicationDeployModelStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/BuildApplicationDeployModelStep.java @@ -8,7 +8,7 @@ import jakarta.inject.Inject; import jakarta.inject.Named; import org.apache.commons.lang3.StringUtils; -import org.cloudfoundry.client.v3.processes.HealthCheckType; +import org.cloudfoundry.multiapps.controller.client.facade.domain.HealthCheckType; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudRoute; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableStaging; import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ClearMtaMetadataBaseStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ClearMtaMetadataBaseStep.java index 11a92109fd..b4cf434a2c 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ClearMtaMetadataBaseStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ClearMtaMetadataBaseStep.java @@ -4,7 +4,7 @@ import java.util.UUID; import org.apache.commons.collections4.MapUtils; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceCreateUpdateServiceActionsStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceCreateUpdateServiceActionsStep.java index da5a65dc5e..818e6de5fc 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceCreateUpdateServiceActionsStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceCreateUpdateServiceActionsStep.java @@ -16,7 +16,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.common.util.JsonUtil; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceKeysToRecreateStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceKeysToRecreateStep.java index 4a0dfe41ad..d0fe2541e3 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceKeysToRecreateStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceKeysToRecreateStep.java @@ -8,7 +8,7 @@ import jakarta.inject.Named; import org.apache.commons.collections4.ListUtils; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.common.util.JsonUtil; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareApplicationForBackupStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareApplicationForBackupStep.java index ec99aa294e..3fcf4a2856 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareApplicationForBackupStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareApplicationForBackupStep.java @@ -3,7 +3,7 @@ import java.text.MessageFormat; import jakarta.inject.Named; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadataAnnotations; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RemoveMtaBackupMetadataStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RemoveMtaBackupMetadataStep.java index 78bff75200..7d93f193a0 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RemoveMtaBackupMetadataStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RemoveMtaBackupMetadataStep.java @@ -1,7 +1,7 @@ package org.cloudfoundry.multiapps.controller.process.steps; import jakarta.inject.Named; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadataAnnotations; import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadataLabels; 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 8a1036276a..d5058281c8 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 @@ -14,7 +14,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.lang3.StringUtils; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.common.util.JsonUtil; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/AsyncJobToAsyncExecutionStateAdapter.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/AsyncJobToAsyncExecutionStateAdapter.java index bad6812d53..46e6bc95ed 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/AsyncJobToAsyncExecutionStateAdapter.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/AsyncJobToAsyncExecutionStateAdapter.java @@ -3,8 +3,8 @@ import java.text.MessageFormat; import java.util.function.Consumer; -import org.cloudfoundry.client.v3.jobs.JobState; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; +import org.cloudfoundry.multiapps.controller.client.facade.domain.JobState; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.steps.AsyncExecutionState; import org.immutables.value.Value; 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 9fb4a67889..3127360fa3 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 @@ -9,15 +9,16 @@ import jakarta.inject.Inject; import jakarta.inject.Named; -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.domain.CloudApplication; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; 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.cloudlogging.CloudLoggingServiceClient; import org.cloudfoundry.multiapps.controller.core.model.DeployedMta; import org.cloudfoundry.multiapps.controller.core.model.DeployedMtaApplication; import org.cloudfoundry.multiapps.controller.core.util.LoggingUtil; @@ -25,13 +26,12 @@ 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.cloudlogging.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.core.cloudlogging.CloudLoggingServiceClient; import org.cloudfoundry.multiapps.controller.persistence.services.OperationService; +import org.cloudfoundry.multiapps.controller.persistence.services.cloudlogging.CloudLoggingServiceConfigurationService; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.dynatrace.DynatraceProcessDuration; import org.cloudfoundry.multiapps.controller.process.dynatrace.DynatracePublisher; @@ -213,7 +213,9 @@ private void deleteCloudLoggingServiceConfiguration(DelegateExecution execution) ProcessType processType = processTypeParser.getProcessType(execution); if (processType.equals(ProcessType.UNDEPLOY)) { - cloudLoggingServiceConfigurationService.createQuery().id(loggingConfiguration.getId()).delete(); + cloudLoggingServiceConfigurationService.createQuery() + .id(loggingConfiguration.getId()) + .delete(); cloudLoggingServiceConfigurationAuditLog.logDeleteLoggingConfiguration(VariableHandling.get(execution, Variables.USER), VariableHandling.get(execution, Variables.SPACE_GUID), loggingConfiguration); diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/client/LoggingCloudControllerClientTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/client/LoggingCloudControllerClientTest.java index 27fd087ce1..8f9d46421c 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/client/LoggingCloudControllerClientTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/client/LoggingCloudControllerClientTest.java @@ -4,7 +4,7 @@ import java.util.Optional; import java.util.UUID; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.ApplicationServicesUpdateCallback; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/CreateServiceStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/CreateServiceStepTest.java index 51fe4a72c7..cfa0657540 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/CreateServiceStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/CreateServiceStepTest.java @@ -6,13 +6,13 @@ import java.util.UUID; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.common.util.JsonUtil; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; 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.ServiceInstanceType; import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceOperation; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudServiceInstanceExtended; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DeleteServiceMtaMetadataStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DeleteServiceMtaMetadataStepTest.java index 96a150b2fc..650b4faa3c 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DeleteServiceMtaMetadataStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DeleteServiceMtaMetadataStepTest.java @@ -2,7 +2,7 @@ import java.util.UUID; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudServiceInstanceExtended; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetachServiceKeysFromMtaStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetachServiceKeysFromMtaStepTest.java index 6dc70e3ff9..b3f581c732 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetachServiceKeysFromMtaStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetachServiceKeysFromMtaStepTest.java @@ -5,7 +5,7 @@ import java.util.UUID; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudMetadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetachServicesFromMtaStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetachServicesFromMtaStepTest.java index 40b15d83d3..d3b8e2005e 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetachServicesFromMtaStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetachServicesFromMtaStepTest.java @@ -6,7 +6,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudMetadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetectDeployedMtaStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetectDeployedMtaStepTest.java index 9d9c09c8db..418341ae0d 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetectDeployedMtaStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetectDeployedMtaStepTest.java @@ -4,7 +4,7 @@ import java.util.List; import java.util.Optional; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.common.test.TestUtil; import org.cloudfoundry.multiapps.common.test.Tester.Expectation; import org.cloudfoundry.multiapps.common.util.JsonUtil; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceCreateUpdateServiceActionsStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceCreateUpdateServiceActionsStepTest.java index 069619f4e8..62a2cad73e 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceCreateUpdateServiceActionsStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceCreateUpdateServiceActionsStepTest.java @@ -8,13 +8,13 @@ import java.util.UUID; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; import org.cloudfoundry.multiapps.common.test.TestUtil; import org.cloudfoundry.multiapps.common.util.JsonUtil; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudMetadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType; import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceOperation; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudServiceInstanceExtended; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceDeleteActionsToExecuteStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceDeleteActionsToExecuteStepTest.java index f04e7ca7b6..41a47a3cd8 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceDeleteActionsToExecuteStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceDeleteActionsToExecuteStepTest.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.UUID; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBinding; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceKeysToRecreateStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceKeysToRecreateStepTest.java index 82da244ea5..be639d0bd6 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceKeysToRecreateStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceKeysToRecreateStepTest.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.UUID; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ExtractBatchedServicesWithResolvedDynamicParametersStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ExtractBatchedServicesWithResolvedDynamicParametersStepTest.java index 1a1384edb2..4f8d239545 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ExtractBatchedServicesWithResolvedDynamicParametersStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ExtractBatchedServicesWithResolvedDynamicParametersStepTest.java @@ -9,8 +9,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; import org.cloudfoundry.multiapps.common.ContentException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.core.cf.v2.ResourceType; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBindingOperationExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBindingOperationExecutionTest.java index c4c30ded04..c4304b6b11 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBindingOperationExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBindingOperationExecutionTest.java @@ -3,10 +3,10 @@ import java.util.List; import java.util.UUID; -import org.cloudfoundry.client.v3.jobs.JobState; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; 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.JobState; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.junit.jupiter.api.Test; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBrokerOperationsExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBrokerOperationsExecutionTest.java index fb82e84353..cafb3e24bd 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBrokerOperationsExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBrokerOperationsExecutionTest.java @@ -3,8 +3,8 @@ import java.util.List; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.jobs.JobState; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; +import org.cloudfoundry.multiapps.controller.client.facade.domain.JobState; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudAsyncJob; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceBroker; import org.cloudfoundry.multiapps.controller.process.variables.Variables; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBrokersOperationsExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBrokersOperationsExecutionTest.java index 94760131fd..c00b848696 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBrokersOperationsExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceBrokersOperationsExecutionTest.java @@ -5,8 +5,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.jobs.JobState; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudAsyncJob; +import org.cloudfoundry.multiapps.controller.client.facade.domain.JobState; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceKeyCreationOperationExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceKeyCreationOperationExecutionTest.java index a3b563ae2c..0836b1c072 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceKeyCreationOperationExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceKeyCreationOperationExecutionTest.java @@ -4,8 +4,8 @@ import java.util.UUID; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.jobs.JobState; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; +import org.cloudfoundry.multiapps.controller.client.facade.domain.JobState; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudAsyncJob; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceUnbindingsOperationExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceUnbindingsOperationExecutionTest.java index 671c01c888..e1ad3e6065 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceUnbindingsOperationExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollServiceUnbindingsOperationExecutionTest.java @@ -2,13 +2,13 @@ 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.facade.domain.JobState; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType; 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; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareApplicationForBackupStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareApplicationForBackupStepTest.java index ee1671799e..c3c511a19d 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareApplicationForBackupStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareApplicationForBackupStepTest.java @@ -3,7 +3,7 @@ import java.util.UUID; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudApplicationExtended; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareBackupMtaForDeploymentStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareBackupMtaForDeploymentStepTest.java index 861a639057..4d9262cec4 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareBackupMtaForDeploymentStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareBackupMtaForDeploymentStepTest.java @@ -13,7 +13,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.common.ContentException; import org.cloudfoundry.multiapps.controller.core.cf.detect.DeployedMtaDetector; import org.cloudfoundry.multiapps.controller.core.cf.metadata.ImmutableMtaMetadata; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/RemoveMtaBackupMetadataStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/RemoveMtaBackupMetadataStepTest.java index 3d67e6d51f..a3e0442a32 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/RemoveMtaBackupMetadataStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/RemoveMtaBackupMetadataStepTest.java @@ -2,7 +2,7 @@ import java.util.UUID; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; import org.cloudfoundry.multiapps.controller.core.cf.metadata.ImmutableMtaMetadata; import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadataAnnotations; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/StepsTestUtil.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/StepsTestUtil.java index 53e1e33fb0..daf246a99c 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/StepsTestUtil.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/StepsTestUtil.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.UUID; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.common.util.JsonUtil; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; 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 59ceec125e..5a3065be72 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 @@ -8,7 +8,7 @@ import java.util.UUID; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.common.test.Tester; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UpdateServiceKeysMetadataStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UpdateServiceKeysMetadataStepTest.java index 1533a3af32..7f9d40033c 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UpdateServiceKeysMetadataStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UpdateServiceKeysMetadataStepTest.java @@ -5,7 +5,7 @@ import java.util.List; import java.util.UUID; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudMetadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ExistingAppsToBackupCalculatorTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ExistingAppsToBackupCalculatorTest.java index 1147c87e67..a4d2c66be7 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ExistingAppsToBackupCalculatorTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ExistingAppsToBackupCalculatorTest.java @@ -7,7 +7,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudApplication; import org.cloudfoundry.multiapps.controller.core.cf.metadata.ImmutableMtaMetadata; diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ModuleDeterminerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ModuleDeterminerTest.java index 3b9cb47816..dfa90198c3 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ModuleDeterminerTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ModuleDeterminerTest.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.Map; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientProvider; From 68742580ccb03532d0134fe282cedb601dad962a Mon Sep 17 00:00:00 2001 From: Krasimir Kargov Date: Fri, 7 Aug 2026 14:13:26 +0300 Subject: [PATCH 2/6] Removing more leftovers, changing logging and removing cf-java-client from poms LMCROSSITXSADEPLOY-3054 --- multiapps-controller-client/pom.xml | 38 +- .../src/main/java/module-info.java | 3 - .../controller/client/facade/AllowNulls.java | 16 + .../facade/CloudControllerClientImpl.java | 15 +- .../adapters/CloudFoundryClientFactory.java | 155 - .../facade/adapters/LogCacheClient.java | 110 +- .../facade/adapters/OAuthTokenProvider.java | 27 - .../facade/adapters/RawCloudApplication.java | 72 - .../facade/adapters/RawCloudAsyncJob.java | 51 - .../client/facade/adapters/RawCloudBuild.java | 67 - .../facade/adapters/RawCloudDomain.java | 24 - .../facade/adapters/RawCloudEntity.java | 110 - .../client/facade/adapters/RawCloudEvent.java | 56 - .../facade/adapters/RawCloudPackage.java | 74 - .../facade/adapters/RawCloudProcess.java | 62 - .../client/facade/adapters/RawCloudRoute.java | 99 - .../adapters/RawCloudServiceBinding.java | 37 - .../adapters/RawCloudServiceBroker.java | 40 - .../adapters/RawCloudServiceInstance.java | 56 - .../facade/adapters/RawCloudServiceKey.java | 41 - .../adapters/RawCloudServiceOffering.java | 45 - .../facade/adapters/RawCloudServicePlan.java | 37 - .../client/facade/adapters/RawCloudStack.java | 25 - .../client/facade/adapters/RawCloudTask.java | 50 - .../facade/adapters/RawInstancesInfo.java | 47 - .../client/facade/adapters/RawUserRole.java | 22 - .../adapters/RawV3CloudServiceInstance.java | 25 - .../facade/domain/CloudServiceBinding.java | 2 +- .../facade/domain/CloudServiceInstance.java | 2 +- .../client/facade/domain/CloudServiceKey.java | 2 +- .../client/facade/domain/Lifecycle.java | 2 +- .../client/facade/oauth2/OAuthClient.java | 6 - .../CloudControllerRestClientBuilder.java | 9 +- .../CloudControllerRestClientFactory.java | 149 +- .../rest/CloudControllerRestClientImpl.java | 2631 ----------------- .../facade/rest/CloudControllerV3Client.java | 5 - .../client/facade/rest/CloudSpaceClient.java | 127 +- .../rest/resources/LogCacheReadResponse.java | 45 + .../facade/rest/resources/V3Organization.java | 12 + .../client/facade/rest/resources/V3Space.java | 40 + .../client/facade/util/JobV3Util.java | 48 - ...sCloudControllerClientIntegrationTest.java | 23 +- .../CloudControllerClientIntegrationTest.java | 10 +- .../adapters/OAuthTokenProviderTest.java | 79 - .../adapters/RawCloudApplicationTest.java | 132 - .../facade/adapters/RawCloudAsyncJobTest.java | 82 - .../facade/adapters/RawCloudBuildTest.java | 137 - .../facade/adapters/RawCloudDomainTest.java | 49 - .../facade/adapters/RawCloudEntityTest.java | 158 - .../facade/adapters/RawCloudEventTest.java | 126 - .../facade/adapters/RawCloudPackageTest.java | 113 - .../facade/adapters/RawCloudProcessTest.java | 146 - .../facade/adapters/RawCloudRouteTest.java | 107 - .../adapters/RawCloudServiceBindingTest.java | 74 - .../adapters/RawCloudServiceBrokerTest.java | 55 - .../adapters/RawCloudServiceInstanceTest.java | 117 - .../adapters/RawCloudServiceKeyTest.java | 91 - .../adapters/RawCloudServiceOfferingTest.java | 107 - .../adapters/RawCloudServicePlanTest.java | 92 - .../facade/adapters/RawCloudServiceTest.java | 130 - .../facade/adapters/RawCloudStackTest.java | 40 - .../facade/adapters/RawCloudTaskTest.java | 72 - .../facade/adapters/RawInstancesInfoTest.java | 57 - .../facade/adapters/RawUserRoleTest.java | 49 - .../CloudControllerRestClientImplTest.java | 309 -- multiapps-controller-core/pom.xml | 8 + .../src/main/java/module-info.java | 2 - .../core/cf/CloudControllerClientFactory.java | 9 +- .../src/main/java/module-info.java | 2 - .../process/jobs/OrphanedDataCleaner.java | 4 +- .../web/upload/UploadFromUrlContext.java | 2 +- pom.xml | 16 +- 72 files changed, 326 insertions(+), 6556 deletions(-) create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/AllowNulls.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/CloudFoundryClientFactory.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/OAuthTokenProvider.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplication.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJob.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudBuild.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudDomain.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntity.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEvent.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudPackage.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcess.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudRoute.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBinding.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBroker.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstance.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKey.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceOffering.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServicePlan.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudStack.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudTask.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawInstancesInfo.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawUserRole.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawV3CloudServiceInstance.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientImpl.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/LogCacheReadResponse.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Organization.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Space.java delete mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/util/JobV3Util.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/OAuthTokenProviderTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplicationTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJobTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudBuildTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudDomainTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntityTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEventTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudPackageTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcessTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudRouteTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBindingTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBrokerTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstanceTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKeyTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceOfferingTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServicePlanTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudStackTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudTaskTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawInstancesInfoTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawUserRoleTest.java delete mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientImplTest.java diff --git a/multiapps-controller-client/pom.xml b/multiapps-controller-client/pom.xml index acfb0201c9..ee32a60a98 100644 --- a/multiapps-controller-client/pom.xml +++ b/multiapps-controller-client/pom.xml @@ -85,17 +85,19 @@ value provided - - - org.cloudfoundry - cloudfoundry-client-reactor - - + io.projectreactor.netty reactor-netty + + + + commons-logging + commons-logging + ${commons-logging.version} + @@ -140,30 +142,6 @@ - - org.cloudfoundry - cloudfoundry-client-reactor - ${cloudfoundry-client.version} - - - - org.slf4j - jcl-over-slf4j - - - - com.fasterxml.jackson.core - jackson-databind - - - commons-logging diff --git a/multiapps-controller-client/src/main/java/module-info.java b/multiapps-controller-client/src/main/java/module-info.java index 4201bc0a0b..111c91e054 100644 --- a/multiapps-controller-client/src/main/java/module-info.java +++ b/multiapps-controller-client/src/main/java/module-info.java @@ -12,7 +12,6 @@ exports org.cloudfoundry.multiapps.controller.client.facade.util; exports org.cloudfoundry.multiapps.controller.client.facade.dto; - requires transitive org.cloudfoundry.client; requires spring.security.oauth2.core; requires transitive spring.web; @@ -20,9 +19,7 @@ requires org.apache.commons.collections4; requires org.apache.commons.io; requires org.apache.commons.logging; - requires org.cloudfoundry.client.reactor; requires org.cloudfoundry.multiapps.common; - requires org.cloudfoundry.util; requires org.slf4j; requires java.net.http; requires spring.core; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/AllowNulls.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/AllowNulls.java new file mode 100644 index 0000000000..680efaa956 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/AllowNulls.java @@ -0,0 +1,16 @@ +package org.cloudfoundry.multiapps.controller.client.facade; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks an Immutables collection attribute (typically a {@code Map}) as permitting {@code null} values. Project-owned replacement for + * {@code org.cloudfoundry.AllowNulls} (from cf-java-client) so the domain model no longer depends on the OSS client. Immutables + * recognises this marker by its simple name {@code AllowNulls}. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.CLASS) +public @interface AllowNulls { +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientImpl.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientImpl.java index bdb1855440..e016da176c 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientImpl.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientImpl.java @@ -11,7 +11,6 @@ import java.util.UUID; import java.util.function.Supplier; -import org.cloudfoundry.AbstractCloudFoundryException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudBuild; @@ -40,7 +39,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudControllerRestClient; import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudControllerRestClientFactory; import org.cloudfoundry.multiapps.controller.client.facade.rest.ImmutableCloudControllerRestClientFactory; -import org.springframework.http.HttpStatus; import org.springframework.util.Assert; /** @@ -659,16 +657,9 @@ private void handleExceptions(Runnable runnable) { } private T handleExceptions(Supplier runnable) { - try { - return runnable.get(); - } catch (AbstractCloudFoundryException e) { - throw convertV3ClientException(e); - } - } - - private CloudOperationException convertV3ClientException(AbstractCloudFoundryException e) { - HttpStatus httpStatus = HttpStatus.valueOf(e.getStatusCode()); - return new CloudOperationException(httpStatus, httpStatus.getReasonPhrase(), e.getMessage(), e); + // The underlying CloudControllerRestClient already surfaces failures as CloudOperationException + // (via CloudControllerResponseErrorHandler), so no exception translation is needed here. + return runnable.get(); } } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/CloudFoundryClientFactory.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/CloudFoundryClientFactory.java deleted file mode 100644 index 5461f765a8..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/CloudFoundryClientFactory.java +++ /dev/null @@ -1,155 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.text.MessageFormat; -import java.time.Duration; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; - -import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.client.v3.organizations.OrganizationsV3; -import org.cloudfoundry.client.v3.spaces.SpacesV3; -import org.cloudfoundry.reactor.ConnectionContext; -import org.cloudfoundry.reactor.DefaultConnectionContext; -import org.cloudfoundry.reactor.client.ReactorCloudFoundryClient; -import org.cloudfoundry.reactor.client.v3.organizations.ReactorOrganizationsV3; -import org.cloudfoundry.reactor.client.v3.spaces.ReactorSpacesV3; -import org.immutables.value.Value; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; - -import org.cloudfoundry.multiapps.controller.client.facade.CloudException; -import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; -import org.cloudfoundry.multiapps.controller.client.facade.Messages; -import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; -import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudSpaceClient; -import org.cloudfoundry.multiapps.controller.client.facade.util.CloudUtil; -import org.cloudfoundry.multiapps.controller.client.facade.util.JsonUtil; - -import reactor.core.publisher.Mono; - -@Value.Immutable -public abstract class CloudFoundryClientFactory { - - private static final Logger LOGGER = LoggerFactory.getLogger(CloudFoundryClientFactory.class); - - static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() - .executor(Executors.newSingleThreadExecutor()) - .followRedirects(HttpClient.Redirect.NORMAL) - .connectTimeout(Duration.ofMinutes(10)) - .build(); - - private final Map connectionContextCache = new ConcurrentHashMap<>(); - - public abstract Optional getSslHandshakeTimeout(); - - public abstract Optional getConnectTimeout(); - - public abstract Optional getConnectionPoolSize(); - - public abstract Optional getThreadPoolSize(); - - public abstract Optional getResponseTimeout(); - - public CloudFoundryClient createClient(URL controllerUrl, OAuthClient oAuthClient, Map requestTags) { - return ReactorCloudFoundryClient.builder() - .connectionContext(getOrCreateConnectionContext(controllerUrl.getHost())) - .tokenProvider(oAuthClient.getTokenProvider()) - .requestTags(requestTags) - .build(); - } - - public LogCacheClient createLogCacheClient(URL controllerUrl, OAuthClient oAuthClient, Map requestTags) { - return new LogCacheClient(oAuthClient, requestTags, getOrCreateConnectionContext(controllerUrl.getHost())); - } - - @SuppressWarnings("unchecked") - private Map callCfRoot(URL controllerUrl, Map requestTags) { - HttpResponse response; - try { - HttpRequest request = buildCfRootRequest(controllerUrl, requestTags); - LOGGER.info(MessageFormat.format(Messages.CALLING_CF_ROOT_0_TO_ACCESS_LOG_CACHE_URL, controllerUrl)); - response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() / 100 != 2) { - var status = HttpStatus.valueOf(response.statusCode()); - throw new CloudOperationException(status, status.getReasonPhrase(), response.body()); - } - LOGGER.info(Messages.CF_ROOT_REQUEST_FINISHED); - } catch (InterruptedException | URISyntaxException | IOException e) { - throw new CloudException(e.getMessage(), e); - } - var map = JsonUtil.convertJsonToMap(response.body()); - return (Map) map.get("links"); - } - - private HttpRequest buildCfRootRequest(URL controllerUrl, Map requestTags) throws URISyntaxException { - var requestBuilder = HttpRequest.newBuilder() - .GET() - .uri(controllerUrl.toURI()) - .timeout(Duration.ofMinutes(5)); - requestTags.forEach(requestBuilder::header); - return requestBuilder.build(); - } - - public CloudSpaceClient createSpaceClient(URL controllerUrl, OAuthClient oAuthClient, Map requestTags) { - String v3Api; - try { - var links = CloudUtil.executeWithRetry(() -> callCfRoot(controllerUrl, requestTags)); - @SuppressWarnings("unchecked") - var ccv3 = (Map) links.get("cloud_controller_v3"); - v3Api = (String) ccv3.get("href"); - } catch (CloudException e) { - LOGGER.warn(MessageFormat.format(Messages.CALL_TO_0_FAILED_WITH_1, controllerUrl.toString(), e.getMessage()), e); - v3Api = controllerUrl + "/v3"; - } - var spacesV3 = createV3SpacesClient(controllerUrl, v3Api, oAuthClient, requestTags); - var orgsV3 = createV3OrgsClient(controllerUrl, v3Api, oAuthClient, requestTags); - return new CloudSpaceClient(spacesV3, orgsV3); - } - - private SpacesV3 createV3SpacesClient(URL controllerUrl, String v3Api, OAuthClient oAuthClient, Map requestTags) { - return new ReactorSpacesV3(getOrCreateConnectionContext(controllerUrl.getHost()), - Mono.just(v3Api), - oAuthClient.getTokenProvider(), - requestTags); - } - - private OrganizationsV3 createV3OrgsClient(URL controllerUrl, String v3Api, OAuthClient oAuthClient, Map requestTags) { - return new ReactorOrganizationsV3(getOrCreateConnectionContext(controllerUrl.getHost()), - Mono.just(v3Api), - oAuthClient.getTokenProvider(), - requestTags); - } - - public ConnectionContext getOrCreateConnectionContext(String controllerApiHost) { - return connectionContextCache.computeIfAbsent(controllerApiHost, this::createConnectionContext); - } - - private ConnectionContext createConnectionContext(String controllerApiHost) { - DefaultConnectionContext.Builder builder = DefaultConnectionContext.builder() - .apiHost(controllerApiHost); - getSslHandshakeTimeout().ifPresent(builder::sslHandshakeTimeout); - getConnectTimeout().ifPresent(builder::connectTimeout); - getConnectionPoolSize().ifPresent(builder::connectionPoolSize); - getThreadPoolSize().ifPresent(builder::threadPoolSize); - builder.additionalHttpClientConfiguration(this::getAdditionalHttpClientConfiguration); - return builder.build(); - } - - private reactor.netty.http.client.HttpClient getAdditionalHttpClientConfiguration(reactor.netty.http.client.HttpClient client) { - var clientWithOptions = client; - if (getResponseTimeout().isPresent()) { - clientWithOptions = clientWithOptions.responseTimeout(getResponseTimeout().get()); - } - - return clientWithOptions; - } -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/LogCacheClient.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/LogCacheClient.java index ea8d519b3f..67d4bba810 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/LogCacheClient.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/LogCacheClient.java @@ -1,19 +1,5 @@ package org.cloudfoundry.multiapps.controller.client.facade.adapters; -import org.cloudfoundry.multiapps.controller.client.facade.CloudException; -import org.cloudfoundry.multiapps.controller.client.facade.Messages; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ApplicationLog; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableApplicationLog; -import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; -import org.cloudfoundry.logcache.v1.Envelope; -import org.cloudfoundry.logcache.v1.EnvelopeType; -import org.cloudfoundry.logcache.v1.ReadRequest; -import org.cloudfoundry.logcache.v1.ReadResponse; -import org.cloudfoundry.reactor.ConnectionContext; -import org.cloudfoundry.reactor.logcache.v1.ReactorLogCacheClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.time.Duration; @@ -24,77 +10,77 @@ import java.util.Base64; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.UUID; +import org.cloudfoundry.multiapps.controller.client.facade.CloudException; +import org.cloudfoundry.multiapps.controller.client.facade.Messages; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ApplicationLog; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableApplicationLog; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.LogCacheReadResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.client.RestClient; + +/** + * Fetches recent application logs from Log-Cache. Reimplemented on a plain synchronous Spring {@link RestClient} against Log-Cache's + * JSON {@code read} REST API ({@code GET /api/v1/read/{source_id}}), removing the previous dependency on the OSS cf-java-client + * ({@code org.cloudfoundry.logcache.v1.*} / {@code org.cloudfoundry.reactor.*}). The {@link RestClient} is pre-configured with the + * log-cache base URL, bearer-token auth and request tags by the factory; this class only builds the request and maps the response to + * {@link ApplicationLog}, preserving the previous behaviour exactly. + */ public class LogCacheClient { private static final Logger LOGGER = LoggerFactory.getLogger(LogCacheClient.class); private static final String SOURCE_TYPE_KEY_NAME = "source_type"; private static final int MAX_LOG_COUNT = 1000; - private final OAuthClient oAuthClient; - private final Map requestTags; - private final ConnectionContext connectionContext; - public LogCacheClient(OAuthClient oAuthClient, Map requestTags, ConnectionContext connectionContext) { - this.oAuthClient = oAuthClient; - this.requestTags = requestTags; - this.connectionContext = connectionContext; + private final RestClient restClient; + + public LogCacheClient(RestClient restClient) { + this.restClient = restClient; } public List getRecentLogs(UUID applicationGuid, LocalDateTime offset) { LOGGER.info(Messages.TRYING_TO_GET_APP_LOGS); - org.cloudfoundry.logcache.v1.LogCacheClient logCacheClient = createReactorLogCacheClient(); - ReadResponse applicationLogsResponse = readApplicationLogs(logCacheClient, applicationGuid, offset); + LogCacheReadResponse response = readApplicationLogs(applicationGuid, offset); - if (applicationLogsResponse != null) { - LOGGER.info(Messages.APP_LOGS_WERE_FETCHED_SUCCESSFULLY); - return applicationLogsResponse.getEnvelopes() - .getBatch() - .stream() - .map(this::mapToAppLog) - // we use a linked list so that the log messages can be a LIFO sequence - // that way, we avoid unnecessary sorting and copying to and from another collection/array - .collect(LinkedList::new, LinkedList::addFirst, LinkedList::addAll); - } else { + if (response == null) { throw new CloudException(MessageFormat.format(Messages.FAILED_TO_FETCH_APP_LOGS_FOR_APP, applicationGuid)); } - - } - - private ReactorLogCacheClient createReactorLogCacheClient() { - return ReactorLogCacheClient.builder() - .requestTags(requestTags) - .connectionContext(connectionContext) - .tokenProvider(oAuthClient.getTokenProvider()) - .build(); + LOGGER.info(Messages.APP_LOGS_WERE_FETCHED_SUCCESSFULLY); + return response.batch() + .stream() + .map(this::mapToAppLog) + // we use a linked list so that the log messages can be a LIFO sequence + // that way, we avoid unnecessary sorting and copying to and from another collection/array + .collect(LinkedList::new, LinkedList::addFirst, LinkedList::addAll); } - private ReadResponse readApplicationLogs(org.cloudfoundry.logcache.v1.LogCacheClient logCacheClient, UUID applicationGuid, - LocalDateTime offset) { + private LogCacheReadResponse readApplicationLogs(UUID applicationGuid, LocalDateTime offset) { var instant = offset.toInstant(ZoneOffset.UTC); var secondsInNanos = Duration.ofSeconds(instant.getEpochSecond()) .toNanos(); - return logCacheClient.read(ReadRequest.builder() - .envelopeType(EnvelopeType.LOG) - .sourceId(applicationGuid.toString()) - .descending(Boolean.TRUE) - .limit(MAX_LOG_COUNT) - .startTime(secondsInNanos + instant.getNano() + 1) - .build()) - .block(); + long startTime = secondsInNanos + instant.getNano() + 1; + return restClient.get() + .uri(uriBuilder -> uriBuilder.path("/api/v1/read/{sourceId}") + .queryParam("envelope_types", "LOG") + .queryParam("descending", Boolean.TRUE) + .queryParam("limit", MAX_LOG_COUNT) + .queryParam("start_time", startTime) + .build(applicationGuid.toString())) + .retrieve() + .body(LogCacheReadResponse.class); } - private ApplicationLog mapToAppLog(Envelope envelope) { + private ApplicationLog mapToAppLog(LogCacheReadResponse.Envelope envelope) { return ImmutableApplicationLog.builder() - .applicationGuid(envelope.getSourceId()) - .message(decodeLogPayload(envelope.getLog() - .getPayload())) - .timestamp(fromLogTimestamp(envelope.getTimestamp())) - .messageType(fromLogMessageType(envelope.getLog() - .getType() - .getValue())) - .sourceName(envelope.getTags() + .applicationGuid(envelope.sourceId()) + .message(decodeLogPayload(envelope.log() + .payload())) + .timestamp(fromLogTimestamp(envelope.timestamp())) + .messageType(fromLogMessageType(envelope.log() + .type())) + .sourceName(envelope.tags() .get(SOURCE_TYPE_KEY_NAME)) .build(); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/OAuthTokenProvider.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/OAuthTokenProvider.java deleted file mode 100644 index ff5def4c97..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/OAuthTokenProvider.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.reactor.ConnectionContext; -import org.cloudfoundry.reactor.TokenProvider; - -import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo; -import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; - -import reactor.core.publisher.Mono; - -public class OAuthTokenProvider implements TokenProvider { - - private final OAuthClient oAuthClient; - - public OAuthTokenProvider(OAuthClient oAuthClient) { - this.oAuthClient = oAuthClient; - } - - @Override - public Mono getToken(ConnectionContext connectionContext) { - return Mono.fromSupplier(() -> { - OAuth2AccessTokenWithAdditionalInfo token = oAuthClient.getToken(); - return token.getAuthorizationHeaderValue(); - }); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplication.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplication.java deleted file mode 100644 index 63489ddd70..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplication.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.Collections; -import java.util.Map; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Derivable; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudApplication; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableLifecycle; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Lifecycle; -import org.cloudfoundry.multiapps.controller.client.facade.domain.LifecycleType; -import org.cloudfoundry.client.v3.BuildpackData; -import org.cloudfoundry.client.v3.CnbData; -import org.cloudfoundry.client.v3.LifecycleData; -import org.cloudfoundry.client.v3.applications.Application; -import org.cloudfoundry.client.v3.applications.ApplicationState; -import org.immutables.value.Value; - -@Value.Immutable -public abstract class RawCloudApplication extends RawCloudEntity { - - public static final String BUILDPACKS = "buildpacks"; - public static final String STACK = "stack"; - - public abstract Application getApplication(); - - public abstract Derivable getSpace(); - - @Override - public CloudApplication derive() { - Application app = getApplication(); - return ImmutableCloudApplication.builder() - .metadata(parseResourceMetadata(app)) - .v3Metadata(toDomainMetadata(app.getMetadata())) - .name(app.getName()) - .state(parseState(app.getState())) - .lifecycle(parseLifecycle(app.getLifecycle())) - .space(getSpace().derive()) - .build(); - } - - private static CloudApplication.State parseState(ApplicationState state) { - return CloudApplication.State.valueOf(state.getValue()); - } - - private static Lifecycle parseLifecycle(org.cloudfoundry.client.v3.Lifecycle lifecycle) { - Map data = extractLifecycleData(lifecycle.getData()); - - return ImmutableLifecycle.builder() - .type(LifecycleType.valueOf(lifecycle.getType() - .toString() - .toUpperCase())) - .data(data) - .build(); - } - - private static Map extractLifecycleData(LifecycleData lifecycleData) { - if (lifecycleData instanceof BuildpackData buildpackData) { - return Map.of( - BUILDPACKS, buildpackData.getBuildpacks(), - STACK, buildpackData.getStack()); - } else if (lifecycleData instanceof CnbData cnbData) { - return Map.of( - BUILDPACKS, cnbData.getBuildpacks(), - STACK, cnbData.getStack()); - } else { - return Collections.emptyMap(); - } - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJob.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJob.java deleted file mode 100644 index 3650a8de77..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJob.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.stream.Collectors; - -import org.cloudfoundry.client.v3.jobs.Job; -import org.cloudfoundry.client.v3.jobs.Warning; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudAsyncJob; - -@Value.Immutable -public abstract class RawCloudAsyncJob extends RawCloudEntity { - - @Value.Parameter - public abstract Job getJob(); - - @Override - public CloudAsyncJob derive() { - Job job = getJob(); - return ImmutableCloudAsyncJob.builder() - .metadata(parseResourceMetadata(job)) - .state(org.cloudfoundry.multiapps.controller.client.facade.domain.JobState.valueOf(job.getState() - .name())) - .operation(job.getOperation()) - .warnings(getWarnings(job)) - .errors(getErrors(job)) - .build(); - } - - private String getWarnings(Job job) { - return job.getWarnings() - .stream() - .map(Warning::getDetail) - .collect(Collectors.joining(",")); - } - - private String getErrors(Job job) { - return job.getErrors() - .stream() - .map(this::joinErrorDetails) - .collect(Collectors.joining(",")); - } - - private String joinErrorDetails(org.cloudfoundry.client.v3.Error error) { - return String.join(" ", error.getCode() - .toString(), - error.getTitle(), error.getDetail()); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudBuild.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudBuild.java deleted file mode 100644 index 2cdbc39eaa..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudBuild.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.Optional; - -import org.cloudfoundry.client.v3.Relationship; -import org.cloudfoundry.client.v3.builds.Build; -import org.cloudfoundry.client.v3.builds.BuildState; -import org.cloudfoundry.client.v3.builds.CreatedBy; -import org.cloudfoundry.client.v3.builds.Droplet; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudBuild; -import org.cloudfoundry.multiapps.controller.client.facade.domain.DropletInfo; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudBuild; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudBuild.ImmutableCreatedBy; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudBuild.ImmutablePackageInfo; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableDropletInfo; - -@Value.Immutable -public abstract class RawCloudBuild extends RawCloudEntity { - - private static CloudBuild.CreatedBy parseCreatedBy(Build buildResource) { - CreatedBy createdBy = buildResource.getCreatedBy(); - return ImmutableCreatedBy.builder() - .guid(parseNullableGuid(createdBy.getId())) - .name(createdBy.getName()) - .build(); - } - - private static CloudBuild.PackageInfo parsePackageInfo(Build buildResource) { - Relationship packageRelationship = buildResource.getInputPackage(); - String packageId = packageRelationship.getId(); - return ImmutablePackageInfo.of(parseNullableGuid(packageId)); - } - - private static DropletInfo parseDropletInfo(Build buildResource) { - Droplet droplet = buildResource.getDroplet(); - return Optional.ofNullable(droplet) - .map(Droplet::getId) - .map(RawCloudEntity::parseNullableGuid) - .map(dropletGuid -> ImmutableDropletInfo.builder() - .guid(dropletGuid) - .build()) - .orElse(null); - } - - private static CloudBuild.State parseState(BuildState state) { - return parseEnum(state, CloudBuild.State.class); - } - - @Value.Parameter - public abstract Build getResource(); - - @Override - public CloudBuild derive() { - Build resource = getResource(); - return ImmutableCloudBuild.builder() - .metadata(parseResourceMetadata(resource)) - .createdBy(parseCreatedBy(resource)) - .packageInfo(parsePackageInfo(resource)) - .dropletInfo(parseDropletInfo(resource)) - .state(parseState(resource.getState())) - .error(resource.getError()) - .build(); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudDomain.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudDomain.java deleted file mode 100644 index f03981dbaa..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudDomain.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.client.v3.domains.Domain; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudDomain; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudDomain; - -@Value.Immutable -public abstract class RawCloudDomain extends RawCloudEntity { - - @Value.Parameter - public abstract Domain getResource(); - - @Override - public CloudDomain derive() { - Domain resource = getResource(); - return ImmutableCloudDomain.builder() - .metadata(parseResourceMetadata(resource)) - .name(resource.getName()) - .build(); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntity.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntity.java deleted file mode 100644 index e5da78b0d5..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntity.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.text.MessageFormat; -import java.time.LocalDateTime; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.Collection; -import java.util.UUID; -import java.util.stream.Collectors; - -import org.cloudfoundry.client.v3.Resource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudMetadata; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Derivable; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; - -public abstract class RawCloudEntity implements Derivable { - - private static final Logger LOGGER = LoggerFactory.getLogger(RawCloudEntity.class); - - protected RawCloudEntity() { - // Recommended by Sonar. - } - - public static CloudMetadata parseResourceMetadata(Resource resource) { - return ImmutableCloudMetadata.builder() - .guid(parseNullableGuid(resource.getId())) - .createdAt(parseNullableDate(resource.getCreatedAt())) - .updatedAt(parseNullableDate(resource.getUpdatedAt())) - .build(); - } - - /** - * Convert the OSS {@code LastOperation} to the project-owned {@link org.cloudfoundry.multiapps.controller.client.facade.domain.LastOperation}. - * Shared by the service instance / binding / key adapters. (Part of the OSS-backed path; removed with the OSS client.) - */ - protected static org.cloudfoundry.multiapps.controller.client.facade.domain.LastOperation toDomainLastOperation( - org.cloudfoundry.client.v3.LastOperation lastOperation) { - if (lastOperation == null) { - return null; - } - return new org.cloudfoundry.multiapps.controller.client.facade.domain.LastOperation(lastOperation.getType(), - lastOperation.getState(), - lastOperation.getDescription(), - lastOperation.getCreatedAt(), - lastOperation.getUpdatedAt()); - } - - /** - * Convert the OSS v3 {@code Metadata} to the project-owned - * {@link org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata}. (OSS-backed path; removed with the OSS client.) - */ - protected static org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata toDomainMetadata( - org.cloudfoundry.client.v3.Metadata metadata) { - if (metadata == null) { - return null; - } - return org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata.builder() - .labels(metadata.getLabels()) - .annotations(metadata.getAnnotations()) - .build(); - } - - protected static UUID parseNullableGuid(String guid) { - return guid == null ? null : parseGuid(guid); - } - - protected static UUID parseGuid(String guid) { - try { - return UUID.fromString(guid); - } catch (IllegalArgumentException e) { - LOGGER.warn(MessageFormat.format("Could not parse GUID string: \"{0}\"", guid), e); - return null; - } - } - - protected static LocalDateTime parseNullableDate(String date) { - return date == null ? null : parseDate(date); - } - - protected static LocalDateTime parseDate(String dateString) { - try { - return ZonedDateTime.parse(dateString, DateTimeFormatter.ISO_DATE_TIME) - .toLocalDateTime(); - } catch (DateTimeParseException e) { - LOGGER.warn(MessageFormat.format("Could not parse date string: \"{0}\"", dateString), e); - return null; - } - } - - protected static > E parseEnum(Enum value, Class targetEnum) { - String name = value.name() - .toUpperCase(); - return Enum.valueOf(targetEnum, name); - } - - protected static D deriveFromNullable(Derivable derivable) { - return derivable == null ? null : derivable.derive(); - } - - protected static Collection derive(Collection> derivables) { - return derivables.stream() - .map(Derivable::derive) - .collect(Collectors.toList()); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEvent.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEvent.java deleted file mode 100644 index 7a0937f970..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEvent.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.client.v3.auditevents.AuditEventActor; -import org.cloudfoundry.client.v3.auditevents.AuditEventResource; -import org.cloudfoundry.client.v3.auditevents.AuditEventTarget; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEvent; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEvent.Participant; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudEvent; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudEvent.ImmutableParticipant; - -@Value.Immutable -public abstract class RawCloudEvent extends RawCloudEntity { - - @Value.Parameter - public abstract AuditEventResource getResource(); - - @Override - public CloudEvent derive() { - AuditEventResource resource = getResource(); - return ImmutableCloudEvent.builder() - .metadata(parseResourceMetadata(resource)) - .target(parseTarget(resource)) - .actor(parseActor(resource)) - .type(resource.getType()) - .build(); - } - - private static Participant parseTarget(AuditEventResource resource) { - AuditEventTarget target = resource.getAuditEventTarget(); - if (target == null) { - return ImmutableParticipant.builder() - .build(); - } - return ImmutableParticipant.builder() - .guid(parseNullableGuid(target.getId())) - .name(target.getName()) - .type(target.getType()) - .build(); - } - - private static Participant parseActor(AuditEventResource resource) { - AuditEventActor actor = resource.getAuditEventActor(); - if (actor == null) { - return ImmutableParticipant.builder() - .build(); - } - return ImmutableParticipant.builder() - .guid(parseNullableGuid(actor.getId())) - .name(actor.getName()) - .type(actor.getType()) - .build(); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudPackage.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudPackage.java deleted file mode 100644 index 73933bf781..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudPackage.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.client.v3.Checksum; -import org.cloudfoundry.client.v3.packages.BitsData; -import org.cloudfoundry.client.v3.packages.DockerData; -import org.cloudfoundry.client.v3.packages.Package; -import org.cloudfoundry.client.v3.packages.PackageType; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudPackage; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableBitsData; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudPackage; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableDockerData; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Status; - -@Value.Immutable -public abstract class RawCloudPackage extends RawCloudEntity { - - @Value.Parameter - public abstract Package getResource(); - - @Override - public CloudPackage derive() { - Package resource = getResource(); - return ImmutableCloudPackage.builder() - .metadata(parseResourceMetadata(resource)) - .status(parseStatus(resource)) - .data(parseData(resource)) - .type(parseType(resource)) - .build(); - } - - private static Status parseStatus(Package resource) { - return parseEnum(resource.getState(), Status.class); - } - - private static CloudPackage.PackageData parseData(Package resource) { - if (resource.getType() == PackageType.BITS) { - return parseBitsData((BitsData) resource.getData()); - } - return parseDockerData((DockerData) resource.getData()); - } - - private static CloudPackage.PackageData parseBitsData(BitsData data) { - return ImmutableBitsData.builder() - .checksum(parseBitsChecksum(data.getChecksum())) - .error(data.getError()) - .build(); - } - - private static org.cloudfoundry.multiapps.controller.client.facade.domain.BitsData.Checksum parseBitsChecksum(Checksum checksum) { - if (checksum == null) { - return null; - } - return ImmutableBitsData.ImmutableChecksum.builder() - .algorithm(checksum.getType() - .toString()) - .value(checksum.getValue()) - .build(); - } - - private static CloudPackage.PackageData parseDockerData(DockerData data) { - return ImmutableDockerData.builder() - .image(data.getImage()) - .username(data.getUsername()) - .password(data.getPassword()) - .build(); - } - - private static CloudPackage.Type parseType(Package resource) { - return parseEnum(resource.getType(), CloudPackage.Type.class); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcess.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcess.java deleted file mode 100644 index 931f528b75..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcess.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.client.v3.processes.Data; -import org.cloudfoundry.client.v3.processes.HealthCheck; -import org.cloudfoundry.client.v3.processes.Process; -import org.cloudfoundry.client.v3.processes.ReadinessHealthCheck; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudProcess; -import org.cloudfoundry.multiapps.controller.client.facade.domain.HealthCheckType; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudProcess; -import org.immutables.value.Value; - -@Value.Immutable -public abstract class RawCloudProcess extends RawCloudEntity { - - @Value.Parameter - public abstract Process getProcess(); - - @Override - public CloudProcess derive() { - Process process = getProcess(); - HealthCheck healthCheck = process.getHealthCheck(); - ReadinessHealthCheck readinessHealthCheckType = process.getReadinessHealthCheck(); - Integer healthCheckTimeout = null; - String healthCheckHttpEndpoint = null; - Integer healthCheckInvocationTimeout = null; - Integer healthCheckInterval = null; - if (healthCheck.getData() != null) { - Data healthCheckData = healthCheck.getData(); - healthCheckTimeout = healthCheckData.getTimeout(); - healthCheckInvocationTimeout = healthCheckData.getInvocationTimeout(); - healthCheckHttpEndpoint = healthCheckData.getEndpoint(); - healthCheckInterval = healthCheckData.getInterval(); - } - Integer readinessHealthCheckInvocationTimeout = null; - String readinessHealthCheckHttpEndpoint = null; - Integer readinessHealthCheckInterval = null; - if (readinessHealthCheckType.getData() != null) { - Data readinessHealthCheckData = readinessHealthCheckType.getData(); - readinessHealthCheckInvocationTimeout = readinessHealthCheckData.getInvocationTimeout(); - readinessHealthCheckHttpEndpoint = readinessHealthCheckData.getEndpoint(); - readinessHealthCheckInterval = readinessHealthCheckData.getInterval(); - } - return ImmutableCloudProcess.builder() - .command(process.getCommand()) - .instances(process.getInstances()) - .memoryInMb(process.getMemoryInMb()) - .diskInMb(process.getDiskInMb()) - .healthCheckType(HealthCheckType.valueOf(healthCheck.getType() - .getValue() - .toUpperCase())) - .healthCheckHttpEndpoint(healthCheckHttpEndpoint) - .healthCheckTimeout(healthCheckTimeout) - .healthCheckInvocationTimeout(healthCheckInvocationTimeout) - .healthCheckInterval(healthCheckInterval) - .readinessHealthCheckType(readinessHealthCheckType.getType() - .getValue()) - .readinessHealthCheckHttpEndpoint(readinessHealthCheckHttpEndpoint) - .readinessHealthCheckInvocationTimeout(readinessHealthCheckInvocationTimeout) - .readinessHealthCheckInterval(readinessHealthCheckInterval) - .build(); - } -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudRoute.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudRoute.java deleted file mode 100644 index e2ae758814..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudRoute.java +++ /dev/null @@ -1,99 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.List; -import java.util.Objects; -import java.util.UUID; -import java.util.stream.Collectors; - -import org.cloudfoundry.client.v3.routes.Route; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.Nullable; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudRoute; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudDomain; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudRoute; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableRouteDestination; -import org.cloudfoundry.multiapps.controller.client.facade.domain.RouteDestination; - -@Value.Immutable -public abstract class RawCloudRoute extends RawCloudEntity { - - @Value.Parameter - public abstract Route getRoute(); - - @Nullable - public abstract UUID getApplicationGuid(); - - @Override - public CloudRoute derive() { - Route route = getRoute(); - List destinations = mapDestinations(); - String domainGuid = route.getRelationships() - .getDomain() - .getData() - .getId(); - return ImmutableCloudRoute.builder() - .metadata(parseResourceMetadata(route)) - .appsUsingRoute(route.getDestinations() - .size()) - .host(route.getHost()) - .port(route.getPort()) - .domain(ImmutableCloudDomain.builder() - .name(computeDomain(route)) - .metadata(ImmutableCloudMetadata.of(UUID.fromString(domainGuid))) - .build()) - .path(route.getPath()) - .url(route.getUrl()) - .destinations(destinations) - .requestedProtocol(computeRequestedProtocol(destinations)) - .build(); - } - - private static String computeDomain(Route route) { - String domain = route.getUrl(); - if (!route.getHost() - .isEmpty()) { - domain = domain.substring(route.getHost() - .length() - + 1); - } - if (!route.getPath() - .isEmpty()) { - domain = domain.substring(0, domain.indexOf('/')); - } - if (route.getPort() != null) { - domain = domain.substring(0, domain.indexOf(':')); - } - return domain; - } - - private List mapDestinations() { - return getRoute().getDestinations() - .stream() - .map(destination -> ImmutableRouteDestination.builder() - .metadata(ImmutableCloudMetadata.builder() - .guid(UUID.fromString(destination.getDestinationId())) - .build()) - .applicationGuid(UUID.fromString(destination.getApplication() - .getApplicationId())) - .port(destination.getPort()) - .weight(destination.getWeight()) - .protocol(destination.getProtocol()) - .build()) - .collect(Collectors.toList()); - } - - private String computeRequestedProtocol(List destinations) { - UUID applicationGuid = getApplicationGuid(); - if (applicationGuid == null) { - return null; - } - return destinations.stream() - .filter(destination -> Objects.equals(destination.getApplicationGuid(), applicationGuid)) - .findFirst() - .map(RouteDestination::getProtocol) - .orElse(null); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBinding.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBinding.java deleted file mode 100644 index b91a44dac7..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBinding.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.UUID; - -import org.cloudfoundry.client.v3.servicebindings.ServiceBinding; -import org.cloudfoundry.client.v3.servicebindings.ServiceBindingResource; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBinding; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceBinding; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceCredentialBindingOperation; - -@Value.Immutable -public abstract class RawCloudServiceBinding extends RawCloudEntity { - - @Value.Parameter - public abstract ServiceBindingResource getServiceBinding(); - - @Override - public CloudServiceBinding derive() { - ServiceBinding serviceBinding = getServiceBinding(); - var appRelationship = serviceBinding.getRelationships() - .getApplication(); - return ImmutableCloudServiceBinding.builder() - .metadata(parseResourceMetadata(serviceBinding)) - .applicationGuid(parseNullableGuid(appRelationship == null ? null - : appRelationship.getData() - .getId())) - .serviceInstanceGuid(UUID.fromString(serviceBinding.getRelationships() - .getServiceInstance() - .getData() - .getId())) - .serviceBindingOperation(ServiceCredentialBindingOperation.from(toDomainLastOperation(getServiceBinding().getLastOperation()))) - .build(); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBroker.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBroker.java deleted file mode 100644 index d76fcc5cdd..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBroker.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.Optional; - -import org.cloudfoundry.client.v3.Relationship; -import org.cloudfoundry.client.v3.ToOneRelationship; -import org.cloudfoundry.client.v3.servicebrokers.ServiceBroker; -import org.cloudfoundry.client.v3.servicebrokers.ServiceBrokerRelationships; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBroker; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceBroker; - -@Value.Immutable -public abstract class RawCloudServiceBroker extends RawCloudEntity { - - @Value.Parameter - public abstract ServiceBroker getServiceBroker(); - - @Override - public CloudServiceBroker derive() { - ServiceBroker serviceBroker = getServiceBroker(); - String spaceGuid = getSpaceGuid(serviceBroker); - return ImmutableCloudServiceBroker.builder() - .metadata(parseResourceMetadata(serviceBroker)) - .name(serviceBroker.getName()) - .url(serviceBroker.getUrl()) - .spaceGuid(spaceGuid) - .build(); - } - - private String getSpaceGuid(ServiceBroker serviceBroker) { - return Optional.ofNullable(serviceBroker.getRelationships()) - .map(ServiceBrokerRelationships::getSpace) - .map(ToOneRelationship::getData) - .map(Relationship::getId) - .orElse(null); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstance.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstance.java deleted file mode 100644 index f66b1580aa..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstance.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.Optional; - -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceResource; -import org.cloudfoundry.client.v3.serviceofferings.ServiceOffering; -import org.cloudfoundry.client.v3.serviceplans.ServicePlan; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.Nullable; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceInstance; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceOperation; - -@Value.Immutable -public abstract class RawCloudServiceInstance extends RawCloudEntity { - - @Value.Parameter - public abstract ServiceInstanceResource getResource(); - - @Nullable - public abstract ServicePlan getServicePlan(); - - @Nullable - public abstract ServiceOffering getServiceOffering(); - - @Override - public CloudServiceInstance derive() { - ServiceInstanceResource resource = getResource(); - return ImmutableCloudServiceInstance.builder() - .metadata(parseResourceMetadata(resource)) - .v3Metadata(toDomainMetadata(resource.getMetadata())) - .name(resource.getName()) - .plan(getServicePlanName()) - .label(getLabelName()) - .type(org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType.valueOf(resource.getType() - .name())) - .tags(resource.getTags()) - .lastOperation(ServiceOperation.fromLastOperation(toDomainLastOperation(resource.getLastOperation()))) - .syslogDrainUrl(resource.getSyslogDrainUrl()) - .build(); - } - - private String getServicePlanName() { - return Optional.ofNullable(getServicePlan()) - .map(ServicePlan::getName) - .orElse(null); - } - - private String getLabelName() { - return Optional.ofNullable(getServiceOffering()) - .map(ServiceOffering::getName) - .orElse(null); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKey.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKey.java deleted file mode 100644 index 87afdbe471..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKey.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.Map; - -import org.cloudfoundry.AllowNulls; -import org.cloudfoundry.client.v3.servicebindings.ServiceBindingResource; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.Nullable; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Derivable; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceKey; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceCredentialBindingOperation; - -@Value.Immutable -public abstract class RawCloudServiceKey extends RawCloudEntity { - - public abstract ServiceBindingResource getServiceBindingResource(); - - @Nullable - @AllowNulls - public abstract Map getCredentials(); - - public abstract Derivable getServiceInstance(); - - @Override - public CloudServiceKey derive() { - ServiceBindingResource serviceBindingResource = getServiceBindingResource(); - Derivable serviceInstance = getServiceInstance(); - return ImmutableCloudServiceKey.builder() - .metadata(parseResourceMetadata(serviceBindingResource)) - .v3Metadata(toDomainMetadata(serviceBindingResource.getMetadata())) - .name(serviceBindingResource.getName()) - .credentials(getCredentials()) - .serviceInstance(serviceInstance.derive()) - .serviceKeyOperation(ServiceCredentialBindingOperation.from(toDomainLastOperation(serviceBindingResource.getLastOperation()))) - .build(); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceOffering.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceOffering.java deleted file mode 100644 index c252aa50bd..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceOffering.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.List; - -import org.cloudfoundry.client.v3.serviceofferings.ServiceOfferingResource; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceOffering; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServicePlan; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Derivable; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceOffering; - -@Value.Immutable -public abstract class RawCloudServiceOffering extends RawCloudEntity { - - public abstract ServiceOfferingResource getServiceOffering(); - - public abstract List> getServicePlans(); - - @Override - public CloudServiceOffering derive() { - ServiceOfferingResource serviceOffering = getServiceOffering(); - return ImmutableCloudServiceOffering.builder() - .metadata(parseResourceMetadata(serviceOffering)) - .name(serviceOffering.getName()) - .isAvailable(serviceOffering.getAvailable()) - .isBindable(serviceOffering.getBrokerCatalog() - .getFeatures() - .getBindable()) - .description(serviceOffering.getDescription()) - .isShareable(serviceOffering.getShareable()) - .extra(serviceOffering.getBrokerCatalog() - .getMetadata()) - .docUrl(serviceOffering.getDocumentationUrl()) - .brokerId(serviceOffering.getRelationships() - .getServiceBroker() - .getData() - .getId()) - .uniqueId(serviceOffering.getBrokerCatalog() - .getBrokerCatalogId()) - .servicePlans(derive(getServicePlans())) - .build(); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServicePlan.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServicePlan.java deleted file mode 100644 index a79527037e..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServicePlan.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.client.v3.serviceplans.ServicePlan; -import org.cloudfoundry.client.v3.serviceplans.Visibility; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServicePlan; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServicePlan; - -@Value.Immutable -public abstract class RawCloudServicePlan extends RawCloudEntity { - - @Value.Parameter - public abstract ServicePlan getResource(); - - @Override - public CloudServicePlan derive() { - ServicePlan resource = getResource(); - return ImmutableCloudServicePlan.builder() - .metadata(parseResourceMetadata(resource)) - .name(resource.getName()) - .description(resource.getDescription()) - .extra(resource.getBrokerCatalog() - .getMetadata()) - .uniqueId(resource.getBrokerCatalog() - .getBrokerCatalogId()) - .serviceOfferingId(resource.getRelationships() - .getServiceOffering() - .getData() - .getId()) - .isPublic(resource.getVisibilityType() - .equals(Visibility.PUBLIC)) - .isFree(resource.getFree()) - .build(); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudStack.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudStack.java deleted file mode 100644 index 6c2a8ce152..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudStack.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.client.v3.stacks.Stack; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudStack; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudStack; - -@Value.Immutable -public abstract class RawCloudStack extends RawCloudEntity { - - @Value.Parameter - public abstract Stack getStack(); - - @Override - public CloudStack derive() { - Stack stack = getStack(); - return ImmutableCloudStack.builder() - .metadata(parseResourceMetadata(stack)) - .name(stack.getName()) - .description(stack.getDescription()) - .build(); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudTask.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudTask.java deleted file mode 100644 index b963d4457a..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudTask.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.Optional; - -import org.cloudfoundry.client.v3.tasks.Result; -import org.cloudfoundry.client.v3.tasks.Task; -import org.cloudfoundry.client.v3.tasks.TaskState; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudTask; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudTask; - -@Value.Immutable -public abstract class RawCloudTask extends RawCloudEntity { - - @Value.Parameter - public abstract Task getResource(); - - @Override - public CloudTask derive() { - Task resource = getResource(); - return ImmutableCloudTask.builder() - .metadata(parseResourceMetadata(resource)) - .name(resource.getName()) - .command(resource.getCommand()) - .limits(parseLimits(resource)) - .result(parseResult(resource)) - .state(parseState(resource.getState())) - .build(); - } - - private static CloudTask.Result parseResult(Task resource) { - return Optional.ofNullable(resource.getResult()) - .map(Result::getFailureReason) - .map(ImmutableCloudTask.ImmutableResult::of) - .orElse(null); - } - - private static CloudTask.Limits parseLimits(Task resource) { - return ImmutableCloudTask.ImmutableLimits.builder() - .disk(resource.getDiskInMb()) - .memory(resource.getMemoryInMb()) - .build(); - } - - private static CloudTask.State parseState(TaskState state) { - return parseEnum(state, CloudTask.State.class); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawInstancesInfo.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawInstancesInfo.java deleted file mode 100644 index 1b55721485..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawInstancesInfo.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -import org.cloudfoundry.client.v3.applications.GetApplicationProcessStatisticsResponse; -import org.cloudfoundry.client.v3.processes.ProcessStatisticsResource; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableInstanceInfo; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableInstancesInfo; -import org.cloudfoundry.multiapps.controller.client.facade.domain.InstanceInfo; -import org.cloudfoundry.multiapps.controller.client.facade.domain.InstanceState; -import org.cloudfoundry.multiapps.controller.client.facade.domain.InstancesInfo; -import org.immutables.value.Value; - -@Value.Immutable -public abstract class RawInstancesInfo extends RawCloudEntity { - - @Value.Parameter - public abstract GetApplicationProcessStatisticsResponse getProcessStatisticsResponse(); - - @Override - public InstancesInfo derive() { - var processStats = getProcessStatisticsResponse(); - return ImmutableInstancesInfo.builder() - .instances(parseProcessStatistics(processStats.getResources())) - .build(); - } - - private static List parseProcessStatistics(List stats) { - if (stats == null) { - return Collections.emptyList(); - } - return stats.stream() - .map(RawInstancesInfo::parseProcessStatistic) - .collect(Collectors.toList()); - } - - private static InstanceInfo parseProcessStatistic(ProcessStatisticsResource statsResource) { - return ImmutableInstanceInfo.builder() - .index(statsResource.getIndex()) - .state(InstanceState.valueOfWithDefault(statsResource.getState() - .getValue())) - .isRoutable(Boolean.parseBoolean(statsResource.getRoutable())) - .build(); - } -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawUserRole.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawUserRole.java deleted file mode 100644 index 18aee09f06..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawUserRole.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.Derivable; -import org.cloudfoundry.client.v3.roles.RoleResource; -import org.immutables.value.Value; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.UserRole; - -@Value.Immutable -public abstract class RawUserRole implements Derivable { - - @Value.Parameter - public abstract RoleResource getRoleResource(); - - @Override - public UserRole derive() { - RoleResource role = getRoleResource(); - return UserRole.fromRoleType(role.getType() - .getValue()); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawV3CloudServiceInstance.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawV3CloudServiceInstance.java deleted file mode 100644 index db610973ef..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawV3CloudServiceInstance.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceInstance; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstance; -import org.immutables.value.Value; - -@Value.Immutable -public abstract class RawV3CloudServiceInstance extends RawCloudEntity { - - @Value.Parameter - public abstract ServiceInstance getServiceInstance(); - - @Override - public CloudServiceInstance derive() { - ServiceInstance serviceInstance = getServiceInstance(); - return ImmutableCloudServiceInstance.builder() - .metadata(parseResourceMetadata(serviceInstance)) - .v3Metadata(toDomainMetadata(serviceInstance.getMetadata())) - .name(serviceInstance.getName()) - .tags(serviceInstance.getTags()) - .build(); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceBinding.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceBinding.java index bceb94223b..e3fb8544fe 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceBinding.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceBinding.java @@ -2,7 +2,7 @@ import java.util.UUID; -import org.cloudfoundry.Nullable; +import org.cloudfoundry.multiapps.controller.client.facade.Nullable; import org.immutables.value.Value; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceInstance.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceInstance.java index 06f546412c..2cf5216efa 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceInstance.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceInstance.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.Map; -import org.cloudfoundry.AllowNulls; +import org.cloudfoundry.multiapps.controller.client.facade.AllowNulls; import org.immutables.value.Value; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceKey.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceKey.java index a77cad1967..76c211eeda 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceKey.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceKey.java @@ -2,7 +2,7 @@ import java.util.Map; -import org.cloudfoundry.AllowNulls; +import org.cloudfoundry.multiapps.controller.client.facade.AllowNulls; import org.immutables.value.Value; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Lifecycle.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Lifecycle.java index f448f2c668..d6453dd89e 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Lifecycle.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Lifecycle.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.cloudfoundry.multiapps.controller.client.facade.Nullable; -import org.cloudfoundry.AllowNulls; +import org.cloudfoundry.multiapps.controller.client.facade.AllowNulls; import org.immutables.value.Value; import java.util.Map; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/oauth2/OAuthClient.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/oauth2/OAuthClient.java index 414fc822b8..03ae12365f 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/oauth2/OAuthClient.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/oauth2/OAuthClient.java @@ -8,9 +8,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; import org.cloudfoundry.multiapps.controller.client.facade.Constants; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.OAuthTokenProvider; import org.cloudfoundry.multiapps.controller.client.facade.util.JsonUtil; -import org.cloudfoundry.reactor.TokenProvider; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.LinkedMultiValueMap; @@ -78,10 +76,6 @@ public String getAuthorizationHeaderValue() { return null; } - public TokenProvider getTokenProvider() { - return new OAuthTokenProvider(this); - } - private boolean shouldRefreshToken() { return credentials.isRefreshable() && token.getOAuth2AccessToken() .getExpiresAt() diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java index 4c0d158f1d..bcf7f5e322 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java @@ -18,6 +18,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.client.JdkClientHttpRequestFactory; import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.client.RestClient; @@ -56,10 +57,14 @@ public static RestClient build(URL v3ApiUrl, OAuthClient oAuthClient, Map // MappingJackson2YamlHttpMessageConverter -> Jackson2ObjectMapperBuilder.yaml(), which loads a YAMLFactory that is // incompatible with this codebase's jackson-core (mixed Jackson 2/3 classpath) and throws VerifyError at load time - // — so clearing the list afterwards is too late. The List overload skips initMessageConverters() entirely, so the - // YAML converter is never built. CF v3 is JSON-only; JSON + String + byte[] (from an ObjectMapper we control) suffice. + // — so clearing the list afterwards is too late. The List overload skips initMessageConverters() entirely. + // Converters: byte[] + String + JSON (from an ObjectMapper we control) for the v3 JSON APIs, plus a plain + // FormHttpMessageConverter for multipart/form-data (the POST /v3/packages/{guid}/upload app-bits upload). We use + // FormHttpMessageConverter, NOT AllEncompassingFormHttpMessageConverter, precisely because the latter is what + // triggers the YAML VerifyError above. .messageConverters(List.of(new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(), + new FormHttpMessageConverter(), new MappingJackson2HttpMessageConverter(new ObjectMapper()))) .requestInterceptor((request, body, execution) -> { String authorization = oAuthClient.getAuthorizationHeaderValue(); diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java index 47a209a587..fa394c5925 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java @@ -1,19 +1,21 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.CloudFoundryClientFactory; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableCloudFoundryClientFactory; +import org.cloudfoundry.multiapps.controller.client.facade.CloudException; +import org.cloudfoundry.multiapps.controller.client.facade.adapters.LogCacheClient; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; import org.cloudfoundry.multiapps.controller.client.facade.util.JsonUtil; import org.cloudfoundry.multiapps.controller.client.facade.util.RestUtil; -import org.cloudfoundry.client.CloudFoundryClient; import org.immutables.value.Value; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.client.RestClient; +import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; +import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; @@ -21,9 +23,15 @@ import java.util.Map; import java.util.Optional; +/** + * Builds the in-house {@link CloudControllerRestClient} (and the auxiliary {@link CloudSpaceClient} / {@link LogCacheClient}) that talk to + * the Cloud Controller v3 REST API directly through a blocking Spring {@link RestClient}, with no dependency on the OSS cf-java-client. + */ @Value.Immutable public abstract class CloudControllerRestClientFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(CloudControllerRestClientFactory.class); + private final RestUtil restUtil = new RestUtil(); public abstract Optional getSslHandshakeTimeout(); @@ -41,35 +49,10 @@ public boolean shouldTrustSelfSignedCertificates() { return false; } - /** - * Feature flag for the cf-java-client migration PoC. When {@code true}, {@link #createClient} builds the in-house - * {@link CloudControllerRestClientV3Impl} (direct CF v3 REST, no cf-java-client); when {@code false} (default), it builds the - * OSS-backed {@link CloudControllerRestClientImpl}. Kept default-off so existing behaviour is unchanged and the two can be A/B - * compared against the same live Cloud Foundry. - */ - @Value.Default - public boolean useV3RestClient() { - // cf_java_client_poc branch: default ON so the in-house CloudControllerRestClientV3Impl is always chosen for OQ verification. - // Do NOT merge this default to main — the switch belongs off there until the client is validated. - return true; - } - - @Value.Derived - public CloudFoundryClientFactory getCloudFoundryClientFactory() { - ImmutableCloudFoundryClientFactory.Builder builder = ImmutableCloudFoundryClientFactory.builder(); - getSslHandshakeTimeout().ifPresent(builder::sslHandshakeTimeout); - getConnectTimeout().ifPresent(builder::connectTimeout); - getConnectionPoolSize().ifPresent(builder::connectionPoolSize); - getThreadPoolSize().ifPresent(builder::threadPoolSize); - getResponseTimeout().ifPresent(builder::responseTimeout); - return builder.build(); - } - public CloudControllerRestClient createClient(URL controllerUrl, CloudCredentials credentials, String organizationName, String spaceName, OAuthClient oAuthClient, Map requestTags) { oAuthClient.init(credentials); - CloudSpaceClient spaceGetter = getCloudFoundryClientFactory().createSpaceClient(controllerUrl, oAuthClient, requestTags); - CloudSpace target = spaceGetter.getSpace(organizationName, spaceName); + CloudSpace target = createSpaceClient(controllerUrl, oAuthClient, requestTags).getSpace(organizationName, spaceName); return createClient(controllerUrl, credentials, target, oAuthClient, requestTags); } @@ -81,45 +64,78 @@ public CloudControllerRestClient createClient(URL controllerUrl, CloudCredential public CloudControllerRestClient createClient(URL controllerUrl, CloudCredentials credentials, CloudSpace target, OAuthClient oAuthClient, Map requestTags) { oAuthClient.init(credentials); - if (useV3RestClient()) { - return createV3RestClient(controllerUrl, target, oAuthClient, requestTags); - } - CloudFoundryClient delegate = getCloudFoundryClientFactory().createClient(controllerUrl, oAuthClient, requestTags); - return new CloudControllerRestClientImpl(delegate, target); + URL v3ApiUrl = deriveV3ApiUrl(controllerUrl, requestTags); + RestClient restClient = buildRestClient(toOrigin(v3ApiUrl), oAuthClient, requestTags); + return new CloudControllerRestClientV3Impl(v3ApiUrl, oAuthClient, target, restClient); } - private CloudControllerRestClient createV3RestClient(URL controllerUrl, CloudSpace target, OAuthClient oAuthClient, - Map requestTags) { + public CloudSpaceClient createSpaceClient(URL controllerUrl, OAuthClient oAuthClient, Map requestTags) { URL v3ApiUrl = deriveV3ApiUrl(controllerUrl, requestTags); - // The RestClient baseUrl must be the API ORIGIN (scheme://host[:port]) only — every operation path already carries the - // "/v3/..." prefix, so using the ".../v3" href as the base would produce a duplicated "/v3/v3/apps" path (CF -> 404 Unknown - // request). We keep v3ApiUrl (which includes /v3) for reference/root-link semantics but strip it to the origin for the base. - URL baseUrl = toOrigin(v3ApiUrl); - LOGGER.warn("[cf-v3-poc] controllerUrl={} resolved v3ApiUrl={} restClientBaseUrl={}", controllerUrl, v3ApiUrl, baseUrl); + RestClient restClient = buildRestClient(toOrigin(v3ApiUrl), oAuthClient, requestTags); + return new CloudSpaceClient(new CloudControllerV3Client(restClient)); + } + + public LogCacheClient createLogCacheClient(URL controllerUrl, OAuthClient oAuthClient, Map requestTags) { + URL logCacheUrl = deriveLogCacheUrl(controllerUrl, requestTags); + RestClient restClient = buildRestClient(logCacheUrl, oAuthClient, requestTags); + return new LogCacheClient(restClient); + } + + private RestClient buildRestClient(URL baseUrl, OAuthClient oAuthClient, Map requestTags) { var transportOptions = new CloudControllerRestClientBuilder.TransportOptions(getConnectTimeout(), getResponseTimeout(), getConnectionPoolSize(), shouldTrustSelfSignedCertificates()); - RestClient restClient = CloudControllerRestClientBuilder.build(baseUrl, oAuthClient, requestTags, transportOptions); - return new CloudControllerRestClientV3Impl(v3ApiUrl, oAuthClient, target, restClient); + return CloudControllerRestClientBuilder.build(baseUrl, oAuthClient, requestTags, transportOptions); } - // scheme://host[:port] of the given URL, dropping any path (e.g. the trailing /v3). + // scheme://host[:port] of the given URL, dropping any path (e.g. the trailing /v3). The RestClient baseUrl must be the origin only, + // because every operation path already carries the "/v3/..." prefix. private URL toOrigin(URL url) { try { int port = url.getPort(); String origin = url.getProtocol() + "://" + url.getHost() + (port == -1 ? "" : ":" + port); - return java.net.URI.create(origin) - .toURL(); - } catch (java.net.MalformedURLException e) { + return URI.create(origin) + .toURL(); + } catch (MalformedURLException e) { throw new IllegalArgumentException("Could not derive the API origin from " + url, e); } } - // Resolve the CF v3 API base URL the same way the OSS factory does: GET the CF root and read links.cloud_controller_v3.href. - // The conventional controllerUrl + "/v3" is only a fallback — on some landscapes the real v3 href differs, and using the naive - // path yields "404 Not Found: Unknown request" from the Cloud Controller. + // Resolve the CF v3 API base URL from the CF root "cloud_controller_v3" link; fall back to controllerUrl + "/v3". @SuppressWarnings("unchecked") private URL deriveV3ApiUrl(URL controllerUrl, Map requestTags) { + try { + Map links = fetchRootLinks(controllerUrl, requestTags); + Map ccv3 = (Map) links.get("cloud_controller_v3"); + return URI.create((String) ccv3.get("href")) + .toURL(); + } catch (Exception e) { + return fallbackUrl(controllerUrl, "/v3"); + } + } + + // Log-Cache base URL from the CF root "log_cache" link; fall back to the "log-cache" subdomain of the controller host. + @SuppressWarnings("unchecked") + private URL deriveLogCacheUrl(URL controllerUrl, Map requestTags) { + try { + Map links = fetchRootLinks(controllerUrl, requestTags); + Map logCache = (Map) links.get("log_cache"); + return URI.create((String) logCache.get("href")) + .toURL(); + } catch (Exception e) { + String host = controllerUrl.getHost(); + String logCacheHost = host.startsWith("api.") ? "log-cache." + host.substring("api.".length()) : "log-cache." + host; + try { + return URI.create(controllerUrl.getProtocol() + "://" + logCacheHost) + .toURL(); + } catch (MalformedURLException me) { + throw new CloudException("Could not derive the log-cache URL from " + controllerUrl, me); + } + } + } + + @SuppressWarnings("unchecked") + private Map fetchRootLinks(URL controllerUrl, Map requestTags) { try { HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .GET() @@ -128,39 +144,34 @@ private URL deriveV3ApiUrl(URL controllerUrl, Map requestTags) { requestTags.forEach(requestBuilder::header); HttpResponse response = rootHttpClient().send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); if (response.statusCode() / 100 != 2) { - return fallbackV3ApiUrl(controllerUrl); + throw new CloudException("CF root request to " + controllerUrl + " returned " + response.statusCode()); } - Map links = (Map) JsonUtil.convertJsonToMap(response.body()) - .get("links"); - Map ccv3 = (Map) links.get("cloud_controller_v3"); - String href = (String) ccv3.get("href"); - return java.net.URI.create(href) - .toURL(); + return (Map) JsonUtil.convertJsonToMap(response.body()) + .get("links"); } catch (InterruptedException e) { Thread.currentThread() .interrupt(); - return fallbackV3ApiUrl(controllerUrl); + throw new CloudException("Interrupted while calling the CF root at " + controllerUrl, e); } catch (Exception e) { - return fallbackV3ApiUrl(controllerUrl); + throw new CloudException("Failed to call the CF root at " + controllerUrl + ": " + e.getMessage(), e); } } - private static java.net.http.HttpClient rootHttpClient() { - return java.net.http.HttpClient.newBuilder() - .connectTimeout(Duration.ofMinutes(1)) - .build(); + private static HttpClient rootHttpClient() { + return HttpClient.newBuilder() + .connectTimeout(Duration.ofMinutes(1)) + .build(); } - private URL fallbackV3ApiUrl(URL controllerUrl) { + private URL fallbackUrl(URL controllerUrl, String pathSuffix) { try { - return java.net.URI.create(controllerUrl.toString() + "/v3") - .toURL(); - } catch (java.net.MalformedURLException e) { - throw new IllegalArgumentException("Could not derive the CF v3 API URL from " + controllerUrl, e); + return URI.create(controllerUrl.toString() + pathSuffix) + .toURL(); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Could not derive URL from " + controllerUrl + pathSuffix, e); } } - private OAuthClient createOAuthClient(URL controllerUrl, String origin) { return restUtil.createOAuthClientByControllerUrl(controllerUrl, shouldTrustSelfSignedCertificates()); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientImpl.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientImpl.java deleted file mode 100644 index 95172fea6c..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientImpl.java +++ /dev/null @@ -1,2631 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.rest; - -import java.nio.file.Path; -import java.text.MessageFormat; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; -import java.util.function.IntFunction; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import org.cloudfoundry.AbstractCloudFoundryException; -import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.client.v3.BuildpackData; -import org.cloudfoundry.client.v3.DockerData; -import org.cloudfoundry.client.v3.Lifecycle; -import org.cloudfoundry.client.v3.LifecycleType; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; -import org.cloudfoundry.client.v3.Relationship; -import org.cloudfoundry.client.v3.Resource; -import org.cloudfoundry.client.v3.ToOneRelationship; -import org.cloudfoundry.client.v3.applications.Application; -import org.cloudfoundry.client.v3.applications.ApplicationFeature; -import org.cloudfoundry.client.v3.applications.ApplicationRelationships; -import org.cloudfoundry.client.v3.applications.ApplicationState; -import org.cloudfoundry.client.v3.applications.CreateApplicationRequest; -import org.cloudfoundry.client.v3.applications.CreateApplicationResponse; -import org.cloudfoundry.client.v3.applications.DeleteApplicationRequest; -import org.cloudfoundry.client.v3.applications.GetApplicationCurrentDropletRequest; -import org.cloudfoundry.client.v3.applications.GetApplicationCurrentDropletResponse; -import org.cloudfoundry.client.v3.applications.GetApplicationEnvironmentVariablesRequest; -import org.cloudfoundry.client.v3.applications.GetApplicationEnvironmentVariablesResponse; -import org.cloudfoundry.client.v3.applications.GetApplicationProcessRequest; -import org.cloudfoundry.client.v3.applications.GetApplicationProcessResponse; -import org.cloudfoundry.client.v3.applications.GetApplicationProcessStatisticsRequest; -import org.cloudfoundry.client.v3.applications.GetApplicationProcessStatisticsResponse; -import org.cloudfoundry.client.v3.applications.GetApplicationRequest; -import org.cloudfoundry.client.v3.applications.GetApplicationSshEnabledRequest; -import org.cloudfoundry.client.v3.applications.GetApplicationSshEnabledResponse; -import org.cloudfoundry.client.v3.applications.ListApplicationBuildsRequest; -import org.cloudfoundry.client.v3.applications.ListApplicationFeaturesRequest; -import org.cloudfoundry.client.v3.applications.ListApplicationPackagesRequest; -import org.cloudfoundry.client.v3.applications.ListApplicationRoutesRequest; -import org.cloudfoundry.client.v3.applications.ListApplicationsRequest; -import org.cloudfoundry.client.v3.applications.ScaleApplicationRequest; -import org.cloudfoundry.client.v3.applications.SetApplicationCurrentDropletRequest; -import org.cloudfoundry.client.v3.applications.StartApplicationRequest; -import org.cloudfoundry.client.v3.applications.StopApplicationRequest; -import org.cloudfoundry.client.v3.applications.UpdateApplicationEnvironmentVariablesRequest; -import org.cloudfoundry.client.v3.applications.UpdateApplicationFeatureRequest; -import org.cloudfoundry.client.v3.applications.UpdateApplicationRequest; -import org.cloudfoundry.client.v3.applications.UpdateApplicationResponse; -import org.cloudfoundry.client.v3.auditevents.AuditEventResource; -import org.cloudfoundry.client.v3.auditevents.ListAuditEventsRequest; -import org.cloudfoundry.client.v3.builds.Build; -import org.cloudfoundry.client.v3.builds.CreateBuildRequest; -import org.cloudfoundry.client.v3.builds.GetBuildRequest; -import org.cloudfoundry.client.v3.builds.ListBuildsRequest; -import org.cloudfoundry.client.v3.domains.CreateDomainRequest; -import org.cloudfoundry.client.v3.domains.DeleteDomainRequest; -import org.cloudfoundry.client.v3.domains.Domain; -import org.cloudfoundry.client.v3.domains.DomainRelationships; -import org.cloudfoundry.client.v3.domains.DomainResource; -import org.cloudfoundry.client.v3.domains.ListDomainsRequest; -import org.cloudfoundry.client.v3.jobs.GetJobRequest; -import org.cloudfoundry.client.v3.organizations.GetOrganizationDefaultDomainRequest; -import org.cloudfoundry.client.v3.organizations.ListOrganizationDomainsRequest; -import org.cloudfoundry.client.v3.packages.CreatePackageRequest; -import org.cloudfoundry.client.v3.packages.CreatePackageResponse; -import org.cloudfoundry.client.v3.packages.GetPackageRequest; -import org.cloudfoundry.client.v3.packages.Package; -import org.cloudfoundry.client.v3.packages.PackageRelationships; -import org.cloudfoundry.client.v3.packages.PackageResource; -import org.cloudfoundry.client.v3.packages.PackageType; -import org.cloudfoundry.client.v3.packages.UploadPackageRequest; -import org.cloudfoundry.client.v3.processes.Data; -import org.cloudfoundry.client.v3.processes.HealthCheck; -import org.cloudfoundry.client.v3.processes.HealthCheckType; -import org.cloudfoundry.client.v3.processes.ReadinessHealthCheck; -import org.cloudfoundry.client.v3.processes.ReadinessHealthCheckType; -import org.cloudfoundry.client.v3.processes.UpdateProcessRequest; -import org.cloudfoundry.client.v3.roles.ListRolesRequest; -import org.cloudfoundry.client.v3.roles.RoleResource; -import org.cloudfoundry.client.v3.roles.RoleType; -import org.cloudfoundry.client.v3.routes.CreateRouteRequest; -import org.cloudfoundry.client.v3.routes.CreateRouteResponse; -import org.cloudfoundry.client.v3.routes.DeleteRouteRequest; -import org.cloudfoundry.client.v3.routes.Destination; -import org.cloudfoundry.client.v3.routes.InsertRouteDestinationsRequest; -import org.cloudfoundry.client.v3.routes.ListRoutesRequest; -import org.cloudfoundry.client.v3.routes.RemoveRouteDestinationsRequest; -import org.cloudfoundry.client.v3.routes.RouteRelationships; -import org.cloudfoundry.client.v3.routes.RouteResource; -import org.cloudfoundry.client.v3.servicebindings.CreateServiceBindingRequest; -import org.cloudfoundry.client.v3.servicebindings.CreateServiceBindingResponse; -import org.cloudfoundry.client.v3.servicebindings.DeleteServiceBindingRequest; -import org.cloudfoundry.client.v3.servicebindings.GetServiceBindingDetailsRequest; -import org.cloudfoundry.client.v3.servicebindings.GetServiceBindingDetailsResponse; -import org.cloudfoundry.client.v3.servicebindings.GetServiceBindingParametersRequest; -import org.cloudfoundry.client.v3.servicebindings.GetServiceBindingParametersResponse; -import org.cloudfoundry.client.v3.servicebindings.ListServiceBindingsRequest; -import org.cloudfoundry.client.v3.servicebindings.ServiceBindingRelationships; -import org.cloudfoundry.client.v3.servicebindings.ServiceBindingResource; -import org.cloudfoundry.client.v3.servicebindings.ServiceBindingType; -import org.cloudfoundry.client.v3.servicebindings.UpdateServiceBindingRequest; -import org.cloudfoundry.client.v3.servicebrokers.BasicAuthentication; -import org.cloudfoundry.client.v3.servicebrokers.CreateServiceBrokerRequest; -import org.cloudfoundry.client.v3.servicebrokers.DeleteServiceBrokerRequest; -import org.cloudfoundry.client.v3.servicebrokers.ListServiceBrokersRequest; -import org.cloudfoundry.client.v3.servicebrokers.ServiceBrokerRelationships; -import org.cloudfoundry.client.v3.servicebrokers.ServiceBrokerResource; -import org.cloudfoundry.client.v3.servicebrokers.UpdateServiceBrokerRequest; -import org.cloudfoundry.client.v3.serviceinstances.CreateServiceInstanceRequest; -import org.cloudfoundry.client.v3.serviceinstances.DeleteServiceInstanceRequest; -import org.cloudfoundry.client.v3.serviceinstances.GetManagedServiceParametersRequest; -import org.cloudfoundry.client.v3.serviceinstances.GetManagedServiceParametersResponse; -import org.cloudfoundry.client.v3.serviceinstances.GetServiceInstanceRequest; -import org.cloudfoundry.client.v3.serviceinstances.GetUserProvidedCredentialsRequest; -import org.cloudfoundry.client.v3.serviceinstances.GetUserProvidedCredentialsResponse; -import org.cloudfoundry.client.v3.serviceinstances.ListServiceInstancesRequest; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstance; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceRelationships; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceResource; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; -import org.cloudfoundry.client.v3.serviceinstances.UpdateServiceInstanceRequest; -import org.cloudfoundry.client.v3.serviceofferings.GetServiceOfferingRequest; -import org.cloudfoundry.client.v3.serviceofferings.ListServiceOfferingsRequest; -import org.cloudfoundry.client.v3.serviceofferings.ServiceOffering; -import org.cloudfoundry.client.v3.serviceofferings.ServiceOfferingResource; -import org.cloudfoundry.client.v3.serviceplans.GetServicePlanRequest; -import org.cloudfoundry.client.v3.serviceplans.ListServicePlansRequest; -import org.cloudfoundry.client.v3.serviceplans.ServicePlan; -import org.cloudfoundry.client.v3.serviceplans.ServicePlanResource; -import org.cloudfoundry.client.v3.serviceplans.UpdateServicePlanVisibilityRequest; -import org.cloudfoundry.client.v3.serviceplans.Visibility; -import org.cloudfoundry.client.v3.spaces.DeleteUnmappedRoutesRequest; -import org.cloudfoundry.client.v3.stacks.ListStacksRequest; -import org.cloudfoundry.client.v3.stacks.Stack; -import org.cloudfoundry.client.v3.tasks.CancelTaskRequest; -import org.cloudfoundry.client.v3.tasks.CreateTaskRequest; -import org.cloudfoundry.client.v3.tasks.GetTaskRequest; -import org.cloudfoundry.client.v3.tasks.ListTasksRequest; -import org.cloudfoundry.client.v3.tasks.Task; -import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; -import org.cloudfoundry.multiapps.controller.client.facade.Constants; -import org.cloudfoundry.multiapps.controller.client.facade.Messages; -import org.cloudfoundry.multiapps.controller.client.facade.UploadStatusCallback; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudApplication; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudAsyncJob; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudBuild; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudDomain; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudEvent; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudPackage; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudProcess; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudRoute; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudServiceBinding; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudServiceBroker; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudServiceInstance; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudServiceKey; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudServiceOffering; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudServicePlan; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudStack; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawCloudTask; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawInstancesInfo; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawUserRole; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableRawV3CloudServiceInstance; -import org.cloudfoundry.multiapps.controller.client.facade.domain.BitsData; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudBuild; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudDomain; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEntity; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEvent; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudMetadata; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudPackage; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudProcess; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudRoute; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBinding; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBroker; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceOffering; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServicePlan; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudStack; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudTask; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Derivable; -import org.cloudfoundry.multiapps.controller.client.facade.domain.DockerCredentials; -import org.cloudfoundry.multiapps.controller.client.facade.domain.DockerInfo; -import org.cloudfoundry.multiapps.controller.client.facade.domain.DropletInfo; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ErrorDetails; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableDropletInfo; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableErrorDetails; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableInstancesInfo; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableUpload; -import org.cloudfoundry.multiapps.controller.client.facade.domain.InstancesInfo; -import org.cloudfoundry.multiapps.controller.client.facade.domain.RouteDestination; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ServicePlanVisibility; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Status; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Upload; -import org.cloudfoundry.multiapps.controller.client.facade.domain.UserRole; -import org.cloudfoundry.multiapps.controller.client.facade.dto.ApplicationToCreateDto; -import org.cloudfoundry.multiapps.controller.client.facade.util.JobV3Util; -import org.cloudfoundry.util.PaginationUtils; -import org.springframework.http.HttpStatus; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** - * Abstract implementation of the CloudControllerClient intended to serve as the base. - */ -public class CloudControllerRestClientImpl implements CloudControllerRestClient { - - private static final long PACKAGE_UPLOAD_JOB_POLLING_PERIOD = TimeUnit.SECONDS.toMillis(5); - private static final Duration DELETE_JOB_TIMEOUT = Duration.ofMinutes(5); - private static final Duration BINDING_OPERATIONS_TIMEOUT = Duration.ofMinutes(10); - private static final int MAX_CHAR_LENGTH_FOR_PARAMS_IN_REQUEST = 4000; - - private CloudSpace target; // optional, as some operations do not require a targeted space - private CloudFoundryClient delegate; - - /** - * Only for unit tests. This works around the fact that the initialize method is called within the constructor and hence can not be - * overloaded, making it impossible to write unit tests that don't trigger network calls. - */ - protected CloudControllerRestClientImpl() { - } - - public CloudControllerRestClientImpl(CloudFoundryClient delegate) { - this(delegate, null); - } - - public CloudControllerRestClientImpl(CloudFoundryClient delegate, CloudSpace target) { - this.target = target; - this.delegate = delegate; - } - - @Override - public CloudSpace getTarget() { - return target; - } - - @Override - public void addDomain(String domainName) { - assertSpaceProvided("add domain"); - CloudDomain domain = findDomainByName(domainName); - if (domain == null) { - doCreateDomain(domainName); - } - } - - @Override - public void addRoute(String host, String domainName, String path) { - assertSpaceProvided("add route for domain"); - UUID domainGuid = getRequiredDomainGuid(domainName); - doAddRoute(domainGuid, host, path); - } - - @Override - public Optional bindServiceInstance(String bindingName, String applicationName, String serviceInstanceName) { - return bindServiceInstance(bindingName, applicationName, serviceInstanceName, null); - } - - @Override - public Optional bindServiceInstance(String bindingName, String applicationName, String serviceInstanceName, - Map parameters) { - UUID applicationGuid = getRequiredApplicationGuid(applicationName); - UUID serviceInstanceGuid = getRequiredServiceInstanceGuid(serviceInstanceName); - - var createBindingRequest = CreateServiceBindingRequest.builder() - .name(bindingName) - .type(ServiceBindingType.APPLICATION) - .relationships(ServiceBindingRelationships.builder() - .application(buildToOneRelationship( - applicationGuid)) - .serviceInstance( - buildToOneRelationship( - serviceInstanceGuid)) - .build()); - if (!CollectionUtils.isEmpty(parameters)) { - createBindingRequest.parameters(parameters); - } - return delegate.serviceBindingsV3() - .create(createBindingRequest.build()) - .map(CreateServiceBindingResponse::getJobId) - .block(); - } - - @Override - public void createApplication(ApplicationToCreateDto applicationToCreateDto) { - assertSpaceProvided("create application"); - CreateApplicationRequest createApplicationRequest = CreateApplicationRequest.builder() - .name(applicationToCreateDto.getName()) - .metadata(toOssMetadata(applicationToCreateDto.getMetadata())) - .lifecycle(buildApplicationLifecycle( - applicationToCreateDto.getStaging())) - .relationships(buildApplicationRelationships()) - .environmentVariables(applicationToCreateDto.getEnv()) - .build(); - CreateApplicationResponse createApplicationResponse = delegate.applicationsV3() - .create(createApplicationRequest) - .block(); - updateApplicationAttributes(applicationToCreateDto, createApplicationResponse.getId()); - } - - private Lifecycle buildApplicationLifecycle(Staging staging) { - // Prioritize Docker lifecycle if DockerInfo is provided - if (staging.getDockerInfo() != null) { - return createDockerLifecycle(); - } - - // Determine lifecycle type, defaulting to BUILDPACK if lifecycleType is null - LifecycleType lifecycleType = staging.getLifecycleType() != null ? LifecycleType.valueOf(staging.getLifecycleType() - .name()) : LifecycleType.BUILDPACK; - - return createLifecycleByType(staging, lifecycleType); - } - - private Lifecycle createLifecycleByType(Staging staging, LifecycleType lifecycleType) { - validateLifecycleConfiguration(staging, lifecycleType); - BuildpackData buildpackData = createBuildpackData(staging); - return Lifecycle.builder() - .type(lifecycleType) - .data(buildpackData) - .build(); - } - - private Lifecycle createDockerLifecycle() { - return Lifecycle.builder() - .type(LifecycleType.DOCKER) - .data(DockerData.builder() - .build()) - .build(); - } - - private void validateLifecycleConfiguration(Staging staging, LifecycleType lifecycleType) { - if (lifecycleType == LifecycleType.CNB && CollectionUtils.isEmpty(staging.getBuildpacks())) { - throw new IllegalArgumentException(Messages.BUILDPACKS_ARE_REQUIRED_FOR_CNB_LIFECYCLE_TYPE); - } - } - - private BuildpackData createBuildpackData(Staging staging) { - BuildpackData.Builder buildpackDataBuilder = BuildpackData.builder() - .stack(staging.getStackName()); - if (staging.getBuildpacks() != null) { - buildpackDataBuilder.addAllBuildpacks(staging.getBuildpacks()); - } - return buildpackDataBuilder.build(); - } - - private ApplicationRelationships buildApplicationRelationships() { - return ApplicationRelationships.builder() - .space(buildToOneRelationship(getTargetSpaceGuid())) - .build(); - } - - private void updateApplicationAttributes(ApplicationToCreateDto applicationToCreateDto, String applicationGuid) { - UUID createdApplicationGuid = UUID.fromString(applicationGuid); - updateApplicationProcess(createdApplicationGuid, applicationToCreateDto.getStaging()); - delegate.applicationsV3() - .scale(ScaleApplicationRequest.builder() - .applicationId(createdApplicationGuid.toString()) - .type("web") - .memoryInMb(applicationToCreateDto.getMemoryInMb()) - .diskInMb(applicationToCreateDto.getDiskQuotaInMb()) - .build()) - .block(); - if (!CollectionUtils.isEmpty(applicationToCreateDto.getRoutes())) { - addRoutes(applicationToCreateDto.getRoutes(), createdApplicationGuid); - } - } - - private void updateApplicationProcess(UUID applicationGuid, Staging staging) { - staging.getAppFeatures() - .entrySet() - .forEach(entry -> updateAppFeature(applicationGuid, entry.getKey(), entry.getValue())); - GetApplicationProcessResponse getApplicationProcessResponse = getApplicationProcessResource(applicationGuid); - UpdateProcessRequest.Builder updateProcessRequestBuilder = UpdateProcessRequest.builder() - .processId(getApplicationProcessResponse.getId()) - .command(staging.getCommand()); - if (staging.getHealthCheckType() != null) { - updateProcessRequestBuilder.healthCheck(buildHealthCheck(staging)); - } - if (staging.getReadinessHealthCheckType() != null) { - updateProcessRequestBuilder.readinessHealthCheck(buildReadinessHealthCheck(staging)); - } - delegate.processes() - .update(updateProcessRequestBuilder.build()) - .block(); - } - - private ReadinessHealthCheck buildReadinessHealthCheck(Staging staging) { - return ReadinessHealthCheck.builder() - .type(ReadinessHealthCheckType.from(staging.getReadinessHealthCheckType())) - .data(buildReadinessHealthCheckData(staging)) - .build(); - } - - private Data buildReadinessHealthCheckData(Staging staging) { - return Data.builder() - .invocationTimeout(staging.getReadinessHealthCheckInvocationTimeout()) - .endpoint(staging.getReadinessHealthCheckHttpEndpoint()) - .interval(staging.getReadinessHealthCheckInterval()) - .build(); - } - - private void updateAppFeature(UUID applicationGuid, String featureName, boolean enabled) { - delegate.applicationsV3() - .updateFeature(UpdateApplicationFeatureRequest.builder() - .featureName(featureName) - .enabled(enabled) - .applicationId(applicationGuid.toString()) - .build()) - .block(); - } - - private GetApplicationProcessResponse getApplicationProcessResource(UUID applicationGuid) { - return delegate.applicationsV3() - .getProcess(GetApplicationProcessRequest.builder() - .type("web") - .applicationId(applicationGuid.toString()) - .build()) - .block(); - } - - private HealthCheck buildHealthCheck(Staging staging) { - HealthCheckType healthCheckType = HealthCheckType.from(staging.getHealthCheckType()); - return HealthCheck.builder() - .type(healthCheckType) - .data(Data.builder() - .endpoint(staging.getHealthCheckHttpEndpoint()) - .timeout(staging.getHealthCheckTimeout()) - .invocationTimeout(staging.getInvocationTimeout()) - .interval(staging.getHealthCheckInterval()) - .build()) - .build(); - } - - private void addRoutes(Set routes, UUID applicationGuid) { - Map domains = getDomainsFromRoutes(routes); - for (CloudRoute route : routes) { - validateDomainForRoute(route, domains); - UUID domainGuid = domains.get(route.getDomain() - .getName()); - UUID routeGuid = getOrAddRoute(domainGuid, route.getHost(), route.getPath()); - bindRoute(routeGuid, applicationGuid, route.getRequestedProtocol()); - } - } - - @Override - public void createServiceInstance(CloudServiceInstance serviceInstance) { - assertSpaceProvided("create service instance"); - Assert.notNull(serviceInstance, "Service instance must not be null."); - CloudServicePlan servicePlan = findPlanForService(serviceInstance, serviceInstance.getPlan()); - UUID servicePlanGuid = servicePlan.getGuid(); - - delegate.serviceInstancesV3() - .create(CreateServiceInstanceRequest.builder() - .type(ServiceInstanceType.MANAGED) - .name(serviceInstance.getName()) - .metadata(toOssMetadata(serviceInstance.getV3Metadata())) - .relationships(ServiceInstanceRelationships.builder() - .servicePlan(buildToOneRelationship( - servicePlanGuid.toString())) - .space(buildToOneRelationship( - getTargetSpaceGuid().toString())) - .build()) - .tags(serviceInstance.getTags()) - .parameters(serviceInstance.getCredentials()) - .build()) - .block(); - } - - @Override - public String createServiceBroker(CloudServiceBroker serviceBroker) { - Assert.notNull(serviceBroker, "Service broker must not be null."); - - ServiceBrokerRelationships serviceBrokerRelationship = Optional.ofNullable(serviceBroker.getSpaceGuid()) - .map(UUID::fromString) - .map(this::buildToOneRelationship) - .map(spaceRelationship -> ServiceBrokerRelationships.builder() - .space( - spaceRelationship) - .build()) - .orElse(null); - - return delegate.serviceBrokersV3() - .create(CreateServiceBrokerRequest.builder() - .name(serviceBroker.getName()) - .url(serviceBroker.getUrl()) - .authentication(BasicAuthentication.builder() - .username(serviceBroker.getUsername()) - .password(serviceBroker.getPassword()) - .build()) - .relationships(serviceBrokerRelationship) - .build()) - .block(); - } - - @Override - public CloudServiceKey createAndFetchServiceKey(CloudServiceKey keyModel, String serviceInstanceName) { - CloudServiceInstance serviceInstance = getServiceInstance(serviceInstanceName); - doCreateServiceKeySync(keyModel.getName(), keyModel.getCredentials(), keyModel.getV3Metadata(), serviceInstance); - - return fetchWithAuxiliaryContent( - () -> getServiceKeyResourceByNameAndServiceInstanceGuid(keyModel.getName(), getGuid(serviceInstance)), - fetchedKey -> zipWithAuxiliaryServiceKeyContent(fetchedKey, serviceInstance)); - } - - @Override - public Optional createServiceKey(CloudServiceKey keyModel, String serviceInstanceName) { - CloudServiceInstance serviceInstance = getServiceInstance(serviceInstanceName); - return doCreateServiceKey(keyModel.getName(), keyModel.getCredentials(), keyModel.getV3Metadata(), serviceInstance); - } - - @Override - public Optional createServiceKey(String serviceInstanceName, String serviceKeyName, Map parameters) { - CloudServiceInstance serviceInstance = getServiceInstance(serviceInstanceName); - return doCreateServiceKey(serviceKeyName, parameters, null, serviceInstance); - } - - private void doCreateServiceKeySync(String name, Map parameters, Metadata metadata, - CloudServiceInstance serviceInstance) { - CreateServiceBindingRequest createServiceBindingRequest = buildServiceCredentialBindingRequest(name, parameters, metadata, - serviceInstance); - delegate.serviceBindingsV3() - .create(createServiceBindingRequest) - .map(response -> response.getJobId() - .get()) - .flatMap(jobId -> JobV3Util.waitForCompletion(delegate, BINDING_OPERATIONS_TIMEOUT, jobId)) - .block(); - } - - private Optional doCreateServiceKey(String name, Map parameters, Metadata metadata, - CloudServiceInstance serviceInstance) { - CreateServiceBindingRequest createServiceBindingRequest = buildServiceCredentialBindingRequest(name, parameters, metadata, - serviceInstance); - return delegate.serviceBindingsV3() - .create(createServiceBindingRequest) - .map(CreateServiceBindingResponse::getJobId) - .block(); - } - - private CreateServiceBindingRequest buildServiceCredentialBindingRequest(String name, Map parameters, Metadata metadata, - CloudServiceInstance serviceInstance) { - if (serviceInstance.getType() != org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType.MANAGED) { - throw new IllegalArgumentException( - String.format(Messages.CANT_CREATE_SERVICE_KEY_FOR_USER_PROVIDED_SERVICE, serviceInstance.getName())); - } - UUID serviceInstanceGuid = getGuid(serviceInstance); - - var createBindingRequest = CreateServiceBindingRequest.builder() - .type(ServiceBindingType.KEY) - .name(name) - .metadata(toOssMetadata(metadata)) - .relationships(ServiceBindingRelationships.builder() - .serviceInstance( - buildToOneRelationship( - serviceInstanceGuid)) - .build()); - if (!CollectionUtils.isEmpty(parameters)) { - createBindingRequest.parameters(parameters); - } - return createBindingRequest.build(); - } - - private static org.cloudfoundry.client.v3.Metadata toOssMetadata(Metadata metadata) { - if (metadata == null) { - return null; - } - return org.cloudfoundry.client.v3.Metadata.builder() - .labels(metadata.getLabels()) - .annotations(metadata.getAnnotations()) - .build(); - } - - @Override - public void createUserProvidedServiceInstance(CloudServiceInstance serviceInstance) { - assertSpaceProvided("create service instance"); - Assert.notNull(serviceInstance, "Service instance must not be null."); - String syslogDrainUrl = StringUtils.hasText(serviceInstance.getSyslogDrainUrl()) ? serviceInstance.getSyslogDrainUrl() : ""; - delegate.serviceInstancesV3() - .create(CreateServiceInstanceRequest.builder() - .name(serviceInstance.getName()) - .metadata(toOssMetadata(serviceInstance.getV3Metadata())) - .type(ServiceInstanceType.USER_PROVIDED) - .credentials(serviceInstance.getCredentials()) - .syslogDrainUrl(syslogDrainUrl) - .tags(serviceInstance.getTags()) - .relationships(ServiceInstanceRelationships.builder() - .space(buildToOneRelationship( - getTargetSpaceGuid().toString())) - .build()) - .build()) - .block(); - } - - @Override - public void deleteApplication(String applicationName) { - UUID applicationGuid = getRequiredApplicationGuid(applicationName); - deleteApplication(applicationGuid); - } - - private void deleteApplication(UUID applicationGuid) { - delegate.applicationsV3() - .delete(DeleteApplicationRequest.builder() - .applicationId(applicationGuid.toString()) - .build()) - .flatMap(jobId -> JobV3Util.waitForCompletion(delegate, DELETE_JOB_TIMEOUT, jobId)) - .block(); - } - - @Override - public void deleteDomain(String domainName) { - assertSpaceProvided("delete domain"); - CloudDomain domain = findDomainByName(domainName, true); - List routes = findRoutes(domain); - if (!routes.isEmpty()) { - throw new IllegalStateException("Unable to remove domain that is in use --" + " it has " + routes.size() + " routes."); - } - doDeleteDomain(getGuid(domain)); - } - - /** - * Delete routes that do not have any application which is assigned to them. - */ - @Override - public void deleteOrphanedRoutes() { - delegate.spacesV3() - .deleteUnmappedRoutes(DeleteUnmappedRoutesRequest.builder() - .spaceId(getTargetSpaceGuid().toString()) - .build()) - .flatMap(jobId -> JobV3Util.waitForCompletion(delegate, DELETE_JOB_TIMEOUT, jobId)) - .block(); - } - - @Override - public void deleteRoute(String host, String domainName, String path) { - assertSpaceProvided("delete route for domain"); - UUID routeGuid = getRouteGuid(getRequiredDomainGuid(domainName), host, path); - if (routeGuid == null) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", - "Host " + host + " not found for domain " + domainName + "."); - } - doDeleteRoute(routeGuid); - } - - @Override - public void deleteServiceInstance(String serviceInstanceName) { - CloudServiceInstance serviceInstance = getServiceInstanceWithoutAuxiliaryContent(serviceInstanceName); - doDeleteServiceInstance(serviceInstance.getGuid()); - } - - @Override - public void deleteServiceInstance(CloudServiceInstance serviceInstance) { - doDeleteServiceInstance(serviceInstance.getGuid()); - } - - @Override - public String deleteServiceBroker(String name) { - CloudServiceBroker broker = getServiceBroker(name); - UUID guid = broker.getMetadata() - .getGuid(); - return delegate.serviceBrokersV3() - .delete(DeleteServiceBrokerRequest.builder() - .serviceBrokerId(guid.toString()) - .build()) - .block(); - } - - @Override - public Optional deleteServiceBinding(String serviceInstanceName, String serviceKeyName) { - CloudServiceKey serviceKey = getServiceKey(serviceInstanceName, serviceKeyName); - if (serviceKey == null) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service key " + serviceKeyName + " not found."); - } - return doDeleteServiceBindings(List.of(serviceKey.getGuid())).stream() - .findFirst(); - } - - @Override - public CloudApplication getApplication(String applicationName) { - return getApplication(applicationName, true); - } - - @Override - public CloudApplication getApplication(String applicationName, boolean required) { - return findApplicationByName(applicationName, required); - } - - @Override - public UUID getApplicationGuid(String applicationName) { - return getRequiredApplicationGuid(applicationName); - } - - @Override - public String getApplicationName(UUID applicationGuid) { - // This will throw a CloudOperationException with a 404 if no app with given GUID is present - return getApplicationByGuid(applicationGuid).block() - .getName(); - } - - @Override - public Map getApplicationEnvironment(UUID applicationGuid) { - return delegate.applicationsV3() - .getEnvironmentVariables(GetApplicationEnvironmentVariablesRequest.builder() - .applicationId(applicationGuid.toString()) - .build()) - .map(GetApplicationEnvironmentVariablesResponse::getVars) - .block(); - } - - @Override - public Map getApplicationEnvironment(String applicationName) { - UUID applicationGuid = getRequiredApplicationGuid(applicationName); - return getApplicationEnvironment(applicationGuid); - } - - @Override - public List getApplicationEvents(String applicationName) { - UUID applicationGuid = getRequiredApplicationGuid(applicationName); - return getEventsByTarget(applicationGuid); - } - - @Override - public List getEventsByTarget(UUID uuid) { - return findEventsByTarget(uuid.toString()); - } - - @Override - public InstancesInfo getApplicationInstances(CloudApplication application) { - if (application.getState() - .equals(CloudApplication.State.STARTED)) { - return findApplicationInstances(getGuid(application)); - } - return ImmutableInstancesInfo.builder() - .instances(Collections.emptyList()) - .build(); - } - - @Override - public InstancesInfo getApplicationInstances(UUID applicationGuid) { - return findApplicationInstances(applicationGuid); - } - - @Override - public CloudProcess getApplicationProcess(UUID applicationGuid) { - return fetch(() -> Mono.just(getApplicationProcessResource(applicationGuid)), ImmutableRawCloudProcess::of); - } - - @Override - public List getApplicationRoutes(UUID applicationGuid) { - return fetchList(() -> getRouteResourcesByAppGuid(applicationGuid), routeResource -> ImmutableRawCloudRoute.builder() - .route(routeResource) - .applicationGuid( - applicationGuid) - .build()); - } - - @Override - public boolean getApplicationSshEnabled(UUID applicationGuid) { - return delegate.applicationsV3() - .getSshEnabled(GetApplicationSshEnabledRequest.builder() - .applicationId(applicationGuid.toString()) - .build()) - .map(GetApplicationSshEnabledResponse::getEnabled) - .defaultIfEmpty(false) - .block(); - } - - @Override - public Map getApplicationFeatures(UUID applicationGuid) { - IntFunction pageRequestSupplier = page -> ListApplicationFeaturesRequest.builder() - .applicationId( - applicationGuid.toString()) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.applicationsV3() - .listFeatures(pageRequestSupplier.apply(page))) - .collect(Collectors.toMap(ApplicationFeature::getName, ApplicationFeature::getEnabled)) - .block(); - } - - @Override - public List getApplications() { - return fetchList(this::getApplicationResources, application -> ImmutableRawCloudApplication.builder() - .application(application) - .space(target) - .build()); - } - - @Override - public List getApplicationsByMetadataLabelSelector(String labelSelector) { - assertSpaceProvided("get applications"); - return fetchList(() -> getApplicationsByLabelSelector(labelSelector), application -> ImmutableRawCloudApplication.builder() - .application( - application) - .space(target) - .build()); - } - - private Flux getApplicationsByLabelSelector(String labelSelector) { - IntFunction pageRequestSupplier = page -> ListApplicationsRequest.builder() - .spaceId(getTargetSpaceGuid().toString()) - .labelSelector(labelSelector) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.applicationsV3() - .list(pageRequestSupplier.apply(page))); - } - - @Override - public CloudDomain getDefaultDomain() { - return fetch(() -> getDefaultDomainResource(getTargetOrganizationGuid().toString()), ImmutableRawCloudDomain::of); - } - - private Mono getDefaultDomainResource(String guid) { - return delegate.organizationsV3() - .getDefaultDomain(GetOrganizationDefaultDomainRequest.builder() - .organizationId(guid) - .build()); - } - - @Override - public List getSharedDomains() { - return fetchList(this::getSharedDomainResources, ImmutableRawCloudDomain::of); - } - - @Override - public List getDomains() { - return fetchList(this::getDomainResources, ImmutableRawCloudDomain::of); - } - - @Override - public List getDomainsForOrganization() { - assertSpaceProvided("access organization domains"); - return findDomainsByOrganizationGuid(getTargetOrganizationGuid()); - } - - @Override - public List getPrivateDomains() { - return fetchList(this::getPrivateDomainResources, ImmutableRawCloudDomain::of); - } - - @Override - public List getEvents() { - return fetchList(this::getEventResources, ImmutableRawCloudEvent::of); - } - - @Override - public List getRoutes(String domainName) { - assertSpaceProvided("get routes for domain"); - CloudDomain domain = findDomainByName(domainName, true); - return findRoutes(domain); - } - - @Override - public UUID getRequiredServiceInstanceGuid(String name) { - Resource serviceInstanceResource = getServiceInstanceByName(name).block(); - if (serviceInstanceResource == null) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service instance " + name + " not found."); - } - return UUID.fromString(serviceInstanceResource.getId()); - } - - @Override - public CloudServiceInstance getServiceInstance(String serviceInstanceName) { - return getServiceInstance(serviceInstanceName, true); - } - - @Override - public CloudServiceInstance getServiceInstance(String serviceInstanceName, boolean required) { - CloudServiceInstance serviceInstance = findServiceInstanceByName(serviceInstanceName); - return getServiceInstanceIfRequired(serviceInstanceName, serviceInstance, required); - } - - private CloudServiceInstance getServiceInstanceIfRequired(String serviceInstanceName, CloudServiceInstance serviceInstance, - boolean required) { - if (serviceInstance == null && required) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service instance " + serviceInstanceName + " not found."); - } - return serviceInstance; - } - - @Override - public CloudServiceBinding getServiceBinding(UUID serviceBindingGuid) { - // TODO: Refactor and use serviceBindingsV3.get() when https://github.com/cloudfoundry/cf-java-client/pull/1171 is merged and - // released - return fetch(() -> getServiceBindingResource(serviceBindingGuid), ImmutableRawCloudServiceBinding::of); - } - - private Mono getServiceBindingResource(UUID serviceBindingGuid) { - IntFunction pageRequestSupplier = page -> ListServiceBindingsRequest.builder() - .page(page) - .id(serviceBindingGuid.toString()) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.serviceBindingsV3() - .list(pageRequestSupplier.apply(page))) - .singleOrEmpty(); - } - - @Override - public List getServiceAppBindings(UUID serviceInstanceGuid) { - return fetchList(() -> getServiceBindingResourcesByServiceInstanceGuid(serviceInstanceGuid), ImmutableRawCloudServiceBinding::of); - } - - @Override - public List getAppBindings(UUID applicationGuid) { - return fetchList(() -> getServiceBindingResourcesByApplicationGuid(applicationGuid), ImmutableRawCloudServiceBinding::of); - } - - @Override - public List getServiceBindingsForApplication(UUID applicationId, UUID serviceInstanceGuid) { - return fetchList(() -> getServiceBindingResourcesByApplicationGuidAndServiceInstanceGuid(applicationId, serviceInstanceGuid), - ImmutableRawCloudServiceBinding::of); - } - - @Override - public CloudServiceBroker getServiceBroker(String name) { - return getServiceBroker(name, true); - } - - @Override - public CloudServiceBroker getServiceBroker(String name, boolean required) { - CloudServiceBroker serviceBroker = findServiceBrokerByName(name); - if (serviceBroker == null && required) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service broker " + name + " not found."); - } - return serviceBroker; - } - - @Override - public List getServiceBrokers() { - return fetchList(this::getServiceBrokerResources, ImmutableRawCloudServiceBroker::of); - } - - @Override - public CloudServiceKey getServiceKey(String serviceInstanceName, String serviceKeyName) { - CloudServiceInstance serviceInstance = getServiceInstance(serviceInstanceName); - return fetchWithAuxiliaryContent(() -> getServiceKeyResourceByNameAndServiceInstanceGuid(serviceKeyName, serviceInstance.getGuid()), - serviceKey -> zipWithAuxiliaryServiceKeyContent(serviceKey, serviceInstance)); - } - - @Override - public List getServiceKeys(String serviceInstanceName) { - CloudServiceInstance serviceInstance = getServiceInstance(serviceInstanceName); - return getServiceKeys(serviceInstance); - } - - @Override - public List getServiceKeysWithCredentials(String serviceInstanceName) { - CloudServiceInstance serviceInstance = getServiceInstance(serviceInstanceName); - return getServiceKeysWithCredentials(serviceInstance); - } - - @Override - public List getServiceKeys(CloudServiceInstance serviceInstance) { - return fetchList(() -> getServiceKeyResource(serviceInstance), serviceKey -> ImmutableRawCloudServiceKey.builder() - .serviceInstance( - serviceInstance) - .serviceBindingResource( - serviceKey) - .build()); - } - - @Override - public List getServiceKeysWithCredentials(CloudServiceInstance serviceInstance) { - return fetchListWithAuxiliaryContent(() -> getServiceKeyResource(serviceInstance), - serviceKey -> zipWithAuxiliaryServiceKeyContent(serviceKey, serviceInstance)); - } - - private Mono> zipWithAuxiliaryServiceKeyContent(ServiceBindingResource key, - CloudServiceInstance serviceInstance) { - return getServiceKeyCredentials(key.getId()).map(credentials -> ImmutableRawCloudServiceKey.builder() - .serviceBindingResource(key) - .credentials(credentials) - .serviceInstance(serviceInstance) - .build()); - } - - private Mono> getServiceKeyCredentials(String keyGuid) { - return delegate.serviceBindingsV3() - .getDetails(GetServiceBindingDetailsRequest.builder() - .serviceBindingId(keyGuid) - .build()) - // CF V3 API returns 404 when fetching credentials of a service key which creation failed - .onErrorResume(t -> doesErrorMatchStatusCode(t, HttpStatus.NOT_FOUND), t -> Mono.just( - GetServiceBindingDetailsResponse.builder() - .volumeMounts(Collections.emptyList()) - .credentials(Collections.emptyMap()) - .build())) - .map(GetServiceBindingDetailsResponse::getCredentials); - } - - @Override - public Map getServiceInstanceParameters(UUID guid) { - return delegate.serviceInstancesV3() - .getManagedServiceParameters(GetManagedServiceParametersRequest.builder() - .serviceInstanceId(guid.toString()) - .build()) - .map(GetManagedServiceParametersResponse::getParameters) - .block(); - } - - @Override - public Map getUserProvidedServiceInstanceParameters(UUID guid) { - return delegate.serviceInstancesV3() - .getUserProvidedCredentials(GetUserProvidedCredentialsRequest.builder() - .serviceInstanceId(guid.toString()) - .build()) - .map(GetUserProvidedCredentialsResponse::getCredentials) - .block(); - } - - @Override - public Map getServiceBindingParameters(UUID guid) { - return delegate.serviceBindingsV3() - .getParameters(GetServiceBindingParametersRequest.builder() - .serviceBindingId(guid.toString()) - .build()) - .map(GetServiceBindingParametersResponse::getParameters) - .block(); - } - - @Override - public List getServiceOfferings() { - return fetchListWithAuxiliaryContent(this::getServiceResources, this::zipWithAuxiliaryServiceOfferingContent); - } - - @Override - public void updateServicePlan(String serviceName, String planName) { - CloudServiceInstance service = getServiceInstance(serviceName); - if (service.isUserProvided()) { - return; - } - CloudServicePlan plan = findPlanForService(service, planName); - delegate.serviceInstancesV3() - .update(UpdateServiceInstanceRequest.builder() - .serviceInstanceId(service.getGuid() - .toString()) - .relationships(ServiceInstanceRelationships.builder() - .servicePlan( - buildToOneRelationship(plan.getGuid())) - .build()) - .build()) - .block(); - } - - @Override - public String getServiceInstanceName(UUID serviceInstanceGuid) { - return getServiceInstanceByGuid(serviceInstanceGuid).block() - .getName(); - } - - @Override - public CloudServiceInstance getServiceInstanceWithoutAuxiliaryContent(String serviceInstanceName) { - return getServiceInstanceWithoutAuxiliaryContent(serviceInstanceName, true); - } - - @Override - public CloudServiceInstance getServiceInstanceWithoutAuxiliaryContent(String serviceInstanceName, boolean required) { - CloudServiceInstance serviceInstance = fetch(() -> getServiceInstanceResourceByName(serviceInstanceName), - ImmutableRawCloudServiceInstance::of); - return getServiceInstanceIfRequired(serviceInstanceName, serviceInstance, required); - } - - @Override - public void updateServiceParameters(String serviceName, Map parameters) { - CloudServiceInstance service = getServiceInstanceWithoutAuxiliaryContent(serviceName); - var updateServiceInstanceRequest = UpdateServiceInstanceRequest.builder() - .serviceInstanceId(service.getGuid() - .toString()); - if (service.isUserProvided()) { - updateServiceInstanceRequest.credentials(parameters); - } else { - updateServiceInstanceRequest.parameters(parameters); - } - delegate.serviceInstancesV3() - .update(updateServiceInstanceRequest.build()) - .block(); - } - - @Override - public void updateServiceTags(String serviceName, List tags) { - UUID serviceInstanceGuid = getRequiredServiceInstanceGuid(serviceName); - delegate.serviceInstancesV3() - .update(UpdateServiceInstanceRequest.builder() - .serviceInstanceId(serviceInstanceGuid.toString()) - .tags(tags) - .build()) - .block(); - } - - @Override - public void updateServiceSyslogDrainUrl(String serviceName, String syslogDrainUrl) { - CloudServiceInstance service = getServiceInstanceWithoutAuxiliaryContent(serviceName); - if (!service.isUserProvided()) { - return; - } - String updatedSyslogDrain = StringUtils.hasText(syslogDrainUrl) ? syslogDrainUrl : ""; - delegate.serviceInstancesV3() - .update(UpdateServiceInstanceRequest.builder() - .serviceInstanceId(service.getGuid() - .toString()) - .syslogDrainUrl(updatedSyslogDrain) - .build()) - .block(); - } - - private List> toBatches(Collection largeList, int maxCharLength) { - if (largeList.isEmpty()) { - return Collections.emptyList(); - } - - List> batches = new ArrayList<>(); - int currentBatchLength = 0, currentBatchIndex = 0; - batches.add(new ArrayList<>()); - - for (T element : largeList) { - int elementLength = element.toString() - .length(); - if (elementLength + currentBatchLength >= maxCharLength) { - batches.add(new ArrayList<>()); - currentBatchIndex++; - currentBatchLength = 0; - } - batches.get(currentBatchIndex) - .add(element); - currentBatchLength += elementLength; - } - return batches; - } - - private Flux getServiceInstancesByNames(List serviceInstanceNames) { - String spaceGuid = getTargetSpaceGuid().toString(); - IntFunction pageRequestSupplier = page -> ListServiceInstancesRequest.builder() - .spaceId(spaceGuid) - .addAllServiceInstanceNames( - serviceInstanceNames) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.serviceInstancesV3() - .list(pageRequestSupplier.apply(page))); - } - - @Override - public List getServiceInstancesWithoutAuxiliaryContentByNames(List names) { - List allServiceInstances = new ArrayList<>(); - for (List batchOfServiceInstanceNames : toBatches(names, MAX_CHAR_LENGTH_FOR_PARAMS_IN_REQUEST)) { - List serviceInstances = fetchList(() -> getServiceInstancesByNames(batchOfServiceInstanceNames), - ImmutableRawV3CloudServiceInstance::of); - allServiceInstances.addAll(serviceInstances); - } - return allServiceInstances; - } - - @Override - public List getServiceInstancesByMetadataLabelSelector(String labelSelector) { - IntFunction pageRequestSupplier = page -> ListServiceInstancesRequest.builder() - .labelSelector(labelSelector) - .spaceId( - getTargetSpaceGuid().toString()) - .page(page) - .build(); - - return fetchListWithAuxiliaryContent(() -> getServiceInstanceResources(pageRequestSupplier), - this::zipWithAuxiliaryServiceInstanceContent); - } - - @Override - public List getServiceInstancesWithoutAuxiliaryContentByMetadataLabelSelector(String labelSelector) { - IntFunction pageRequestSupplier = page -> ListServiceInstancesRequest.builder() - .labelSelector(labelSelector) - .spaceId( - getTargetSpaceGuid().toString()) - .page(page) - .build(); - - return fetchList(() -> getServiceInstanceResources(pageRequestSupplier), ImmutableRawCloudServiceInstance::of); - } - - @Override - public CloudStack getStack(String name) { - return getStack(name, true); - } - - @Override - public CloudStack getStack(String name, boolean required) { - CloudStack stack = findStackResource(name); - if (stack == null && required) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Stack " + name + " not found."); - } - return stack; - } - - @Override - public List getStacks() { - return fetchList(this::getStackResources, ImmutableRawCloudStack::of); - } - - @Override - public void rename(String applicationName, String newName) { - UUID applicationGuid = getRequiredApplicationGuid(applicationName); - delegate.applicationsV3() - .update(UpdateApplicationRequest.builder() - .applicationId(applicationGuid.toString()) - .name(newName) - .build()) - .onErrorResume(t -> doesErrorMatchStatusCode(t, HttpStatus.SERVICE_UNAVAILABLE), - exception -> fallbackApplicationRename(newName, applicationGuid, exception)) - .block(); - } - - private Mono fallbackApplicationRename(String newName, UUID applicationGuid, Throwable exception) { - String existingApplicationName = getApplicationByGuid(applicationGuid).block() - .getName(); - if (newName.equals(existingApplicationName)) { - return Mono.empty(); - } - return Mono.error(exception); - } - - @Override - public void restartApplication(String applicationName) { - stopApplication(applicationName); - startApplication(applicationName); - } - - @Override - public void startApplication(String applicationName) { - Application application = getApplicationByName(applicationName).block(); - if (application.getState() == ApplicationState.STARTED) { - return; - } - UUID applicationGuid = UUID.fromString(application.getId()); - delegate.applicationsV3() - .start(StartApplicationRequest.builder() - .applicationId(applicationGuid.toString()) - .build()) - .block(); - } - - @Override - public void stopApplication(String applicationName) { - Application application = getApplicationByName(applicationName).block(); - if (application.getState() == ApplicationState.STOPPED) { - return; - } - UUID applicationGuid = UUID.fromString(application.getId()); - delegate.applicationsV3() - .stop(StopApplicationRequest.builder() - .applicationId(applicationGuid.toString()) - .build()) - .block(); - } - - @Override - public List unbindServiceInstance(String applicationName, String serviceInstanceName) { - UUID applicationGuid = getRequiredApplicationGuid(applicationName); - UUID serviceInstanceGuid = getRequiredServiceInstanceGuid(serviceInstanceName); - - return doUnbindServiceInstance(applicationGuid, serviceInstanceGuid); - } - - @Override - public Optional deleteServiceBinding(UUID serviceBindingGuid) { - return doDeleteServiceBindings(List.of(serviceBindingGuid)).stream() - .findFirst(); - } - - @Override - public List unbindServiceInstance(UUID applicationGuid, UUID serviceInstanceGuid) { - return doUnbindServiceInstance(applicationGuid, serviceInstanceGuid); - } - - @Override - public void updateApplicationDiskQuota(String applicationName, int diskQuota) { - UUID applicationGuid = getRequiredApplicationGuid(applicationName); - delegate.applicationsV3() - .scale(ScaleApplicationRequest.builder() - .applicationId(applicationGuid.toString()) - .type("web") - .diskInMb(diskQuota) - .build()) - .block(); - } - - @Override - public void updateApplicationEnv(String applicationName, Map env) { - UUID applicationGuid = getRequiredApplicationGuid(applicationName); - delegate.applicationsV3() - .updateEnvironmentVariables(UpdateApplicationEnvironmentVariablesRequest.builder() - .applicationId(applicationGuid.toString()) - .vars(env) - .build()) - .block(); - } - - @Override - public void updateApplicationInstances(String applicationName, int instances) { - UUID applicationGuid = getRequiredApplicationGuid(applicationName); - delegate.applicationsV3() - .scale(ScaleApplicationRequest.builder() - .applicationId(applicationGuid.toString()) - .type("web") - .instances(instances) - .build()) - .block(); - } - - @Override - public void updateApplicationMemory(String applicationName, int memory) { - UUID applicationGuid = getRequiredApplicationGuid(applicationName); - delegate.applicationsV3() - .scale(ScaleApplicationRequest.builder() - .applicationId(applicationGuid.toString()) - .type("web") - .memoryInMb(memory) - .build()) - .block(); - } - - @Override - public void updateApplicationMetadata(UUID guid, Metadata metadata) { - delegate.applicationsV3() - .update(org.cloudfoundry.client.v3.applications.UpdateApplicationRequest.builder() - .applicationId(guid.toString()) - .metadata(toOssMetadata(metadata)) - .build()) - .block(); - } - - @Override - public void updateApplicationStaging(String applicationName, Staging staging) { - UUID applicationGuid = getRequiredApplicationGuid(applicationName); - delegate.applicationsV3() - .update(UpdateApplicationRequest.builder() - .applicationId(applicationGuid.toString()) - .lifecycle(buildApplicationLifecycle(staging)) - .build()) - .block(); - updateApplicationProcess(applicationGuid, staging); - } - - @Override - public void updateApplicationRoutes(String applicationName, Set updatedRoutes) { - UUID applicationGuid = getApplicationGuid(applicationName); - List appRoutes = fetchList(() -> getRouteResourcesByAppGuid(applicationGuid), ImmutableRawCloudRoute::of); - Set outdatedRoutes = getOutdatedRoutes(applicationGuid, appRoutes, updatedRoutes); - Set newRoutes = getNewRoutes(applicationGuid, appRoutes, updatedRoutes); - removeRoutes(outdatedRoutes, applicationGuid); - addRoutes(newRoutes, applicationGuid); - } - - private Set getOutdatedRoutes(UUID applicationGuid, List currentRoutes, Set updatedRoutes) { - return currentRoutes.stream() - .filter(currentRoute -> isRouteOutdated(applicationGuid, currentRoute, updatedRoutes)) - .collect(Collectors.toSet()); - } - - private boolean isRouteOutdated(UUID applicationGuid, CloudRoute currentRoute, Set updatedRoutes) { - Optional updatedRoute = findRoute(currentRoute.getUrl(), updatedRoutes); - if (updatedRoute.isEmpty()) { - return true; - } - return isProtocolChanged(applicationGuid, currentRoute, updatedRoute.get()); - } - - private Optional findRoute(String url, Collection routes) { - return routes.stream() - .filter(route -> Objects.equals(url, route.getUrl())) - .findFirst(); - } - - private boolean isProtocolChanged(UUID applicationGuid, CloudRoute currentRoute, CloudRoute updatedRoute) { - if (updatedRoute.getRequestedProtocol() == null) { - return false; - } - return currentRoute.getDestinations() - .stream() - .filter(routeDestination -> Objects.equals(routeDestination.getApplicationGuid(), applicationGuid)) - .noneMatch( - routeDestination -> Objects.equals(routeDestination.getProtocol(), updatedRoute.getRequestedProtocol())); - } - - private Set getNewRoutes(UUID applicationGuid, List currentRoutes, Set updatedRoutes) { - return updatedRoutes.stream() - .filter(updatedRoute -> isRouteUpdated(applicationGuid, updatedRoute, currentRoutes)) - .collect(Collectors.toSet()); - } - - private boolean isRouteUpdated(UUID applicationGuid, CloudRoute updatedRoute, List currentRoutes) { - Optional currentRoute = findRoute(updatedRoute.getUrl(), currentRoutes); - if (currentRoute.isEmpty()) { - return true; - } - return isProtocolChanged(applicationGuid, currentRoute.get(), updatedRoute); - } - - @Override - public String updateServiceBroker(CloudServiceBroker serviceBroker) { - Assert.notNull(serviceBroker, "Service broker must not be null."); - - CloudServiceBroker existingBroker = getServiceBroker(serviceBroker.getName()); - UUID brokerGuid = existingBroker.getMetadata() - .getGuid(); - - return delegate.serviceBrokersV3() - .update(UpdateServiceBrokerRequest.builder() - .serviceBrokerId(brokerGuid.toString()) - .name(serviceBroker.getName()) - .authentication(BasicAuthentication.builder() - .username(serviceBroker.getUsername()) - .password(serviceBroker.getPassword()) - .build()) - .url(serviceBroker.getUrl()) - .build()) - .flatMap(response -> Mono.justOrEmpty(response.jobId())) - .block(); - } - - @Override - public void updateServiceInstanceMetadata(UUID guid, Metadata metadata) { - delegate.serviceInstancesV3() - .update(UpdateServiceInstanceRequest.builder() - .serviceInstanceId(guid.toString()) - .metadata(toOssMetadata(metadata)) - .build()) - .block(); - } - - @Override - public void updateServiceBindingMetadata(UUID guid, Metadata metadata) { - delegate.serviceBindingsV3() - .update(UpdateServiceBindingRequest.builder() - .serviceBindingId(guid.toString()) - .metadata(toOssMetadata(metadata)) - .build()) - .block(); - } - - @Override - public void updateServicePlanVisibilityForBroker(String name, ServicePlanVisibility visibility) { - CloudServiceBroker broker = getServiceBroker(name); - List servicePlans = findServicePlansByBrokerGuid(getGuid(broker)); - for (CloudServicePlan servicePlan : servicePlans) { - updateServicePlanVisibility(servicePlan, visibility); - } - } - - @Override - public CloudTask getTask(UUID taskGuid) { - return fetch(() -> getTaskResource(taskGuid), ImmutableRawCloudTask::of); - } - - @Override - public List getTasks(String applicationName) { - UUID applicationGuid = getRequiredApplicationGuid(applicationName); - return fetchList(() -> getTaskResourcesByApplicationGuid(applicationGuid), ImmutableRawCloudTask::of); - } - - @Override - public CloudTask runTask(String applicationName, CloudTask task) { - UUID applicationGuid = getRequiredApplicationGuid(applicationName); - return createTask(applicationGuid, task); - } - - @Override - public CloudTask cancelTask(UUID taskGuid) { - return fetch(() -> cancelTaskResource(taskGuid), ImmutableRawCloudTask::of); - } - - @Override - public CloudPackage asyncUploadApplication(String applicationName, Path file, UploadStatusCallback callback, Duration uploadTimeout) { - CloudPackage cloudPackage = startUpload(applicationName, file, uploadTimeout); - processAsyncUploadInBackground(cloudPackage, callback); - return cloudPackage; - } - - @Override - public Upload getUploadStatus(UUID packageGuid) { - CloudPackage cloudPackage = getPackage(packageGuid); - ErrorDetails errorDetails = null; - if (cloudPackage.getType() == CloudPackage.Type.BITS) { - errorDetails = ImmutableErrorDetails.builder() - .description(((BitsData) cloudPackage.getData()).getError()) - .build(); - } - return ImmutableUpload.builder() - .status(cloudPackage.getStatus()) - .errorDetails(errorDetails) - .build(); - } - - @Override - public CloudBuild getBuild(UUID buildGuid) { - return fetch(() -> getBuildResource(buildGuid), ImmutableRawCloudBuild::of); - } - - @Override - public List getBuildsForApplication(UUID applicationGuid) { - return fetchList(() -> getBuildResourcesByApplicationGuid(applicationGuid), ImmutableRawCloudBuild::of); - } - - @Override - public List getBuildsForPackage(UUID packageGuid) { - return fetchList(() -> getBuildResourcesByPackageGuid(packageGuid), ImmutableRawCloudBuild::of); - } - - @Override - public CloudBuild createBuild(UUID packageGuid) { - return fetch(() -> createBuildResource(packageGuid), ImmutableRawCloudBuild::of); - } - - @Override - public void bindDropletToApp(UUID dropletGuid, UUID applicationGuid) { - delegate.applicationsV3() - .setCurrentDroplet(SetApplicationCurrentDropletRequest.builder() - .applicationId(applicationGuid.toString()) - .data(Relationship.builder() - .id(dropletGuid.toString()) - .build()) - .build()) - .block(); - } - - @Override - public DropletInfo getCurrentDropletForApplication(UUID applicationGuid) { - GetApplicationCurrentDropletResponse dropletResponse = delegate.applicationsV3() - .getCurrentDroplet(GetApplicationCurrentDropletRequest.builder() - .applicationId( - applicationGuid.toString()) - .build()) - .block(); - if (dropletResponse == null) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not found", - "Application with guid " + applicationGuid + " does not have a droplet"); - } - return parseDropletInfo(dropletResponse); - } - - private DropletInfo parseDropletInfo(GetApplicationCurrentDropletResponse getApplicationCurrentDropletResponse) { - String packageUrl = getApplicationCurrentDropletResponse.getLinks() - .get(Constants.PACKAGE) - .getHref(); - if (packageUrl.endsWith("/")) { - packageUrl = packageUrl.substring(0, packageUrl.lastIndexOf("/")); - } - String packageGuid = packageUrl.substring(packageUrl.lastIndexOf("/") + 1); - return ImmutableDropletInfo.builder() - .guid(UUID.fromString(getApplicationCurrentDropletResponse.getId())) - .packageGuid(UUID.fromString(packageGuid)) - .build(); - } - - @Override - public List getPackagesForApplication(UUID applicationGuid) { - return fetchList(() -> getPackages(applicationGuid.toString()), ImmutableRawCloudPackage::of); - } - - private Flux getPackages(String applicationGuid) { - IntFunction pageRequestSupplier = page -> ListApplicationPackagesRequest.builder() - .page(page) - .applicationId( - applicationGuid) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.applicationsV3() - .listPackages(pageRequestSupplier.apply(page))); - } - - @Override - public Set getUserRolesBySpaceAndUser(UUID spaceGuid, UUID userGuid) { - Supplier> setFactory = () -> EnumSet.noneOf(UserRole.class); - return fetchFlux(() -> getRoles(spaceGuid, userGuid), ImmutableRawUserRole::of).collect(Collectors.toCollection(setFactory)) - .block(); - } - - public CloudPackage createDockerPackage(UUID applicationGuid, DockerInfo dockerInfo) { - org.cloudfoundry.client.v3.packages.DockerData.Builder dockerDataBuilder = org.cloudfoundry.client.v3.packages.DockerData.builder() - .image( - dockerInfo.getImage()); - if (dockerInfo.getCredentials() != null) { - addNonNullDockerCredentials(dockerInfo.getCredentials(), dockerDataBuilder); - } - CreatePackageRequest packageRequest = CreatePackageRequest.builder() - .type(PackageType.DOCKER) - .data(dockerDataBuilder.build()) - .relationships(buildPackageRelationships(applicationGuid)) - .build(); - CreatePackageResponse packageResponse = delegate.packages() - .create(packageRequest) - .block(); - return getPackage(UUID.fromString(packageResponse.getId())); - } - - @Override - public CloudAsyncJob getAsyncJob(String jobId) { - return fetch(() -> delegate.jobsV3() - .get(GetJobRequest.builder() - .jobId(jobId) - .build()), ImmutableRawCloudAsyncJob::of); - } - - private void addNonNullDockerCredentials(DockerCredentials dockerCredentials, - org.cloudfoundry.client.v3.packages.DockerData.Builder dockerDataBuilder) { - String username = dockerCredentials.getUsername(); - if (username != null) { - dockerDataBuilder.username(username); - } - String password = dockerCredentials.getPassword(); - if (password != null) { - dockerDataBuilder.password(password); - } - } - - private Flux getRoles(UUID spaceGuid, UUID userGuid) { - IntFunction pageRequestSupplier = page -> ListRolesRequest.builder() - .page(page) - .spaceId(spaceGuid.toString()) - .userId(userGuid.toString()) - .types(RoleType.values()) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.rolesV3() - .list(pageRequestSupplier.apply(page))); - } - - private CloudApplication findApplicationByName(String name, boolean required) { - CloudApplication application = findApplicationByName(name); - if (application == null && required) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + name + " not found."); - } - return application; - } - - private CloudApplication findApplicationByName(String name) { - return fetch(() -> getApplicationByName(name), application -> ImmutableRawCloudApplication.builder() - .application(application) - .space(target) - .build()); - } - - private Flux getApplicationResources() { - assertSpaceProvided("get application"); - IntFunction pageRequestSupplier = page -> ListApplicationsRequest.builder() - .spaceId(getTargetSpaceGuid().toString()) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.applicationsV3() - .list(pageRequestSupplier.apply(page))); - } - - private Mono getApplicationByGuid(UUID guid) { - return delegate.applicationsV3() - .get(GetApplicationRequest.builder() - .applicationId(guid.toString()) - .build()); - } - - private Mono getApplicationByName(String name) { - assertSpaceProvided("get application"); - IntFunction pageRequestSupplier = page -> ListApplicationsRequest.builder() - .spaceId(getTargetSpaceGuid().toString()) - .name(name) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.applicationsV3() - .list(pageRequestSupplier.apply(page))) - .singleOrEmpty(); - } - - private CloudServiceInstance findServiceInstanceByName(String name) { - return fetchWithAuxiliaryContent(() -> getServiceInstanceResourceByName(name), this::zipWithAuxiliaryServiceInstanceContent); - } - - private Mono getServiceInstanceByGuid(UUID serviceInstanceGuid) { - return delegate.serviceInstancesV3() - .get(GetServiceInstanceRequest.builder() - .serviceInstanceId(serviceInstanceGuid.toString()) - .build()); - } - - private Mono getServiceInstanceResourceByName(String name) { - IntFunction pageRequestSupplier = page -> ListServiceInstancesRequest.builder() - .spaceId( - getTargetSpaceGuid().toString()) - .serviceInstanceName(name) - .page(page) - .build(); - return getServiceInstanceResources(pageRequestSupplier).singleOrEmpty(); - } - - private Flux getServiceInstanceResources(IntFunction pageRequestSupplier) { - return PaginationUtils.requestClientV3Resources(page -> delegate.serviceInstancesV3() - .list(pageRequestSupplier.apply(page))); - } - - private Mono> zipWithAuxiliaryServiceInstanceContent(ServiceInstanceResource serviceInstanceResource) { - if (isUserProvided(serviceInstanceResource)) { - return Mono.just(ImmutableRawCloudServiceInstance.of(serviceInstanceResource)); - } - String servicePlanGuid = serviceInstanceResource.getRelationships() - .getServicePlan() - .getData() - .getId(); - - return getServicePlanResource(servicePlanGuid, serviceInstanceResource.getName()).zipWhen(servicePlan -> getServiceOffering( - servicePlan.getRelationships() - .getServiceOffering() - .getData() - .getId())) - .map( - tuple -> ImmutableRawCloudServiceInstance.builder() - .resource( - serviceInstanceResource) - .servicePlan( - tuple.getT1()) - .serviceOffering( - tuple.getT2()) - .build()); - } - - private boolean isUserProvided(ServiceInstanceResource serviceInstanceResource) { - return ServiceInstanceType.USER_PROVIDED.equals(serviceInstanceResource.getType()); - } - - private Flux getServiceBindingResourcesByServiceInstanceGuid(UUID serviceInstanceGuid) { - IntFunction pageRequestSupplier = page -> ListServiceBindingsRequest.builder() - .serviceInstanceId( - serviceInstanceGuid.toString()) - .type( - ServiceBindingType.APPLICATION) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.serviceBindingsV3() - .list(pageRequestSupplier.apply(page))); - } - - private Flux getServiceBindingResourcesByApplicationGuidAndServiceInstanceGuid(UUID applicationGuid, - UUID serviceInstanceGuid) { - IntFunction pageRequestSupplier = page -> ListServiceBindingsRequest.builder() - .applicationId( - applicationGuid.toString()) - .serviceInstanceId( - serviceInstanceGuid.toString()) - .page(page) - .build(); - return getApplicationServiceBindingResources(pageRequestSupplier); - } - - private Flux getServiceBindingResourcesByApplicationGuid(UUID applicationGuid) { - IntFunction pageRequestSupplier = page -> ListServiceBindingsRequest.builder() - .applicationId( - applicationGuid.toString()) - .page(page) - .build(); - return getApplicationServiceBindingResources(pageRequestSupplier); - } - - private Flux getApplicationServiceBindingResources( - IntFunction pageRequestSupplier) { - return PaginationUtils.requestClientV3Resources(page -> delegate.serviceBindingsV3() - .list(pageRequestSupplier.apply(page))); - } - - private List findServicePlansByBrokerGuid(UUID brokerGuid) { - List offerings = findServiceOfferingsByBrokerGuid(brokerGuid); - return getPlans(offerings); - } - - private List getPlans(List offerings) { - return offerings.stream() - .map(CloudServiceOffering::getServicePlans) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - } - - private void updateServicePlanVisibility(CloudServicePlan servicePlan, ServicePlanVisibility visibility) { - updateServicePlanVisibility(getGuid(servicePlan), visibility); - } - - private void updateServicePlanVisibility(UUID servicePlanGuid, ServicePlanVisibility visibility) { - delegate.servicePlansV3() - .updateVisibility(UpdateServicePlanVisibilityRequest.builder() - .servicePlanId(servicePlanGuid.toString()) - .type(Visibility.from(visibility.toString())) - .build()) - .block(); - } - - private Mono getTaskResource(UUID guid) { - GetTaskRequest request = GetTaskRequest.builder() - .taskId(guid.toString()) - .build(); - return delegate.tasks() - .get(request); - } - - private Flux getTaskResourcesByApplicationGuid(UUID applicationGuid) { - IntFunction pageRequestSupplier = page -> ListTasksRequest.builder() - .applicationId(applicationGuid.toString()) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.tasks() - .list(pageRequestSupplier.apply(page))); - } - - private CloudTask createTask(UUID applicationGuid, CloudTask task) { - return fetch(() -> createTaskResource(applicationGuid, task), ImmutableRawCloudTask::of); - } - - private Mono createTaskResource(UUID applicationGuid, CloudTask task) { - CreateTaskRequest request = CreateTaskRequest.builder() - .applicationId(applicationGuid.toString()) - .command(task.getCommand()) - .name(task.getName()) - .memoryInMb(task.getLimits() - .getMemory()) - .diskInMb(task.getLimits() - .getDisk()) - .build(); - return delegate.tasks() - .create(request); - } - - private Mono cancelTaskResource(UUID taskGuid) { - CancelTaskRequest request = CancelTaskRequest.builder() - .taskId(taskGuid.toString()) - .build(); - return delegate.tasks() - .cancel(request); - } - - private CloudPackage startUpload(String applicationName, Path file, Duration uploadTimeout) { - Assert.notNull(applicationName, "AppName must not be null"); - Assert.notNull(file, "File must not be null"); - - UUID applicationGuid = getRequiredApplicationGuid(applicationName); - UUID packageGuid = getGuid(createPackageForApplication(applicationGuid)); - - delegate.packages() - .upload(UploadPackageRequest.builder() - .bits(file) - .packageId(packageGuid.toString()) - .build()) - .timeout(uploadTimeout) - .block(); - - return getPackage(packageGuid); - } - - private CloudPackage createPackageForApplication(UUID applicationGuid) { - return fetch(() -> createPackageResource(applicationGuid), ImmutableRawCloudPackage::of); - } - - private Mono createPackageResource(UUID applicationGuid) { - CreatePackageRequest request = CreatePackageRequest.builder() - .type(PackageType.BITS) - .relationships(buildPackageRelationships(applicationGuid)) - .build(); - return delegate.packages() - .create(request); - } - - private PackageRelationships buildPackageRelationships(UUID applicationGuid) { - return PackageRelationships.builder() - .application(buildToOneRelationship(applicationGuid)) - .build(); - } - - private ToOneRelationship buildToOneRelationship(UUID guid) { - return ToOneRelationship.builder() - .data(buildRelationship(guid.toString())) - .build(); - } - - private ToOneRelationship buildToOneRelationship(String guid) { - return ToOneRelationship.builder() - .data(buildRelationship(guid)) - .build(); - } - - private Relationship buildRelationship(String guid) { - return Relationship.builder() - .id(guid) - .build(); - } - - private Mono createBuildResource(UUID packageGuid) { - CreateBuildRequest request = CreateBuildRequest.builder() - .getPackage(buildRelationship(packageGuid.toString())) - .build(); - return delegate.builds() - .create(request); - } - - private Mono getBuildResource(UUID buildGuid) { - GetBuildRequest request = GetBuildRequest.builder() - .buildId(buildGuid.toString()) - .build(); - return delegate.builds() - .get(request); - } - - private Flux getBuildResourcesByApplicationGuid(UUID applicationGuid) { - IntFunction pageRequestSupplier = page -> ListApplicationBuildsRequest.builder() - .applicationId( - applicationGuid.toString()) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.applicationsV3() - .listBuilds(pageRequestSupplier.apply(page))); - } - - private Flux getBuildResourcesByPackageGuid(UUID packageGuid) { - IntFunction pageRequestSupplier = page -> ListBuildsRequest.builder() - .packageId(packageGuid.toString()) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.builds() - .list(pageRequestSupplier.apply(page))); - } - - private void assertSpaceProvided(String operation) { - Assert.notNull(target, "Unable to " + operation + " without specifying organization and space to use."); - } - - private void removeRoutes(Set routes, UUID applicationGuid) { - for (CloudRoute route : routes) { - for (RouteDestination destination : route.getDestinations()) { - if (destination.getApplicationGuid() - .equals(applicationGuid)) { - unbindRoute(route.getGuid(), destination.getGuid()); - } - } - } - } - - private void validateDomainForRoute(CloudRoute route, Map existingDomains) { - String domain = route.getDomain() - .getName(); - if (!StringUtils.hasLength(domain) || !existingDomains.containsKey(domain)) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), - "Domain '" + domain + "' not found for URI " + route.getUrl()); - } - } - - private void unbindRoute(UUID routeGuid, UUID destinationGuid) { - delegate.routesV3() - .removeDestinations(RemoveRouteDestinationsRequest.builder() - .routeId(routeGuid.toString()) - .destinationId(destinationGuid.toString()) - .build()) - .block(); - } - - private void bindRoute(UUID routeGuid, UUID applicationGuid, String protocol) { - delegate.routesV3() - .insertDestinations(InsertRouteDestinationsRequest.builder() - .routeId(routeGuid.toString()) - .destination(createDestination(applicationGuid, protocol)) - .build()) - .block(); - } - - private Destination createDestination(UUID applicationGuid, String protocol) { - return Destination.builder() - .application(org.cloudfoundry.client.v3.routes.Application.builder() - .applicationId(applicationGuid.toString()) - .build()) - .protocol(protocol) - .build(); - } - - private UUID getOrAddRoute(UUID domainGuid, String host, String path) { - UUID routeGuid = getRouteGuid(domainGuid, host, path); - if (routeGuid == null) { - routeGuid = doAddRoute(domainGuid, host, path); - } - return routeGuid; - } - - private UUID doAddRoute(UUID domainGuid, String host, String path) { - assertSpaceProvided("add route"); - CreateRouteResponse response = delegate.routesV3() - .create(CreateRouteRequest.builder() - .host(host) - .path(path) - .relationships(RouteRelationships.builder() - .domain(buildToOneRelationship( - domainGuid)) - .space(buildToOneRelationship( - getTargetSpaceGuid())) - .build()) - .build()) - .block(); - return getGuid(response); - } - - private void doCreateDomain(String name) { - delegate.domainsV3() - .create(CreateDomainRequest.builder() - .name(name) - .relationships(DomainRelationships.builder() - .organization( - buildToOneRelationship(getTargetOrganizationGuid())) - .build()) - .build()) - .block(); - } - - private void doDeleteDomain(UUID guid) { - delegate.domainsV3() - .delete(DeleteDomainRequest.builder() - .domainId(guid.toString()) - .build()) - .flatMap(jobId -> JobV3Util.waitForCompletion(delegate, DELETE_JOB_TIMEOUT, jobId)) - .block(); - } - - private void doDeleteRoute(UUID guid) { - delegate.routesV3() - .delete(DeleteRouteRequest.builder() - .routeId(guid.toString()) - .build()) - .flatMap(jobId -> JobV3Util.waitForCompletion(delegate, DELETE_JOB_TIMEOUT, jobId)) - .block(); - } - - private void doDeleteServiceInstance(UUID serviceInstanceGuid) { - delegate.serviceInstancesV3() - .delete(DeleteServiceInstanceRequest.builder() - .serviceInstanceId(serviceInstanceGuid.toString()) - .build()) - .block(); - } - - private List doUnbindServiceInstance(UUID applicationGuid, UUID serviceInstanceGuid) { - List serviceBindingGuids = getServiceBindingGuids(applicationGuid, serviceInstanceGuid); - if (serviceBindingGuids.isEmpty()) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", - "Service binding between service with GUID " + serviceInstanceGuid - + " and application with GUID " + applicationGuid + " not found."); - } - return doDeleteServiceBindings(serviceBindingGuids); - } - - private List doDeleteServiceBindings(List guids) { - List jobIds = new ArrayList<>(); - List errors = new ArrayList<>(); - for (UUID guid : guids) { - try { - Optional.ofNullable(deleteSingleServiceBinding(guid)) - .ifPresent(jobIds::add); - } catch (AbstractCloudFoundryException e) { - errors.add(e); - } - } - throwOnErrors(errors); - return jobIds; - } - - private String deleteSingleServiceBinding(UUID guid) { - return delegate.serviceBindingsV3() - .delete(DeleteServiceBindingRequest.builder() - .serviceBindingId(guid.toString()) - .build()) - .block(); - } - - private void throwOnErrors(List errors) { - if (errors.isEmpty()) { - return; - } - RuntimeException first = errors.get(0); - errors.subList(1, errors.size()) - .forEach(first::addSuppressed); - throw first; - } - - private CloudDomain findDomainByName(String name, boolean required) { - CloudDomain domain = findDomainByName(name); - if (domain == null && required) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Domain " + name + " not found."); - } - return domain; - } - - private CloudDomain findDomainByName(String name) { - return fetch(() -> getDomainResourceByName(name), ImmutableRawCloudDomain::of); - } - - private List findDomainsByOrganizationGuid(UUID organizationGuid) { - return fetchList(() -> getDomainResourcesByOrganizationGuid(organizationGuid), ImmutableRawCloudDomain::of); - } - - private Mono getDomainResourceByName(String name) { - IntFunction pageRequestSupplier = page -> ListDomainsRequest.builder() - .name(name) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.domainsV3() - .list(pageRequestSupplier.apply(page))) - .singleOrEmpty(); - } - - private Flux getDomainResources() { - IntFunction pageRequestSupplier = page -> ListDomainsRequest.builder() - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.domainsV3() - .list(pageRequestSupplier.apply(page))); - } - - private Flux getSharedDomainResources() { - return getDomainResources().filter(domain -> domain.getRelationships() - .getOrganization() - .getData() == null); - } - - private Flux getPrivateDomainResources() { - return getDomainResources().filter(domain -> domain.getRelationships() - .getOrganization() - .getData() != null); - } - - private Flux getDomainResourcesByOrganizationGuid(UUID organizationGuid) { - IntFunction pageRequestSupplier = page -> ListOrganizationDomainsRequest.builder() - .organizationId( - organizationGuid.toString()) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.organizationsV3() - .listDomains(pageRequestSupplier.apply(page))); - } - - private Flux getDomainResourcesByNamesInBatches(Set names) { - return Flux.fromIterable(toBatches(names, MAX_CHAR_LENGTH_FOR_PARAMS_IN_REQUEST)) - .flatMap(this::getDomainResourcesByNames); - } - - private Flux getDomainResourcesByNames(Collection names) { - if (names.isEmpty()) { - return Flux.empty(); - } - IntFunction pageRequestSupplier = page -> ListDomainsRequest.builder() - .names(names) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.domainsV3() - .list(pageRequestSupplier.apply(page))); - } - - private List findRoutes(CloudDomain domain) { - return fetchList(() -> getRouteResourcesByDomainGuidAndSpaceGuid(domain.getGuid(), getTargetSpaceGuid()), - ImmutableRawCloudRoute::of); - } - - private Flux getRouteResourcesByDomainGuidAndSpaceGuid(UUID domainGuid, UUID spaceGuid) { - IntFunction pageRequestSupplier = page -> ListRoutesRequest.builder() - .domainId(domainGuid.toString()) - .spaceId(spaceGuid.toString()) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.routesV3() - .list(pageRequestSupplier.apply(page))); - } - - private Flux getRouteResourcesByDomainGuidHostAndPath(UUID domainGuid, String host, String path) { - ListRoutesRequest.Builder requestBuilder = ListRoutesRequest.builder(); - if (host != null) { - requestBuilder.host(host); - } - if (path != null) { - requestBuilder.path(path); - } - requestBuilder.spaceId(getTargetSpaceGuid().toString()) - .domainId(domainGuid.toString()); - - return PaginationUtils.requestClientV3Resources(page -> delegate.routesV3() - .list(requestBuilder.page(page) - .build())); - } - - private Flux getRouteResourcesByAppGuid(UUID applicationGuid) { - IntFunction pageSupplier = page -> ListApplicationRoutesRequest.builder() - .applicationId( - applicationGuid.toString()) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.applicationsV3() - .listRoutes(pageSupplier.apply(page))); - } - - private List findServiceOfferingsByBrokerGuid(UUID brokerGuid) { - return fetchListWithAuxiliaryContent(() -> getServiceResourcesByBrokerGuid(brokerGuid), - this::zipWithAuxiliaryServiceOfferingContent); - } - - private List findServiceOfferingsByLabel(String label) { - Assert.notNull(label, "Service label must not be null"); - return fetchListWithAuxiliaryContent(() -> getServiceResourcesByLabel(label), this::zipWithAuxiliaryServiceOfferingContent); - } - - private List findServiceOfferingsByLabelAndBrokerName(String label, String brokerName) { - Assert.notNull(label, "Service label must not be null"); - Assert.notNull(brokerName, "Service broker must not be null"); - return fetchListWithAuxiliaryContent(() -> getServiceResourcesByLabelAndBrokerName(label, brokerName), - this::zipWithAuxiliaryServiceOfferingContent); - } - - private Flux getServiceResources() { - IntFunction pageRequestSupplier = page -> ListServiceOfferingsRequest.builder() - .spaceId( - getTargetSpaceGuid().toString()) - .page(page) - .build(); - return getServiceResources(pageRequestSupplier); - } - - protected Mono getServiceOffering(String offeringId) { - GetServiceOfferingRequest request = GetServiceOfferingRequest.builder() - .serviceOfferingId(offeringId) - .build(); - return delegate.serviceOfferingsV3() - .get(request) - .onErrorMap(t -> doesErrorMatchStatusCode(t, HttpStatus.FORBIDDEN), - t -> new CloudOperationException(HttpStatus.FORBIDDEN, HttpStatus.FORBIDDEN.getReasonPhrase(), - MessageFormat.format( - Messages.SERVICE_OFFERING_WITH_GUID_0_IS_NOT_AVAILABLE, offeringId), - t)) - .onErrorMap(t -> doesErrorMatchStatusCode(t, HttpStatus.NOT_FOUND), - t -> new CloudOperationException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), - MessageFormat.format(Messages.SERVICE_OFFERING_WITH_GUID_0_NOT_FOUND, - offeringId), t)); - } - - private Flux getServiceResourcesByBrokerGuid(UUID brokerGuid) { - IntFunction pageRequestSupplier = page -> ListServiceOfferingsRequest.builder() - .serviceBrokerId( - brokerGuid.toString()) - .spaceId( - getTargetSpaceGuid().toString()) - .page(page) - .build(); - return getServiceResources(pageRequestSupplier); - } - - private Flux getServiceResourcesByLabel(String label) { - IntFunction pageRequestSupplier = page -> ListServiceOfferingsRequest.builder() - .name(label) - .spaceId( - getTargetSpaceGuid().toString()) - .page(page) - .build(); - return getServiceResources(pageRequestSupplier); - } - - private Flux getServiceResourcesByLabelAndBrokerName(String label, String brokerName) { - IntFunction pageRequestSupplier = page -> ListServiceOfferingsRequest.builder() - .name(label) - .serviceBrokerName(brokerName) - .spaceId( - getTargetSpaceGuid().toString()) - .page(page) - .build(); - return getServiceResources(pageRequestSupplier); - } - - private Flux getServiceResources(IntFunction pageRequestSupplier) { - return PaginationUtils.requestClientV3Resources(page -> delegate.serviceOfferingsV3() - .list(pageRequestSupplier.apply(page))); - } - - private Mono> zipWithAuxiliaryServiceOfferingContent(ServiceOfferingResource serviceOffering) { - UUID serviceOfferingGuid = getGuid(serviceOffering); - return getServicePlansFlux(serviceOfferingGuid).collectList() - .map(servicePlans -> ImmutableRawCloudServiceOffering.builder() - .serviceOffering( - serviceOffering) - .servicePlans(servicePlans) - .build()); - } - - private Flux getServicePlansFlux(UUID serviceOfferingGuid) { - return fetchFlux(() -> getServicePlanResourcesByServiceOfferingGuid(serviceOfferingGuid), ImmutableRawCloudServicePlan::of); - } - - protected Mono getServicePlanResource(String servicePlanGuid, String serviceInstanceName) { - GetServicePlanRequest request = GetServicePlanRequest.builder() - .servicePlanId(servicePlanGuid) - .build(); - - return delegate.servicePlansV3() - .get(request) - .onErrorMap(t -> doesErrorMatchStatusCode(t, HttpStatus.FORBIDDEN), - t -> new CloudOperationException(HttpStatus.FORBIDDEN, HttpStatus.FORBIDDEN.getReasonPhrase(), - MessageFormat.format( - Messages.SERVICE_PLAN_WITH_GUID_0_NOT_AVAILABLE_FOR_SERVICE_INSTANCE_1, - servicePlanGuid, serviceInstanceName), t)) - .onErrorMap(t -> doesErrorMatchStatusCode(t, HttpStatus.NOT_FOUND), - t -> new CloudOperationException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), - MessageFormat.format(Messages.NO_SERVICE_PLAN_FOUND, servicePlanGuid, - serviceInstanceName), t)); - } - - private Flux getServicePlanResourcesByServiceOfferingGuid(UUID serviceOfferingGuid) { - IntFunction pageRequestSupplier = page -> ListServicePlansRequest.builder() - .serviceOfferingId( - serviceOfferingGuid.toString()) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.servicePlansV3() - .list(pageRequestSupplier.apply(page))); - } - - private Flux getServiceKeyResource(CloudServiceInstance serviceInstance) { - UUID serviceInstanceGuid = getGuid(serviceInstance); - return getServiceKeyResourcesByServiceInstanceGuid(serviceInstanceGuid); - } - - private Flux getServiceKeyResourcesByServiceInstanceGuid(UUID serviceInstanceGuid) { - IntFunction pageRequestSupplier = page -> ListServiceBindingsRequest.builder() - .serviceInstanceId( - serviceInstanceGuid.toString()) - .type(ServiceBindingType.KEY) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.serviceBindingsV3() - .list(pageRequestSupplier.apply(page))); - } - - private Mono getServiceKeyResourceByNameAndServiceInstanceGuid(String name, UUID guid) { - IntFunction pageRequestSupplier = page -> ListServiceBindingsRequest.builder() - .serviceInstanceId(guid.toString()) - .type(ServiceBindingType.KEY) - .name(name) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.serviceBindingsV3() - .list(pageRequestSupplier.apply(page))) - .singleOrEmpty(); - } - - private CloudStack findStackResource(String name) { - return fetch(() -> getStackResourceByName(name), ImmutableRawCloudStack::of); - } - - private Flux getStackResources() { - IntFunction pageRequestSupplier = page -> ListStacksRequest.builder() - .page(page) - .build(); - return getStackResources(pageRequestSupplier); - } - - private Mono getStackResourceByName(String name) { - IntFunction pageRequestSupplier = page -> ListStacksRequest.builder() - .name(name) - .page(page) - .build(); - return getStackResources(pageRequestSupplier).singleOrEmpty(); - } - - private Flux getStackResources(IntFunction requestForPage) { - return PaginationUtils.requestClientV3Resources(page -> delegate.stacksV3() - .list(requestForPage.apply(page))); - } - - private List findEventsByTarget(String target) { - return fetchList(() -> getEventResourcesByTarget(target), ImmutableRawCloudEvent::of); - } - - private Flux getEventResources() { - IntFunction pageRequestSupplier = page -> ListAuditEventsRequest.builder() - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.auditEventsV3() - .list(pageRequestSupplier.apply(page))); - } - - private Flux getEventResourcesByTarget(String target) { - IntFunction pageRequestSupplier = page -> ListAuditEventsRequest.builder() - .targetId(target) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.auditEventsV3() - .list(pageRequestSupplier.apply(page))); - } - - private InstancesInfo findApplicationInstances(UUID applicationGuid) { - return fetch(() -> getApplicationProcessStatsResource(applicationGuid), ImmutableRawInstancesInfo::of); - } - - private Mono getApplicationProcessStatsResource(UUID applicationGuid) { - return delegate.applicationsV3() - .getProcessStatistics(GetApplicationProcessStatisticsRequest.builder() - .applicationId(applicationGuid.toString()) - .type("web") - .build()); - } - - private CloudServiceBroker findServiceBrokerByName(String name) { - return fetch(() -> getServiceBrokerResourceByName(name), ImmutableRawCloudServiceBroker::of); - } - - private Flux getServiceBrokerResources() { - IntFunction pageRequestSupplier = page -> ListServiceBrokersRequest.builder() - .page(page) - .build(); - return getServiceBrokerResources(pageRequestSupplier); - } - - private Mono getServiceBrokerResourceByName(String name) { - IntFunction pageRequestSupplier = page -> ListServiceBrokersRequest.builder() - .page(page) - .name(name) - .build(); - return getServiceBrokerResources(pageRequestSupplier).singleOrEmpty(); - } - - private Flux getServiceBrokerResources(IntFunction pageRequestSupplier) { - return PaginationUtils.requestClientV3Resources(page -> delegate.serviceBrokersV3() - .list(pageRequestSupplier.apply(page))); - } - - private CloudServicePlan findPlanForService(CloudServiceInstance service, String planName) { - List offerings = getServiceOfferings(service); - for (var offering : offerings) { - for (var plan : offering.getServicePlans()) { - if (planName.equals(plan.getName())) { - return plan; - } - } - } - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service plan " + planName + " not found."); - } - - private List getServiceOfferings(CloudServiceInstance service) { - if (service.getBroker() == null) { - return findServiceOfferingsByLabel(service.getLabel()); - } - return findServiceOfferingsByLabelAndBrokerName(service.getLabel(), service.getBroker()); - } - - private UUID getRequiredApplicationGuid(String name) { - Resource applicationResource = getApplicationByName(name).block(); - if (applicationResource == null) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + name + " not found."); - } - return UUID.fromString(applicationResource.getId()); - } - - private Mono getServiceInstanceByName(String name) { - IntFunction pageRequestSupplier = page -> ListServiceInstancesRequest.builder() - .spaceId( - getTargetSpaceGuid().toString()) - .serviceInstanceName(name) - .page(page) - .build(); - return PaginationUtils.requestClientV3Resources(page -> delegate.serviceInstancesV3() - .list(pageRequestSupplier.apply(page))) - .singleOrEmpty(); - } - - private UUID getRequiredDomainGuid(String name) { - return getGuid(findDomainByName(name, true)); - } - - private Map getDomainsFromRoutes(Set routes) { - Set domainNames = routes.stream() - .map(route -> route.getDomain() - .getName()) - .filter(StringUtils::hasLength) - .collect(Collectors.toSet()); - return getDomainResourcesByNamesInBatches(domainNames).collectMap(DomainResource::getName, - domain -> UUID.fromString(domain.getId())) - .block(); - } - - private UUID getRouteGuid(UUID domainGuid, String host, String path) { - List routeEntitiesResource = getRouteResourcesByDomainGuidHostAndPath(domainGuid, host, path).collect( - Collectors.toList()) - .block(); - if (CollectionUtils.isEmpty(routeEntitiesResource)) { - return null; - } - return getGuid(routeEntitiesResource.get(0)); - } - - private List getServiceBindingGuids(UUID applicationGuid, UUID serviceInstanceGuid) { - return getServiceBindingResourcesByApplicationGuidAndServiceInstanceGuid(applicationGuid, serviceInstanceGuid).map(this::getGuid) - .collectList() - .block(); - } - - private void processAsyncUploadInBackground(CloudPackage cloudPackage, UploadStatusCallback callback) { - String threadName = String.format("App upload monitor: %s", cloudPackage.getGuid()); - new Thread(() -> processAsyncUpload(cloudPackage, callback), threadName).start(); - } - - private void processAsyncUpload(CloudPackage cloudPackage, UploadStatusCallback callback) { - while (true) { - Upload upload = getUploadStatus(cloudPackage.getGuid()); - Status uploadStatus = upload.getStatus(); - boolean unsubscribe = callback.onProgress(uploadStatus.toString()); - if (unsubscribe || isUploadReady(uploadStatus)) { - return; - } - if (hasUploadFailed(uploadStatus)) { - callback.onError(upload.getErrorDetails() - .getDescription()); - return; - } - try { - Thread.sleep(PACKAGE_UPLOAD_JOB_POLLING_PERIOD); - } catch (InterruptedException e) { - return; - } - } - } - - private boolean isUploadReady(Status status) { - return status == Status.READY; - } - - private boolean hasUploadFailed(Status status) { - return status == Status.EXPIRED || status == Status.FAILED; - } - - @Override - public CloudPackage getPackage(UUID packageGuid) { - return fetch(() -> getPackageResource(packageGuid), ImmutableRawCloudPackage::of); - } - - private Mono getPackageResource(UUID guid) { - GetPackageRequest request = GetPackageRequest.builder() - .packageId(guid.toString()) - .build(); - return delegate.packages() - .get(request); - } - - private UUID getTargetOrganizationGuid() { - return getGuid(target.getOrganization()); - } - - private UUID getTargetSpaceGuid() { - return getGuid(target); - } - - private UUID getGuid(CloudEntity entity) { - return Optional.ofNullable(entity) - .map(CloudEntity::getMetadata) - .map(CloudMetadata::getGuid) - .orElse(null); - } - - private UUID getGuid(Resource resource) { - return Optional.ofNullable(resource) - .map(Resource::getId) - .map(UUID::fromString) - .orElse(null); - } - - private boolean doesErrorMatchStatusCode(Throwable t, HttpStatus status) { - if (t instanceof AbstractCloudFoundryException e) { - return e.getStatusCode() == status.value(); - } - return false; - } - - private > Flux fetchFluxWithAuxiliaryContent(Supplier> resourceSupplier, - Function> resourceMapper) { - return resourceSupplier.get() - .flatMap(resourceMapper) - .map(Derivable::derive); - } - - private > List fetchListWithAuxiliaryContent(Supplier> resourceSupplier, - Function> resourceMapper) { - return fetchFluxWithAuxiliaryContent(resourceSupplier, resourceMapper).collectList() - .block(); - } - - private > List fetchList(Supplier> resourceSupplier, Function resourceMapper) { - return fetchFlux(resourceSupplier, resourceMapper).collectList() - .block(); - } - - private > Flux fetchFlux(Supplier> resourceSupplier, Function resourceMapper) { - return resourceSupplier.get() - .map(resourceMapper) - .map(Derivable::derive); - } - - private > T fetchWithAuxiliaryContent(Supplier> resourceSupplier, - Function> resourceMapper) { - return fetchMonoWithAuxiliaryContent(resourceSupplier, resourceMapper).block(); - } - - private > T fetch(Supplier> resourceSupplier, Function resourceMapper) { - return fetchMono(resourceSupplier, resourceMapper).block(); - } - - private > Mono fetchMonoWithAuxiliaryContent(Supplier> resourceSupplier, - Function> resourceMapper) { - return resourceSupplier.get() - .flatMap(resourceMapper) - .map(Derivable::derive); - } - - private > Mono fetchMono(Supplier> resourceSupplier, Function resourceMapper) { - return resourceSupplier.get() - .map(resourceMapper) - .map(Derivable::derive); - } - -} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java index de312d1a0b..2adab4c566 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java @@ -10,8 +10,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Job; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -33,7 +31,6 @@ */ public class CloudControllerV3Client { - private static final Logger LOGGER = LoggerFactory.getLogger(CloudControllerV3Client.class); // Mirrors the OSS JobV3Util backoff so async behaviour is unchanged. private static final Duration JOB_POLL_MIN_INTERVAL = Duration.ofSeconds(1); @@ -47,7 +44,6 @@ public CloudControllerV3Client(RestClient restClient) { } public T get(String uri, Class responseType) { - LOGGER.warn("[cf-v3-poc] GET {}", uri); return restClient.get() .uri(uri) .retrieve() @@ -77,7 +73,6 @@ public List list(String firstPageUri, ParameterizedTypeReference all = new ArrayList<>(); String nextUri = firstPageUri; while (nextUri != null) { - LOGGER.warn("[cf-v3-poc] LIST {}", nextUri); V3ListResponse page = restClient.get() .uri(nextUri) .retrieve() diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudSpaceClient.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudSpaceClient.java index 0eb52d8e0d..2f860840b0 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudSpaceClient.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudSpaceClient.java @@ -1,127 +1,80 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; +import java.util.List; +import java.util.UUID; + import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.RawCloudEntity; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudOrganization; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Organization; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ResourceMappers; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Space; import org.cloudfoundry.multiapps.controller.client.facade.util.UriUtil; - -import org.cloudfoundry.AbstractCloudFoundryException; -import org.cloudfoundry.client.v3.ClientV3Exception; -import org.cloudfoundry.client.v3.organizations.GetOrganizationRequest; -import org.cloudfoundry.client.v3.organizations.ListOrganizationsRequest; -import org.cloudfoundry.client.v3.organizations.Organization; -import org.cloudfoundry.client.v3.organizations.OrganizationsV3; -import org.cloudfoundry.client.v3.spaces.GetSpaceRequest; -import org.cloudfoundry.client.v3.spaces.ListSpacesRequest; -import org.cloudfoundry.client.v3.spaces.Space; -import org.cloudfoundry.client.v3.spaces.SpacesV3; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import java.time.Duration; -import java.util.List; -import java.util.UUID; +/** + * Resolves a target {@link CloudSpace} (space + its organization) by GUID or by org-name/space-name, talking directly to the CF v3 REST + * API through {@link CloudControllerV3Client}. Replaces the OSS cf-java-client-based implementation (which used {@code SpacesV3} / + * {@code OrganizationsV3}); behaviour and 404 semantics are preserved. + */ public class CloudSpaceClient { private static final List CHARS_TO_ENCODE = List.of(","); - private static final long RETRIES = 3; - private static final Duration RETRY_INTERVAL = Duration.ofSeconds(3); - private final SpacesV3 spacesClient; - private final OrganizationsV3 orgsClient; + private final CloudControllerV3Client cc; - public CloudSpaceClient(SpacesV3 spacesClient, OrganizationsV3 orgsClient) { - this.spacesClient = spacesClient; - this.orgsClient = orgsClient; + public CloudSpaceClient(CloudControllerV3Client cc) { + this.cc = cc; } public CloudSpace getSpace(UUID spaceGuid) { - Space space = spacesClient.get(GetSpaceRequest.builder() - .spaceId(spaceGuid.toString()) - .build()) - .retryWhen(Retry.fixedDelay(RETRIES, RETRY_INTERVAL) - .onRetryExhaustedThrow(this::throwOriginalError)) - .onErrorMap(ClientV3Exception.class, this::convertV3ClientException) - .block(); - if (space == null) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Space with GUID " + spaceGuid + " not found."); - } - - String orgGuid = space.getRelationships() - .getOrganization() - .getData() - .getId(); - Organization org = orgsClient.get(GetOrganizationRequest.builder() - .organizationId(orgGuid) - .build()) - .retryWhen(Retry.fixedDelay(RETRIES, RETRY_INTERVAL) - .onRetryExhaustedThrow(this::throwOriginalError)) - .onErrorMap(ClientV3Exception.class, this::convertV3ClientException) - .block(); - if (org == null) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Organization with GUID " + orgGuid + " not found."); - } + V3Space space = cc.getOptional("/v3/spaces/" + spaceGuid, V3Space.class) + .orElseThrow(() -> new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", + "Space with GUID " + spaceGuid + " not found.")); + String orgGuid = space.organizationGuid(); + V3Organization org = cc.getOptional("/v3/organizations/" + orgGuid, V3Organization.class) + .orElseThrow(() -> new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", + "Organization with GUID " + orgGuid + " not found.")); return mapToCloudSpace(space, org); } public CloudSpace getSpace(String organizationName, String spaceName) { - var orgsResponse = orgsClient.list(ListOrganizationsRequest.builder() - .name(encodeAsQueryParam(organizationName)) - .build()) - .retryWhen(Retry.fixedDelay(RETRIES, RETRY_INTERVAL) - .onRetryExhaustedThrow(this::throwOriginalError)) - .onErrorMap(ClientV3Exception.class, this::convertV3ClientException) - .block(); - List orgs = orgsResponse.getResources(); + List orgs = cc.list("/v3/organizations?names=" + encodeAsQueryParam(organizationName), + new ParameterizedTypeReference>() { + }); if (orgs.isEmpty()) { throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Organization " + organizationName + " not found."); } + V3Organization org = orgs.get(0); - Organization org = orgs.get(0); - - var spacesResponse = spacesClient.list(ListSpacesRequest.builder() - .organizationId(org.getId()) - .name(encodeAsQueryParam(spaceName)) - .build()) - .retryWhen(Retry.fixedDelay(RETRIES, RETRY_INTERVAL) - .onRetryExhaustedThrow(this::throwOriginalError)) - .onErrorMap(ClientV3Exception.class, this::convertV3ClientException) - .block(); - List spaces = spacesResponse.getResources(); + List spaces = cc.list("/v3/spaces?organization_guids=" + org.guid() + "&names=" + encodeAsQueryParam(spaceName), + new ParameterizedTypeReference>() { + }); if (spaces.isEmpty()) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Space " + spaceName + " not found in organization " + organizationName); + throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", + "Space " + spaceName + " not found in organization " + organizationName); } - - Space space = spaces.get(0); - return mapToCloudSpace(space, org); + return mapToCloudSpace(spaces.get(0), org); } private String encodeAsQueryParam(String param) { return UriUtil.encodeChars(param, CHARS_TO_ENCODE); } - private CloudSpace mapToCloudSpace(Space space, Organization org) { + private CloudSpace mapToCloudSpace(V3Space space, V3Organization org) { return ImmutableCloudSpace.builder() - .metadata(RawCloudEntity.parseResourceMetadata(space)) - .name(space.getName()) + .metadata(V3ResourceMappers.parseMetadata(space.guid(), space.createdAt(), space.updatedAt())) + .name(space.name()) .organization(ImmutableCloudOrganization.builder() - .metadata(RawCloudEntity.parseResourceMetadata(org)) - .name(org.getName()) + .metadata(V3ResourceMappers.parseMetadata(org.guid(), + org.createdAt(), + org.updatedAt())) + .name(org.name()) .build()) .build(); } - private Throwable throwOriginalError(RetryBackoffSpec retrySpec, Retry.RetrySignal signal) { - return signal.failure(); - } - - private CloudOperationException convertV3ClientException(AbstractCloudFoundryException e) { - HttpStatus httpStatus = HttpStatus.valueOf(e.getStatusCode()); - return new CloudOperationException(httpStatus, httpStatus.getReasonPhrase(), e.getMessage(), e); - } - } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/LogCacheReadResponse.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/LogCacheReadResponse.java new file mode 100644 index 0000000000..3965703cdb --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/LogCacheReadResponse.java @@ -0,0 +1,45 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a Log-Cache {@code read} response ({@code GET /api/v1/read/{source_id}}), which is a plain JSON/REST API: + * + *
+ * { "envelopes": { "batch": [
+ *     { "source_id": "...", "timestamp": "1699999999000000000", "tags": { "source_type": "APP/PROC/WEB" },
+ *       "log": { "payload": "<base64>", "type": "OUT" | "ERR" } } ] } }
+ * 
+ * + * Replaces the OSS {@code org.cloudfoundry.logcache.v1.*} types so the log-cache client no longer depends on cf-java-client. Only the + * fields the mapper needs are declared. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record LogCacheReadResponse(@JsonProperty("envelopes") EnvelopeBatch envelopes) { + + public List batch() { + if (envelopes == null || envelopes.batch() == null) { + return Collections.emptyList(); + } + return envelopes.batch(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record EnvelopeBatch(@JsonProperty("batch") List batch) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record Envelope(@JsonProperty("source_id") String sourceId, @JsonProperty("timestamp") Long timestamp, + @JsonProperty("tags") Map tags, @JsonProperty("log") Log log) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record Log(@JsonProperty("payload") String payload, @JsonProperty("type") String type) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Organization.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Organization.java new file mode 100644 index 0000000000..e7b9a14729 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Organization.java @@ -0,0 +1,12 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 organization resource ({@code GET /v3/organizations/{guid}} / {@code GET /v3/organizations?...}). + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3Organization(@JsonProperty("guid") String guid, @JsonProperty("name") String name, + @JsonProperty("created_at") String createdAt, @JsonProperty("updated_at") String updatedAt) { +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Space.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Space.java new file mode 100644 index 0000000000..4e7c0d28c8 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Space.java @@ -0,0 +1,40 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Thin Jackson wire-model of a CF v3 space resource ({@code GET /v3/spaces/{guid}} / {@code GET /v3/spaces?...}). + * + *
+ * { "guid": "...", "name": "...", "created_at": "...", "updated_at": "...",
+ *   "relationships": { "organization": { "data": { "guid": "..." } } } }
+ * 
+ */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record V3Space(@JsonProperty("guid") String guid, @JsonProperty("name") String name, @JsonProperty("created_at") String createdAt, + @JsonProperty("updated_at") String updatedAt, @JsonProperty("relationships") V3SpaceRelationships relationships) { + + public String organizationGuid() { + if (relationships == null || relationships.organization() == null || relationships.organization() + .data() == null) { + return null; + } + return relationships.organization() + .data() + .guid(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3SpaceRelationships(@JsonProperty("organization") V3ToOne organization) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3ToOne(@JsonProperty("data") V3Data data) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record V3Data(@JsonProperty("guid") String guid) { + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/util/JobV3Util.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/util/JobV3Util.java deleted file mode 100644 index e7e293c426..0000000000 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/util/JobV3Util.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.util; - -import java.time.Duration; -import java.util.EnumSet; -import java.util.List; -import java.util.Set; - -import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.client.v3.ClientV3Exception; -import org.cloudfoundry.client.v3.Error; -import org.cloudfoundry.client.v3.jobs.GetJobRequest; -import org.cloudfoundry.client.v3.jobs.GetJobResponse; -import org.cloudfoundry.client.v3.jobs.Job; -import org.cloudfoundry.client.v3.jobs.JobState; -import org.cloudfoundry.util.DelayUtils; - -import reactor.core.publisher.Mono; - -public class JobV3Util { - - private JobV3Util() { - } - - private static final Set FINAL_STATES = EnumSet.of(JobState.COMPLETE, JobState.FAILED); - - public static Mono waitForCompletion(CloudFoundryClient cloudFoundryClient, Duration completionTimeout, String jobId) { - return requestJobV3(cloudFoundryClient, jobId).filter(job -> FINAL_STATES.contains(job.getState())) - .repeatWhenEmpty(DelayUtils.exponentialBackOff(Duration.ofSeconds(1), - Duration.ofSeconds(15), - completionTimeout)) - .filter(job -> JobState.FAILED == job.getState()) - .flatMap(JobV3Util::getError); - } - - private static Mono requestJobV3(CloudFoundryClient cloudFoundryClient, String jobId) { - return cloudFoundryClient.jobsV3() - .get(GetJobRequest.builder() - .jobId(jobId) - .build()); - } - - private static Mono getError(Job job) { - List errors = job.getErrors(); - // Status code must be set, otherwise it will throw NPE during getStatusCode() invocation - return Mono.error(new ClientV3Exception(200, errors)); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/ApplicationsCloudControllerClientIntegrationTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/ApplicationsCloudControllerClientIntegrationTest.java index 70cc2867b7..95ca5e50bd 100644 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/ApplicationsCloudControllerClientIntegrationTest.java +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/ApplicationsCloudControllerClientIntegrationTest.java @@ -157,27 +157,12 @@ void updateApplicationHealthcheckType() { String applicationName = "test-application-16"; try { createAndVerifyDefaultApplication(applicationName); + client.updateApplicationStaging(applicationName, ImmutableStaging.builder() + .healthCheckType(HealthCheckType.PROCESS.getValue()) + .build()); UUID applicationGuid = client.getApplicationGuid(applicationName); - var processResponse = delegate.applicationsV3() - .getProcess(org.cloudfoundry.client.v3.applications.GetApplicationProcessRequest.builder() - .applicationId( - applicationGuid.toString()) - .type("web") - .build()) - .block(); - delegate.processes() - .update(org.cloudfoundry.client.v3.processes.UpdateProcessRequest.builder() - .processId(processResponse.getId()) - .healthCheck( - org.cloudfoundry.client.v3.processes.HealthCheck.builder() - .type( - org.cloudfoundry.client.v3.processes.HealthCheckType.PROCESS) - .build()) - .build()) - .block(); CloudProcess cloudProcess = client.getApplicationProcess(applicationGuid); - assertEquals(org.cloudfoundry.multiapps.controller.client.facade.domain.HealthCheckType.PROCESS, - cloudProcess.getHealthCheckType()); + assertEquals(HealthCheckType.PROCESS, cloudProcess.getHealthCheckType()); } catch (Exception e) { fail(e); } finally { diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientIntegrationTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientIntegrationTest.java index b394252229..3b0e958345 100644 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientIntegrationTest.java +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientIntegrationTest.java @@ -7,15 +7,14 @@ import java.util.HashMap; import java.util.Map; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableCloudFoundryClientFactory; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableLifecycle; import org.cloudfoundry.multiapps.controller.client.facade.domain.Lifecycle; import org.cloudfoundry.multiapps.controller.client.facade.domain.LifecycleType; import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudSpaceClient; +import org.cloudfoundry.multiapps.controller.client.facade.rest.ImmutableCloudControllerRestClientFactory; import org.cloudfoundry.multiapps.controller.client.facade.util.RestUtil; -import org.cloudfoundry.client.CloudFoundryClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -30,7 +29,6 @@ abstract class CloudControllerClientIntegrationTest { protected static final String DEFAULT_STACK = "cflinuxfs4"; protected static CloudControllerClient client; - protected static CloudFoundryClient delegate; protected static CloudSpace target; @BeforeAll @@ -39,14 +37,14 @@ static void login() throws MalformedURLException { CloudCredentials credentials = getCloudCredentials(); URL apiUrl = URI.create(ITVariable.CF_API.getValue()) .toURL(); - var clientFactory = ImmutableCloudFoundryClientFactory.builder() - .build(); + var clientFactory = ImmutableCloudControllerRestClientFactory.builder() + .shouldTrustSelfSignedCertificates(true) + .build(); var oauthClient = new RestUtil().createOAuthClientByControllerUrl(apiUrl, true); oauthClient.init(credentials); CloudSpaceClient spaceClient = clientFactory.createSpaceClient(apiUrl, oauthClient, Collections.emptyMap()); target = spaceClient.getSpace(ITVariable.ORG.getValue(), ITVariable.SPACE.getValue()); client = new CloudControllerClientImpl(apiUrl, credentials, target, true); - delegate = clientFactory.createClient(apiUrl, oauthClient, Collections.emptyMap()); } @BeforeEach diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/OAuthTokenProviderTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/OAuthTokenProviderTest.java deleted file mode 100644 index f6131944b1..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/OAuthTokenProviderTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo; -import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; -import org.cloudfoundry.reactor.ConnectionContext; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import reactor.core.publisher.Mono; - -class OAuthTokenProviderTest { - - @Mock - private OAuthClient oAuthClient; - @Mock - private OAuth2AccessTokenWithAdditionalInfo token; - @Mock - private ConnectionContext connectionContext; - - private OAuthTokenProvider provider; - - @BeforeEach - void setUp() throws Exception { - MockitoAnnotations.openMocks(this) - .close(); - provider = new OAuthTokenProvider(oAuthClient); - } - - @Test - void testGetTokenIsLazyAndDoesNotInvokeOAuthClientUntilSubscribed() { - Mockito.when(oAuthClient.getToken()) - .thenReturn(token); - Mockito.when(token.getAuthorizationHeaderValue()) - .thenReturn("Bearer abc123"); - - Mono mono = provider.getToken(connectionContext); - - Mockito.verify(oAuthClient, Mockito.never()) - .getToken(); - - String result = mono.block(); - - Assertions.assertEquals("Bearer abc123", result); - Mockito.verify(oAuthClient) - .getToken(); - } - - @Test - void testGetTokenInvokesOAuthClientOncePerSubscription() { - Mockito.when(oAuthClient.getToken()) - .thenReturn(token); - Mockito.when(token.getAuthorizationHeaderValue()) - .thenReturn("Bearer abc123"); - - Mono mono = provider.getToken(connectionContext); - - mono.block(); - mono.block(); - - Mockito.verify(oAuthClient, Mockito.times(2)) - .getToken(); - } - - @Test - void testGetTokenPropagatesOAuthClientFailure() { - RuntimeException failure = new RuntimeException("token retrieval failed"); - Mockito.when(oAuthClient.getToken()) - .thenThrow(failure); - - Mono mono = provider.getToken(connectionContext); - - RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, mono::block); - Assertions.assertSame(failure, thrown); - } -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplicationTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplicationTest.java deleted file mode 100644 index e864ff93a9..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplicationTest.java +++ /dev/null @@ -1,132 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.List; -import java.util.Map; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; -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.ImmutableCloudSpace; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableLifecycle; -import org.cloudfoundry.client.v3.BuildpackData; -import org.cloudfoundry.client.v3.DockerData; -import org.cloudfoundry.client.v3.Lifecycle; -import org.cloudfoundry.client.v3.LifecycleType; -import org.cloudfoundry.client.v3.applications.ApplicationResource; -import org.cloudfoundry.client.v3.applications.ApplicationState; -import org.junit.jupiter.api.Test; - -class RawCloudApplicationTest { - - private static final String STATE = "STARTED"; - private static final String BUILDPACK = "ruby_buildpack"; - private static final String STACK_NAME = "cflinuxfs3"; - private static final String SPACE_NAME = "test"; - private static final CloudSpace SPACE = ImmutableCloudSpace.builder() - .name(SPACE_NAME) - .build(); - private static final String EXPECTED_BUILDPACK = "ruby_buildpack"; - private static final String BUILDPACK_URL = "custom-buildpack-url"; - - private static final String EXPECTED_STACK = "cflinuxfs3"; - private static final CloudApplication.State EXPECTED_STATE = CloudApplication.State.STARTED; - - @Test - void testDeriveForBuildpackApp() { - RawCloudEntityTest.testDerive(buildApplication(buildBuildpackLifecycle()), buildRawApplication(buildBuildpackLifecycleResource())); - } - - @Test - void testDeriveForDockerApp() { - RawCloudEntityTest.testDerive(buildApplication(buildDockerLifecycle()), buildRawApplication(buildDockerLifecycleResource())); - } - - @Test - void testDeriveForCnbApp() { - RawCloudEntityTest.testDerive(buildApplication(buildCnbLifecycle()), buildRawApplication(buildCnbLifecycleResource())); - } - - private static CloudApplication buildApplication(org.cloudfoundry.multiapps.controller.client.facade.domain.Lifecycle lifecycle) { - return ImmutableCloudApplication.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_V3) - .v3Metadata(RawCloudEntityTest.V3_METADATA_DOMAIN) - .name(RawCloudEntityTest.NAME) - .state(EXPECTED_STATE) - .lifecycle(lifecycle) - .space(SPACE) - .build(); - } - - private static org.cloudfoundry.multiapps.controller.client.facade.domain.Lifecycle buildBuildpackLifecycle() { - return ImmutableLifecycle.builder() - .type(org.cloudfoundry.multiapps.controller.client.facade.domain.LifecycleType.BUILDPACK) - .data(buildLifecycleData(EXPECTED_BUILDPACK)) - .build(); - } - - private org.cloudfoundry.multiapps.controller.client.facade.domain.Lifecycle buildCnbLifecycle() { - return ImmutableLifecycle.builder() - .type(org.cloudfoundry.multiapps.controller.client.facade.domain.LifecycleType.CNB) - .data(buildLifecycleData(BUILDPACK_URL)) - .build(); - } - - private static org.cloudfoundry.multiapps.controller.client.facade.domain.Lifecycle buildDockerLifecycle() { - return ImmutableLifecycle.builder() - .type(org.cloudfoundry.multiapps.controller.client.facade.domain.LifecycleType.DOCKER) - .data(Map.of()) - .build(); - } - - private static Map buildLifecycleData(String buildpack) { - return Map.of("buildpacks", List.of(buildpack), "stack", EXPECTED_STACK); - } - - private static RawCloudApplication buildRawApplication(Lifecycle lifecycle) { - return ImmutableRawCloudApplication.builder() - .application(buildApplicationResource(lifecycle)) - .space(SPACE) - .build(); - } - - private static ApplicationResource buildApplicationResource(Lifecycle lifecycle) { - return ApplicationResource.builder() - .metadata(RawCloudEntityTest.V3_METADATA) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .state(ApplicationState.valueOf(STATE)) - .id(RawCloudEntityTest.GUID.toString()) - .lifecycle(lifecycle) - .name("foo") - .build(); - } - - private static Lifecycle buildBuildpackLifecycleResource() { - return Lifecycle.builder() - .type(LifecycleType.BUILDPACK) - .data(BuildpackData.builder() - .buildpack(BUILDPACK) - .stack(STACK_NAME) - .build()) - .build(); - } - - private Lifecycle buildCnbLifecycleResource() { - return Lifecycle.builder() - .type(LifecycleType.CNB) - .data(BuildpackData.builder() - .buildpack(BUILDPACK_URL) - .stack(STACK_NAME) - .build()) - .build(); - } - - private static Lifecycle buildDockerLifecycleResource() { - return Lifecycle.builder() - .type(LifecycleType.DOCKER) - .data(DockerData.builder() - .build()) - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJobTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJobTest.java deleted file mode 100644 index 41407fb6b6..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJobTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.List; - -import org.cloudfoundry.client.v3.Error; -import org.cloudfoundry.client.v3.jobs.GetJobResponse; -import org.cloudfoundry.client.v3.jobs.Job; -import org.cloudfoundry.client.v3.jobs.JobState; -import org.cloudfoundry.client.v3.jobs.Warning; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudAsyncJob; - -class RawCloudAsyncJobTest { - - private static final String OPERATION = "service_broker.delete"; - private static final String WARNING_1 = "warning1"; - private static final String WARNING_2 = "warning2"; - private static final String WARNINGS = "warning1,warning2"; - private static final int ERROR_CODE_1 = 10008; - private static final int ERROR_CODE_2 = 1000; - private static final String ERROR_TITLE_1 = "CF-UnprocessableEntity"; - private static final String ERROR_TITLE_2 = "CF-InvalidAuthToken"; - private static final String ERROR_DETAIL_1 = "something went wrong"; - private static final String ERROR_DETAIL_2 = "not valid token"; - private static final String ERRORS = "10008 CF-UnprocessableEntity something went wrong,1000 CF-InvalidAuthToken not valid token"; - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedTask(), buildRawTask()); - } - - private CloudAsyncJob buildExpectedTask() { - return ImmutableCloudAsyncJob.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE) - .state(org.cloudfoundry.multiapps.controller.client.facade.domain.JobState.FAILED) - .operation(OPERATION) - .warnings(WARNINGS) - .errors(ERRORS) - .build(); - } - - private RawCloudAsyncJob buildRawTask() { - return ImmutableRawCloudAsyncJob.of(buildTestResource()); - } - - private Job buildTestResource() { - return GetJobResponse.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .state(JobState.FAILED) - .operation(OPERATION) - .warnings(buildWarnings()) - .errors(buildErrors()) - .build(); - } - - private List buildWarnings() { - return List.of(Warning.builder() - .detail(WARNING_1) - .build(), - Warning.builder() - .detail(WARNING_2) - .build()); - } - - private List buildErrors() { - return List.of(Error.builder() - .code(ERROR_CODE_1) - .title(ERROR_TITLE_1) - .detail(ERROR_DETAIL_1) - .build(), - Error.builder() - .code(ERROR_CODE_2) - .title(ERROR_TITLE_2) - .detail(ERROR_DETAIL_2) - .build()); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudBuildTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudBuildTest.java deleted file mode 100644 index c35ec79de0..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudBuildTest.java +++ /dev/null @@ -1,137 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.UUID; - -import org.cloudfoundry.client.v3.BuildpackData; -import org.cloudfoundry.client.v3.Lifecycle; -import org.cloudfoundry.client.v3.LifecycleType; -import org.cloudfoundry.client.v3.Relationship; -import org.cloudfoundry.client.v3.builds.Build; -import org.cloudfoundry.client.v3.builds.BuildResource; -import org.cloudfoundry.client.v3.builds.BuildState; -import org.cloudfoundry.client.v3.builds.CreatedBy; -import org.cloudfoundry.client.v3.builds.Droplet; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudBuild; -import org.cloudfoundry.multiapps.controller.client.facade.domain.DropletInfo; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudBuild; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableDropletInfo; - -class RawCloudBuildTest { - - private static final String PACKAGE_GUID_STRING = "d0bd1437-f112-48cd-b777-6b160cf2fff0"; - private static final String DROPLET_GUID_STRING = "47e172c9-bfb2-41df-bf3f-850108c241ef"; - private static final Droplet DROPLET = buildTestDroplet(); - private static final String CREATED_BY_GUID_STRING = "37964fd8-a234-4c04-af31-c3fe5e477bed"; - private static final String CREATED_BY_NAME = "admin"; - private static final LifecycleType LIFECYCLE_TYPE = LifecycleType.BUILDPACK; - private static final BuildState STATE = BuildState.FAILED; - private static final String ERROR = "blabla"; - - private static final UUID PACKAGE_GUID = UUID.fromString(PACKAGE_GUID_STRING); - private static final UUID DROPLET_GUID = UUID.fromString(DROPLET_GUID_STRING); - private static final UUID CREATED_BY_GUID = UUID.fromString(CREATED_BY_GUID_STRING); - - private static final CloudBuild.State EXPECTED_STATE = CloudBuild.State.FAILED; - private static final DropletInfo EXPECTED_DROPLET_INFO = buildExpectedDropletInfo(); - - private static CloudBuild buildExpectedBuild() { - return buildExpectedBuild(null); - } - - private static CloudBuild buildExpectedBuild(DropletInfo dropletInfo) { - return ImmutableCloudBuild.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE) - .createdBy(buildExpectedCreatedBy()) - .dropletInfo(dropletInfo) - .packageInfo(buildExpectedPackageInfo()) - .state(EXPECTED_STATE) - .error(ERROR) - .build(); - } - - private static CloudBuild.CreatedBy buildExpectedCreatedBy() { - return ImmutableCloudBuild.ImmutableCreatedBy.builder() - .guid(CREATED_BY_GUID) - .name(CREATED_BY_NAME) - .build(); - } - - private static DropletInfo buildExpectedDropletInfo() { - return ImmutableDropletInfo.builder() - .guid(DROPLET_GUID) - .build(); - } - - private static CloudBuild.PackageInfo buildExpectedPackageInfo() { - return ImmutableCloudBuild.ImmutablePackageInfo.builder() - .guid(PACKAGE_GUID) - .build(); - } - - private static Build buildTestResource(Droplet droplet) { - return BuildResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .inputPackage(buildTestPackage()) - .createdBy(buildTestCreatedBy()) - .lifecycle(buildTestLifecycle()) - .droplet(droplet) - .state(STATE) - .error(ERROR) - .build(); - } - - private static Relationship buildTestPackage() { - return Relationship.builder() - .id(PACKAGE_GUID_STRING) - .build(); - } - - private static Droplet buildTestDroplet() { - return Droplet.builder() - .id(DROPLET_GUID_STRING) - .build(); - } - - private static CreatedBy buildTestCreatedBy() { - return CreatedBy.builder() - .id(CREATED_BY_GUID_STRING) - .name(CREATED_BY_NAME) - .email(CREATED_BY_NAME) - .build(); - } - - private static Lifecycle buildTestLifecycle() { - return Lifecycle.builder() - .data(buildTestLifecycleData()) - .type(LIFECYCLE_TYPE) - .build(); - } - - private static BuildpackData buildTestLifecycleData() { - return BuildpackData.builder() - .build(); - } - - @Test - void testDeriveWithoutDroplet() { - RawCloudEntityTest.testDerive(buildExpectedBuild(), buildRawBuild()); - } - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedBuild(EXPECTED_DROPLET_INFO), buildRawBuild(DROPLET)); - } - - private RawCloudBuild buildRawBuild() { - return buildRawBuild(null); - } - - private RawCloudBuild buildRawBuild(Droplet droplet) { - return ImmutableRawCloudBuild.of(buildTestResource(droplet)); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudDomainTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudDomainTest.java deleted file mode 100644 index 65fd5a6642..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudDomainTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.client.v3.ToManyRelationship; -import org.cloudfoundry.client.v3.ToOneRelationship; -import org.cloudfoundry.client.v3.domains.Domain; -import org.cloudfoundry.client.v3.domains.DomainRelationships; -import org.cloudfoundry.client.v3.organizations.GetOrganizationDefaultDomainResponse; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudDomain; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudDomain; - -class RawCloudDomainTest { - - private static final String DOMAIN_NAME = "example.com"; - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedDomain(), buildRawDomain()); - } - - private static CloudDomain buildExpectedDomain() { - return ImmutableCloudDomain.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE) - .name(DOMAIN_NAME) - .build(); - } - - private static RawCloudDomain buildRawDomain() { - return ImmutableRawCloudDomain.of(buildTestResource()); - } - - private static Domain buildTestResource() { - return GetOrganizationDefaultDomainResponse.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .name(DOMAIN_NAME) - .isInternal(false) - .relationships(DomainRelationships.builder() - .organization(ToOneRelationship.builder() - .build()) - .sharedOrganizations(ToManyRelationship.builder() - .build()) - .build()) - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntityTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntityTest.java deleted file mode 100644 index 8e0e74876d..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntityTest.java +++ /dev/null @@ -1,158 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import org.cloudfoundry.client.v3.Metadata; -import org.cloudfoundry.client.v3.Relationship; -import org.cloudfoundry.client.v3.ToOneRelationship; -import org.cloudfoundry.client.v3.applications.ApplicationState; -import org.cloudfoundry.client.v3.organizations.Organization; -import org.cloudfoundry.client.v3.organizations.OrganizationResource; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudBuild; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudMetadata; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Derivable; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; - -class RawCloudEntityTest { - - static final String GUID_STRING = "3725650a-8725-4401-a949-c68f83d54a86"; - static final String CREATED_AT_STRING = "2017-06-22T13:38:41Z"; - static final String UPDATED_AT_STRING = "2019-03-21T12:29:24Z"; - - static final String NAME = "foo"; - static final UUID GUID = UUID.fromString(GUID_STRING); - static final LocalDateTime CREATED_AT = fromZonedDateTime(ZonedDateTime.of(2017, 6, 22, 13, 38, 41, 0, ZoneId.of("Z"))); - static final LocalDateTime UPDATED_AT = fromZonedDateTime(ZonedDateTime.of(2019, 3, 21, 12, 29, 24, 0, ZoneId.of("Z"))); - static final Map V3_ANNOTATIONS = Map.of("annotation1", "value1", "annotation2", "value2"); - static final Map V3_LABELS = Map.of("label1", "value1", "label2", "value2"); - - static final Metadata V3_METADATA = Metadata.builder() - .annotations(V3_ANNOTATIONS) - .labels(V3_LABELS) - .build(); - - // Domain-typed equivalent of V3_METADATA, for building expected domain objects (CloudEntity.getV3Metadata() is domain.Metadata). - static final org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata V3_METADATA_DOMAIN = - org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata.builder() - .annotations(V3_ANNOTATIONS) - .labels(V3_LABELS) - .build(); - - static final CloudMetadata EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE = ImmutableCloudMetadata.builder() - .guid(GUID) - .createdAt(CREATED_AT) - .updatedAt(UPDATED_AT) - .build(); - static final CloudMetadata EXPECTED_METADATA_V3 = ImmutableCloudMetadata.builder() - .guid(GUID) - .createdAt(CREATED_AT) - .updatedAt(UPDATED_AT) - .build(); - - static LocalDateTime fromZonedDateTime(ZonedDateTime dateTime) { - return dateTime.toLocalDateTime(); - } - - @Test - void testParseV3ResourceMetadata() { - Organization resource = OrganizationResource.builder() - .id(GUID_STRING) - .createdAt(CREATED_AT_STRING) - .updatedAt(UPDATED_AT_STRING) - .name(NAME) - .metadata(V3_METADATA) - .build(); - - CloudMetadata metadata = RawCloudEntity.parseResourceMetadata(resource); - assertEquals(EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE, metadata); - } - - @Test - void testParseNullableGuid() { - assertNull(RawCloudEntity.parseNullableGuid(null)); - } - - @Test - void testParseNullableDate() { - assertNull(RawCloudEntity.parseNullableDate(null)); - } - - @Test - void testParseGuid() { - assertEquals(GUID, RawCloudEntity.parseGuid(GUID_STRING)); - } - - @Test - void testParseGuidWithInvalidGuid() { - assertNull(RawCloudEntity.parseGuid("foo")); - } - - @Test - void testParseDateWithInvalidDate() { - assertNull(RawCloudEntity.parseDate("foo")); - } - - @Test - void testParseDateWithInvalidFormat() { - assertNull(RawCloudEntity.parseDate("16.07.2019 15:30:25")); - } - - @Test - void testParseDate() { - assertEquals(CREATED_AT, RawCloudEntity.parseDate(CREATED_AT_STRING)); - } - - @Test - void testParseEnum() { - CloudApplication.State state = RawCloudEntity.parseEnum(ApplicationState.STARTED, CloudApplication.State.class); - assertEquals(CloudApplication.State.STARTED, state); - } - - @Test - void testParseEnumWithIncompatibleEnumTypes() { - Assertions.assertThrows(IllegalArgumentException.class, - () -> RawCloudEntity.parseEnum(ApplicationState.STARTED, CloudBuild.State.class)); - } - - @Test - void testDeriveFromNullable() { - assertEquals(NAME, RawCloudEntity.deriveFromNullable(() -> NAME)); - } - - @Test - void testDeriveFromNullableWithNull() { - assertNull(RawCloudEntity.deriveFromNullable(null)); - } - - @Test - void testDerive() { - List> derivables = Arrays.asList(() -> NAME, () -> GUID_STRING); - assertEquals(Arrays.asList(NAME, GUID_STRING), RawCloudEntity.derive(derivables)); - } - - static void testDerive(T expected, Derivable derivable) { - assertEquals(expected, derivable.derive()); - } - - static ToOneRelationship buildToOneRelationship(String id) { - return ToOneRelationship.builder() - .data(Relationship.builder() - .id(id) - .build()) - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEventTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEventTest.java deleted file mode 100644 index 2a826d312a..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEventTest.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.UUID; - -import org.cloudfoundry.client.v3.auditevents.AuditEventActor; -import org.cloudfoundry.client.v3.auditevents.AuditEventResource; -import org.cloudfoundry.client.v3.auditevents.AuditEventTarget; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEvent; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEvent.Participant; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudEvent; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudEvent.ImmutableParticipant; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; - -class RawCloudEventTest { - - private static final String TARGET_GUID_STRING = "b7c57058-afc0-43c9-afee-64019e850bef"; - private static final String TARGET_NAME = "foo"; - private static final String TARGET_TYPE = "app"; - private static final String ACTOR_GUID_STRING = "72c1e48a-9629-4cfe-a64c-f53851d81f61"; - private static final String ACTOR_NAME = "john"; - private static final String ACTOR_TYPE = "user"; - private static final String TIMESTAMP_STRING = "2019-07-03T20:00:46Z"; - private static final String TYPE = "audit.app.create"; - - private static final UUID TARGET_GUID = UUID.fromString(TARGET_GUID_STRING); - private static final UUID ACTOR_GUID = UUID.fromString(ACTOR_GUID_STRING); - private static final LocalDateTime TIMESTAMP = RawCloudEntityTest.fromZonedDateTime(ZonedDateTime.of(2019, 7, 3, 20, 0, 46, 0, - ZoneId.of("Z"))); - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedEvent(), buildRawEvent()); - } - - @Test - void testDeriveWithEmptyResponse() { - RawCloudEntityTest.testDerive(buildEmptyExpectedEvent(), buildEmptyRawEvent()); - } - - private CloudEvent buildExpectedEvent() { - return ImmutableCloudEvent.builder() - .metadata(ImmutableCloudMetadata.builder() - .guid(RawCloudEntityTest.GUID) - .createdAt(TIMESTAMP) - .updatedAt(RawCloudEntityTest.UPDATED_AT) - .build()) - .target(buildExpectedTarget()) - .actor(buildExpectedActor()) - .type(TYPE) - .build(); - } - - private Participant buildExpectedTarget() { - return ImmutableParticipant.builder() - .guid(TARGET_GUID) - .name(TARGET_NAME) - .type(TARGET_TYPE) - .build(); - } - - private Participant buildExpectedActor() { - return ImmutableParticipant.builder() - .guid(ACTOR_GUID) - .name(ACTOR_NAME) - .type(ACTOR_TYPE) - .build(); - } - - private CloudEvent buildEmptyExpectedEvent() { - return ImmutableCloudEvent.builder() - .metadata(ImmutableCloudMetadata.builder() - .guid(RawCloudEntityTest.GUID) - .createdAt(TIMESTAMP) - .updatedAt(RawCloudEntityTest.UPDATED_AT) - .build()) - .target(buildEmptyParticipant()) - .actor(buildEmptyParticipant()) - .build(); - } - - private Participant buildEmptyParticipant() { - return ImmutableParticipant.builder() - .build(); - } - - private RawCloudEvent buildRawEvent() { - return ImmutableRawCloudEvent.of(buildTestResource()); - } - - private AuditEventResource buildTestResource() { - return AuditEventResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(TIMESTAMP_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .type(TYPE) - .auditEventActor(AuditEventActor.builder() - .id(ACTOR_GUID_STRING) - .name(ACTOR_NAME) - .type(ACTOR_TYPE) - .build()) - .auditEventTarget(AuditEventTarget.builder() - .id(TARGET_GUID_STRING) - .type(TARGET_TYPE) - .name(TARGET_NAME) - .build()) - .build(); - } - - private RawCloudEvent buildEmptyRawEvent() { - return ImmutableRawCloudEvent.of(buildEmptyTestResource()); - } - - private AuditEventResource buildEmptyTestResource() { - return AuditEventResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(TIMESTAMP_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudPackageTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudPackageTest.java deleted file mode 100644 index 2f636ea79b..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudPackageTest.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.client.v3.Checksum; -import org.cloudfoundry.client.v3.ChecksumType; -import org.cloudfoundry.client.v3.packages.BitsData; -import org.cloudfoundry.client.v3.packages.DockerData; -import org.cloudfoundry.client.v3.packages.Package; -import org.cloudfoundry.client.v3.packages.PackageData; -import org.cloudfoundry.client.v3.packages.PackageResource; -import org.cloudfoundry.client.v3.packages.PackageState; -import org.cloudfoundry.client.v3.packages.PackageType; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudPackage; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableBitsData; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudPackage; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableDockerData; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Status; - -class RawCloudPackageTest { - - private static final PackageState STATE = PackageState.EXPIRED; - private static final String ERROR = "blabla"; - private static final String DOCKER_IMAGE = "cloudfoundry/test"; - private static final ChecksumType CHECKSUM_TYPE = ChecksumType.SHA256; - private static final String CHECKSUM_VALUE = "7251a608605c0f45710f8415bba0d117c90598824697a2a5d00850ea9b179112"; - - private static final Status EXPECTED_STATUS = Status.EXPIRED; - private static final String EXPECTED_CHECKSUM_ALGORITHM = CHECKSUM_TYPE.toString(); - - @Test - void testDeriveWithBitsData() { - RawCloudEntityTest.testDerive(buildExpectedPackage(CloudPackage.Type.BITS), buildRawPackage(PackageType.BITS)); - } - - @Test - void testDeriveWithDockerData() { - RawCloudEntityTest.testDerive(buildExpectedPackage(CloudPackage.Type.DOCKER), buildRawPackage(PackageType.DOCKER)); - } - - private static CloudPackage buildExpectedPackage(CloudPackage.Type type) { - return ImmutableCloudPackage.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE) - .data(buildExpectedData(type)) - .type(type) - .status(EXPECTED_STATUS) - .build(); - } - - private static CloudPackage.PackageData buildExpectedData(CloudPackage.Type type) { - if (type == CloudPackage.Type.DOCKER) { - return ImmutableDockerData.builder() - .image(DOCKER_IMAGE) - .build(); - } - return ImmutableBitsData.builder() - .checksum(buildExpectedChecksum()) - .error(ERROR) - .build(); - } - - private static org.cloudfoundry.multiapps.controller.client.facade.domain.BitsData.Checksum buildExpectedChecksum() { - return ImmutableBitsData.ImmutableChecksum.builder() - .algorithm(EXPECTED_CHECKSUM_ALGORITHM) - .value(CHECKSUM_VALUE) - .build(); - } - - private static RawCloudPackage buildRawPackage(PackageType type) { - return ImmutableRawCloudPackage.builder() - .resource(buildTestResource(type)) - .build(); - } - - private static Package buildTestResource(PackageType type) { - return PackageResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .type(type) - .data(buildTestData(type)) - .state(STATE) - .build(); - } - - private static PackageData buildTestData(PackageType type) { - if (type == PackageType.DOCKER) { - return buildDockerData(); - } - return buildBitsData(); - } - - private static DockerData buildDockerData() { - return DockerData.builder() - .image(DOCKER_IMAGE) - .build(); - } - - private static BitsData buildBitsData() { - return BitsData.builder() - .checksum(buildTestChecksum()) - .error(ERROR) - .build(); - } - - private static Checksum buildTestChecksum() { - return Checksum.builder() - .type(CHECKSUM_TYPE) - .value(CHECKSUM_VALUE) - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcessTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcessTest.java deleted file mode 100644 index b57652433c..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcessTest.java +++ /dev/null @@ -1,146 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.client.v3.Metadata; -import org.cloudfoundry.client.v3.processes.Data; -import org.cloudfoundry.client.v3.processes.HealthCheck; -import org.cloudfoundry.client.v3.processes.HealthCheckType; -import org.cloudfoundry.client.v3.processes.ProcessRelationships; -import org.cloudfoundry.client.v3.processes.ProcessResource; -import org.cloudfoundry.client.v3.processes.ReadinessHealthCheck; -import org.cloudfoundry.client.v3.processes.ReadinessHealthCheckType; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudProcess; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudProcess; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -class RawCloudProcessTest { - - private static final String COMMAND = "bundle exec rackup"; - private static final Integer DISK_IN_MB = 1024; - private static final Integer INSTANCES = 2; - private static final Integer MEMORY_IN_MB = 512; - private static final String HTTP_ENDPOINT = "/health"; - private static final Integer TIMEOUT = 30; - private static final Integer INVOCATION_TIMEOUT = 5; - private static final Integer INTERVAL = 10; - private static final String READINESS_HTTP_ENDPOINT = "/ready"; - private static final Integer READINESS_INVOCATION_TIMEOUT = 4; - private static final Integer READINESS_INTERVAL = 8; - - @Test - void testDeriveWithFullHealthCheckDataIncludingInterval() { - RawCloudProcess raw = ImmutableRawCloudProcess.of(buildProcessResource(HealthCheckType.HTTP, buildHealthCheckData(), - ReadinessHealthCheckType.HTTP, - buildReadinessHealthCheckData())); - - CloudProcess derived = raw.derive(); - - assertEquals(buildExpected(org.cloudfoundry.multiapps.controller.client.facade.domain.HealthCheckType.HTTP, INTERVAL, - ReadinessHealthCheckType.HTTP.getValue()), - derived); - } - - @Test - void testDeriveWithMissingHealthCheckDataLeavesIntervalNull() { - // healthCheck.getData() returns null -> all four health-check Integer/String fields stay null - RawCloudProcess raw = ImmutableRawCloudProcess.of(buildProcessResource(HealthCheckType.PORT, null, - ReadinessHealthCheckType.PORT, null)); - - CloudProcess derived = raw.derive(); - - assertEquals(org.cloudfoundry.multiapps.controller.client.facade.domain.HealthCheckType.PORT, derived.getHealthCheckType()); - assertNull(derived.getHealthCheckTimeout()); - assertNull(derived.getHealthCheckInvocationTimeout()); - assertNull(derived.getHealthCheckHttpEndpoint()); - assertNull(derived.getHealthCheckInterval()); - assertNull(derived.getReadinessHealthCheckInvocationTimeout()); - assertNull(derived.getReadinessHealthCheckHttpEndpoint()); - assertNull(derived.getReadinessHealthCheckInterval()); - } - - @Test - void testDeriveWithHealthCheckDataPresentButIntervalNull() { - Data dataWithoutInterval = Data.builder() - .endpoint(HTTP_ENDPOINT) - .timeout(TIMEOUT) - .invocationTimeout(INVOCATION_TIMEOUT) - .build(); - RawCloudProcess raw = ImmutableRawCloudProcess.of(buildProcessResource(HealthCheckType.HTTP, dataWithoutInterval, - ReadinessHealthCheckType.PORT, null)); - - CloudProcess derived = raw.derive(); - - assertEquals(TIMEOUT, derived.getHealthCheckTimeout()); - assertEquals(INVOCATION_TIMEOUT, derived.getHealthCheckInvocationTimeout()); - assertEquals(HTTP_ENDPOINT, derived.getHealthCheckHttpEndpoint()); - assertNull(derived.getHealthCheckInterval()); - } - - private static ProcessResource buildProcessResource(HealthCheckType healthCheckType, Data healthCheckData, - ReadinessHealthCheckType readinessHealthCheckType, Data readinessHealthCheckData) { - HealthCheck.Builder healthCheckBuilder = HealthCheck.builder() - .type(healthCheckType); - if (healthCheckData != null) { - healthCheckBuilder.data(healthCheckData); - } - ReadinessHealthCheck.Builder readinessBuilder = ReadinessHealthCheck.builder() - .type(readinessHealthCheckType); - if (readinessHealthCheckData != null) { - readinessBuilder.data(readinessHealthCheckData); - } - return ProcessResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .command(COMMAND) - .diskInMb(DISK_IN_MB) - .instances(INSTANCES) - .memoryInMb(MEMORY_IN_MB) - .type("web") - .metadata(Metadata.builder() - .build()) - .relationships(ProcessRelationships.builder() - .build()) - .healthCheck(healthCheckBuilder.build()) - .readinessHealthCheck(readinessBuilder.build()) - .build(); - } - - private static Data buildHealthCheckData() { - return Data.builder() - .endpoint(HTTP_ENDPOINT) - .timeout(TIMEOUT) - .invocationTimeout(INVOCATION_TIMEOUT) - .interval(INTERVAL) - .build(); - } - - private static Data buildReadinessHealthCheckData() { - return Data.builder() - .endpoint(READINESS_HTTP_ENDPOINT) - .invocationTimeout(READINESS_INVOCATION_TIMEOUT) - .interval(READINESS_INTERVAL) - .build(); - } - - private static CloudProcess buildExpected(org.cloudfoundry.multiapps.controller.client.facade.domain.HealthCheckType type, - Integer interval, String readinessHealthCheckType) { - return ImmutableCloudProcess.builder() - .command(COMMAND) - .instances(INSTANCES) - .memoryInMb(MEMORY_IN_MB) - .diskInMb(DISK_IN_MB) - .healthCheckType(type) - .healthCheckHttpEndpoint(HTTP_ENDPOINT) - .healthCheckTimeout(TIMEOUT) - .healthCheckInvocationTimeout(INVOCATION_TIMEOUT) - .healthCheckInterval(interval) - .readinessHealthCheckType(readinessHealthCheckType) - .readinessHealthCheckHttpEndpoint(READINESS_HTTP_ENDPOINT) - .readinessHealthCheckInvocationTimeout(READINESS_INVOCATION_TIMEOUT) - .readinessHealthCheckInterval(READINESS_INTERVAL) - .build(); - } -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudRouteTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudRouteTest.java deleted file mode 100644 index 94f19d18d3..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudRouteTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.List; -import java.util.UUID; - -import org.cloudfoundry.client.v3.Relationship; -import org.cloudfoundry.client.v3.ToOneRelationship; -import org.cloudfoundry.client.v3.routes.Application; -import org.cloudfoundry.client.v3.routes.Destination; -import org.cloudfoundry.client.v3.routes.Route; -import org.cloudfoundry.client.v3.routes.RouteRelationships; -import org.cloudfoundry.client.v3.routes.RouteResource; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudDomain; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudRoute; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudDomain; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudRoute; - -class RawCloudRouteTest { - - private static final String HOST = "foo"; - private static final String DOMAIN_NAME = "example.com"; - private static final CloudDomain DOMAIN = buildTestDomain(); - private static final Integer APPS_USING_ROUTE = 2; - private static final UUID DESTINATION_1_GUID = UUID.randomUUID(); - private static final UUID APPLICATION_1_GUID = UUID.randomUUID(); - private static final String DESTINATION_1_PROTOCOL = "http2"; - private static final UUID DESTINATION_2_GUID = UUID.randomUUID(); - private static final UUID APPLICATION_2_GUID = UUID.randomUUID(); - private static final String DESTINATION_2_PROTOCOL = "http1"; - private static final List DESTINATIONS = buildTestDestinations(); - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedRoute(), buildRawRoute()); - } - - private static CloudRoute buildExpectedRoute() { - return ImmutableCloudRoute.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_V3) - .host(HOST) - .domain(DOMAIN) - .path("") - .appsUsingRoute(APPS_USING_ROUTE) - .url(HOST + "." + DOMAIN_NAME) - .build(); - } - - private static RawCloudRoute buildRawRoute() { - return ImmutableRawCloudRoute.builder() - .route(buildTestRoute()) - .build(); - } - - private static Route buildTestRoute() { - return RouteResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .relationships(RouteRelationships.builder() - .space(buildToOneRelationship(RawCloudEntityTest.GUID)) - .domain(buildToOneRelationship(RawCloudEntityTest.GUID)) - .build()) - .metadata(RawCloudEntityTest.V3_METADATA) - .host(HOST) - .path("") - .url(HOST + "." + DOMAIN_NAME) - .addAllDestinations(DESTINATIONS) - .build(); - } - - private static CloudDomain buildTestDomain() { - return ImmutableCloudDomain.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_V3) - .name(DOMAIN_NAME) - .build(); - } - - private static List buildTestDestinations() { - return List.of(buildDestination(DESTINATION_1_GUID.toString(), APPLICATION_1_GUID.toString(), DESTINATION_1_PROTOCOL), - buildDestination(DESTINATION_2_GUID.toString(), APPLICATION_2_GUID.toString(), DESTINATION_2_PROTOCOL)); - } - - private static Destination buildDestination(String destinationGuid, String applicationGuid, String protocol) { - return Destination.builder() - .destinationId(destinationGuid) - .application(Application.builder() - .applicationId(applicationGuid) - .build()) - .protocol(protocol) - .build(); - } - - private static ToOneRelationship buildToOneRelationship(UUID guid) { - return ToOneRelationship.builder() - .data(buildRelationship(guid)) - .build(); - } - - private static Relationship buildRelationship(UUID guid) { - return Relationship.builder() - .id(guid.toString()) - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBindingTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBindingTest.java deleted file mode 100644 index b2d1600aa8..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBindingTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.time.LocalDateTime; -import java.util.UUID; - -import org.cloudfoundry.client.v3.LastOperation; -import org.cloudfoundry.client.v3.servicebindings.ServiceBindingRelationships; -import org.cloudfoundry.client.v3.servicebindings.ServiceBindingResource; -import org.cloudfoundry.client.v3.servicebindings.ServiceBindingType; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBinding; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceBinding; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceCredentialBindingOperation; - -class RawCloudServiceBindingTest { - - private static final String APPLICATION_GUID_STRING = "3725650a-8725-4401-a949-c68f83d54a86"; - private static final String SERVICE_INSTANCE_GUID_STRING = "3725650a-8725-4401-a949-c68f83d54a86"; - private static final UUID APPLICATION_GUID = UUID.fromString(APPLICATION_GUID_STRING); - private static final UUID SERVICE_INSTANCE_GUID = UUID.fromString(SERVICE_INSTANCE_GUID_STRING); - private static final LastOperation LAST_OPERATION = buildLastOperation(); - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedServiceBinding(), buildRawServiceBinding()); - } - - private static CloudServiceBinding buildExpectedServiceBinding() { - return ImmutableCloudServiceBinding.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_V3) - .applicationGuid(APPLICATION_GUID) - .serviceInstanceGuid(SERVICE_INSTANCE_GUID) - .serviceBindingOperation(ServiceCredentialBindingOperation.from( - new org.cloudfoundry.multiapps.controller.client.facade.domain.LastOperation( - LAST_OPERATION.getType(), LAST_OPERATION.getState(), LAST_OPERATION.getDescription(), - LAST_OPERATION.getCreatedAt(), LAST_OPERATION.getUpdatedAt()))) - .build(); - } - - private static RawCloudServiceBinding buildRawServiceBinding() { - return ImmutableRawCloudServiceBinding.builder() - .serviceBinding(buildTestResource()) - - .build(); - } - - private static ServiceBindingResource buildTestResource() { - return ServiceBindingResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .type(ServiceBindingType.APPLICATION) - .lastOperation(LAST_OPERATION) - .relationships(ServiceBindingRelationships.builder() - .serviceInstance(RawCloudEntityTest.buildToOneRelationship(SERVICE_INSTANCE_GUID_STRING)) - .application(RawCloudEntityTest.buildToOneRelationship(APPLICATION_GUID_STRING)) - .build()) - .build(); - } - - private static LastOperation buildLastOperation() { - return LastOperation.builder() - .state(ServiceCredentialBindingOperation.State.SUCCEEDED.toString()) - .type(ServiceCredentialBindingOperation.Type.CREATE.toString()) - .createdAt(LocalDateTime.now() - .toString()) - .updatedAt(LocalDateTime.now() - .toString()) - .description("Service binding created") - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBrokerTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBrokerTest.java deleted file mode 100644 index c3c9dca252..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBrokerTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.client.v3.Relationship; -import org.cloudfoundry.client.v3.ToOneRelationship; -import org.cloudfoundry.client.v3.servicebrokers.ServiceBrokerRelationships; -import org.cloudfoundry.client.v3.servicebrokers.ServiceBrokerResource; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBroker; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceBroker; - -class RawCloudServiceBrokerTest { - - private static final String NAME = "auditlog-broker"; - private static final String URL = "https://auditlog-broker.example.com"; - private static final String SPACE_GUID_STRING = "ef93547f-74c3-4bad-ba69-b7dc4f212622"; - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedServiceBroker(), buildRawServiceBroker()); - } - - private static CloudServiceBroker buildExpectedServiceBroker() { - return ImmutableCloudServiceBroker.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE) - .name(NAME) - .spaceGuid(SPACE_GUID_STRING) - .url(URL) - .build(); - } - - private static RawCloudServiceBroker buildRawServiceBroker() { - return ImmutableRawCloudServiceBroker.builder() - .serviceBroker(buildTestServiceBroker()) - .build(); - } - - private static ServiceBrokerResource buildTestServiceBroker() { - return ServiceBrokerResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .name(NAME) - .url(URL) - .relationships(ServiceBrokerRelationships.builder() - .space(ToOneRelationship.builder() - .data(Relationship.builder() - .id(SPACE_GUID_STRING) - .build()) - .build()) - .build()) - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstanceTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstanceTest.java deleted file mode 100644 index 1c41c4f866..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstanceTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import org.cloudfoundry.client.v3.LastOperation; -import org.cloudfoundry.client.v3.MaintenanceInfo; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; -import org.cloudfoundry.client.v3.Relationship; -import org.cloudfoundry.client.v3.ToOneRelationship; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceRelationships; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceResource; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceInstance; - -class RawCloudServiceInstanceTest { - - private static final String SERVICE_INSTANCE_ID = "7cb26016-342d-11ed-a261-0242ac120002"; - private static final String SERVICE_INSTANCE_NAME = "v3-service-instance"; - private static final String DATE = "2022-01-01T13:35:11Z"; - private static final String DASHBOARD_URL = "dashboard-url"; - private static final String LAST_OPERATION_DESCRIPTION = "Last operation has succeeded"; - private static final String LAST_OPERATION_STATE = "succeeded"; - private static final String LAST_OPERATION_TYPE = "create"; - private static final String MAINTENANCE_INFO_DESCRIPTION = "A long time ago"; - private static final String MAINTENANCE_INFO_VERSION = "0.9.9"; - private static final Map METADATA_ANNOTATIONS = Map.of("foo", "bar"); - private static final Map METADATA_LABELS = Map.of("baz", "mop"); - private static final String SPACE_GUID = "5f5a8db4-342c-11ed-a261-0242ac120002"; - private static final String SERVICE_PLAN_GUID = "810a0322-342c-11ed-a261-0242ac120002"; - private static final String ROUTE_SERVICE_URL = "https://route-service-url"; - private static final String SYSLOG_DRAIN_URL = "https://syslog-drain"; - private static final List TAGS = List.of("one", "two", "three"); - private static final ServiceInstanceType SERVICE_INSTANCE_TYPE = ServiceInstanceType.MANAGED; - private static final boolean UPDATE_AVAILABLE = false; - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedServiceInstance(), buildRawServiceInstance()); - } - - private CloudServiceInstance buildExpectedServiceInstance() { - return ImmutableCloudServiceInstance.builder() - .metadata(ImmutableCloudMetadata.builder() - .createdAt(ZonedDateTime.parse(DATE, - DateTimeFormatter.ISO_DATE_TIME) - .toLocalDateTime()) - .updatedAt(ZonedDateTime.parse(DATE, - DateTimeFormatter.ISO_DATE_TIME) - .toLocalDateTime()) - .guid(UUID.fromString(SERVICE_INSTANCE_ID)) - .build()) - .name(SERVICE_INSTANCE_NAME) - .v3Metadata(org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata.builder() - .annotations(METADATA_ANNOTATIONS) - .labels(METADATA_LABELS) - .build()) - .tags(TAGS) - .build(); - } - - private RawV3CloudServiceInstance buildRawServiceInstance() { - return ImmutableRawV3CloudServiceInstance.builder() - .serviceInstance(buildServiceInstanceResource()) - .build(); - } - - private ServiceInstanceResource buildServiceInstanceResource() { - return ServiceInstanceResource.builder() - .id(SERVICE_INSTANCE_ID) - .name(SERVICE_INSTANCE_NAME) - .createdAt(DATE) - .updatedAt(DATE) - .dashboardUrl(DASHBOARD_URL) - .lastOperation(LastOperation.builder() - .createdAt(DATE) - .description(LAST_OPERATION_DESCRIPTION) - .state(LAST_OPERATION_STATE) - .type(LAST_OPERATION_TYPE) - .updatedAt(DATE) - .build()) - .maintenanceInfo(MaintenanceInfo.builder() - .description(MAINTENANCE_INFO_DESCRIPTION) - .version(MAINTENANCE_INFO_VERSION) - .build()) - .metadata(org.cloudfoundry.client.v3.Metadata.builder() - .annotations(METADATA_ANNOTATIONS) - .labels(METADATA_LABELS) - .build()) - .relationships(ServiceInstanceRelationships.builder() - .space(ToOneRelationship.builder() - .data(Relationship.builder() - .id(SPACE_GUID) - .build()) - .build()) - .servicePlan(ToOneRelationship.builder() - .data(Relationship.builder() - .id(SERVICE_PLAN_GUID) - .build()) - .build()) - .build()) - .routeServiceUrl(ROUTE_SERVICE_URL) - .syslogDrainUrl(SYSLOG_DRAIN_URL) - .tags(TAGS) - .type(SERVICE_INSTANCE_TYPE) - .updateAvailable(UPDATE_AVAILABLE) - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKeyTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKeyTest.java deleted file mode 100644 index 4a8a69f161..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKeyTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; - -import org.cloudfoundry.client.v3.LastOperation; -import org.cloudfoundry.client.v3.servicebindings.ServiceBindingRelationships; -import org.cloudfoundry.client.v3.servicebindings.ServiceBindingResource; -import org.cloudfoundry.client.v3.servicebindings.ServiceBindingType; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; -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.ServiceCredentialBindingOperation; - -class RawCloudServiceKeyTest { - - private static final String SERVICE_NAME = "foo"; - private static final String NAME = "bar"; - private static final Map CREDENTIALS = buildTestCredentials(); - private static final CloudServiceInstance SERVICE_INSTANCE = ImmutableCloudServiceInstance.builder() - .name(SERVICE_NAME) - .build(); - private static final LastOperation LAST_OPERATION = buildLastOperation(); - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedServiceKey(), buildRawServiceKey()); - } - - private static CloudServiceKey buildExpectedServiceKey() { - return ImmutableCloudServiceKey.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_V3) - .v3Metadata(RawCloudEntityTest.V3_METADATA_DOMAIN) - .name(NAME) - .credentials(CREDENTIALS) - .serviceInstance(SERVICE_INSTANCE) - .serviceKeyOperation(ServiceCredentialBindingOperation.from( - new org.cloudfoundry.multiapps.controller.client.facade.domain.LastOperation( - LAST_OPERATION.getType(), LAST_OPERATION.getState(), LAST_OPERATION.getDescription(), - LAST_OPERATION.getCreatedAt(), LAST_OPERATION.getUpdatedAt()))) - .build(); - } - - private static RawCloudServiceKey buildRawServiceKey() { - return ImmutableRawCloudServiceKey.builder() - .serviceBindingResource(buildTestResource()) - .serviceInstance(SERVICE_INSTANCE) - .credentials(CREDENTIALS) - .build(); - } - - private static ServiceBindingResource buildTestResource() { - return ServiceBindingResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .metadata(RawCloudEntityTest.V3_METADATA) - .name(NAME) - .type(ServiceBindingType.KEY) - .lastOperation(LAST_OPERATION) - .relationships(ServiceBindingRelationships.builder() - .serviceInstance(RawCloudEntityTest.buildToOneRelationship("")) - .build()) - .build(); - } - - private static Map buildTestCredentials() { - Map parameters = new HashMap<>(); - parameters.put("foo", "bar"); - parameters.put("baz", false); - parameters.put("qux", 3.141); - return parameters; - } - - private static LastOperation buildLastOperation() { - return LastOperation.builder() - .state(ServiceCredentialBindingOperation.State.SUCCEEDED.toString()) - .type(ServiceCredentialBindingOperation.Type.CREATE.toString()) - .createdAt(LocalDateTime.now() - .toString()) - .updatedAt(LocalDateTime.now() - .toString()) - .description("Service key created") - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceOfferingTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceOfferingTest.java deleted file mode 100644 index b80fd6d986..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceOfferingTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import org.cloudfoundry.client.v3.Relationship; -import org.cloudfoundry.client.v3.ToOneRelationship; -import org.cloudfoundry.client.v3.serviceofferings.BrokerCatalog; -import org.cloudfoundry.client.v3.serviceofferings.Features; -import org.cloudfoundry.client.v3.serviceofferings.ServiceOfferingRelationships; -import org.cloudfoundry.client.v3.serviceofferings.ServiceOfferingResource; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceOffering; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServicePlan; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceOffering; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServicePlan; - -public class RawCloudServiceOfferingTest { - - private static final String NAME = "postgresql"; - static final boolean AVAILABLE = true; - static final boolean BINDABLE = true; - static final boolean ALLOW_CONTEXT_UPDATES = false; - static final boolean BINDINGS_RETRIEVABLE = true; - static final boolean INSTANCES_RETRIEVABLE = true; - static final boolean PLAN_UPDATEABLE = false; - static final boolean SHAREABLE = true; - static final String DESCRIPTION = "description"; - static final String DOCUMENTATION_URL = "/documentation"; - static final String UNIQUE_ID = "unique-id"; - static final String SERVICE_BROKER_GUID = UUID.randomUUID() - .toString(); - private static final Map EXTRA = Map.of("key-metadata", "value-metadata"); - private static final List PLANS = buildTestServiceBindings(); - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedServiceOffering(), buildRawServiceOffering()); - } - - private static CloudServiceOffering buildExpectedServiceOffering() { - return ImmutableCloudServiceOffering.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE) - .brokerId(SERVICE_BROKER_GUID) - .name(NAME) - .isAvailable(AVAILABLE) - .isBindable(BINDABLE) - .isShareable(SHAREABLE) - .description(DESCRIPTION) - .extra(EXTRA) - .docUrl(DOCUMENTATION_URL) - .uniqueId(UNIQUE_ID) - .servicePlans(PLANS) - .build(); - } - - private static RawCloudServiceOffering buildRawServiceOffering() { - return ImmutableRawCloudServiceOffering.builder() - .serviceOffering(buildTestServiceOffering()) - .servicePlans(PLANS) - .build(); - } - - public static ServiceOfferingResource buildTestServiceOffering() { - return ServiceOfferingResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .available(AVAILABLE) - .name(NAME) - .brokerCatalog(BrokerCatalog.builder() - .brokerCatalogId(UNIQUE_ID) - .features(Features.builder() - .bindable(BINDABLE) - .allowContextUpdates(ALLOW_CONTEXT_UPDATES) - .bindingsRetrievable(BINDINGS_RETRIEVABLE) - .instancesRetrievable(INSTANCES_RETRIEVABLE) - .planUpdateable(PLAN_UPDATEABLE) - .build()) - .metadata(EXTRA) - .build()) - .relationships(ServiceOfferingRelationships.builder() - .serviceBroker(ToOneRelationship.builder() - .data(Relationship.builder() - .id(SERVICE_BROKER_GUID) - .build()) - .build()) - .build()) - .shareable(SHAREABLE) - .description(DESCRIPTION) - .documentationUrl(DOCUMENTATION_URL) - .build(); - } - - private static List buildTestServiceBindings() { - return List.of(buildTestServiceBinding()); - } - - private static CloudServicePlan buildTestServiceBinding() { - return ImmutableCloudServicePlan.builder() - .name("v9.4-small") - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServicePlanTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServicePlanTest.java deleted file mode 100644 index f43f5e6a6a..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServicePlanTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.Map; -import java.util.UUID; - -import org.cloudfoundry.client.v3.Relationship; -import org.cloudfoundry.client.v3.ToOneRelationship; -import org.cloudfoundry.client.v3.serviceplans.BrokerCatalog; -import org.cloudfoundry.client.v3.serviceplans.Features; -import org.cloudfoundry.client.v3.serviceplans.Schema; -import org.cloudfoundry.client.v3.serviceplans.Schemas; -import org.cloudfoundry.client.v3.serviceplans.ServiceInstanceSchema; -import org.cloudfoundry.client.v3.serviceplans.ServicePlan; -import org.cloudfoundry.client.v3.serviceplans.ServicePlanRelationships; -import org.cloudfoundry.client.v3.serviceplans.ServicePlanResource; -import org.cloudfoundry.client.v3.serviceplans.Visibility; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServicePlan; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServicePlan; - -public class RawCloudServicePlanTest { - - private static final String NAME = "v9.4-small"; - private static final String DESCRIPTION = "description"; - private static final Map EXTRA = Map.of("extra", "value"); - private static final String UNIQUE_ID = "unique-id"; - private static final boolean PUBLIC = true; - private static final boolean FREE = false; - private static final boolean PLAN_BINDABLE = true; - private static final boolean PLAN_UPDATABLE = true; - private static final boolean AVAILABLE = true; - private static final String SERVICE_OFFERING_ID = UUID.randomUUID() - .toString(); - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedPlan(), buildRawServicePlan()); - } - - private static CloudServicePlan buildExpectedPlan() { - return ImmutableCloudServicePlan.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE) - .name(NAME) - .description(DESCRIPTION) - .extra(EXTRA) - .uniqueId(UNIQUE_ID) - .isPublic(PUBLIC) - .isFree(FREE) - .serviceOfferingId(SERVICE_OFFERING_ID) - .build(); - } - - private static RawCloudServicePlan buildRawServicePlan() { - return ImmutableRawCloudServicePlan.of(buildTestServicePlan(NAME)); - } - - public static ServicePlan buildTestServicePlan(String planName) { - return ServicePlanResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .available(AVAILABLE) - .name(planName) - .description(DESCRIPTION) - .brokerCatalog(BrokerCatalog.builder() - .metadata(EXTRA) - .brokerCatalogId(UNIQUE_ID) - .features(Features.builder() - .bindable(PLAN_BINDABLE) - .planUpdateable(PLAN_UPDATABLE) - .build()) - .build()) - .visibilityType(Visibility.PUBLIC) - .free(FREE) - .relationships(ServicePlanRelationships.builder() - .serviceOffering(ToOneRelationship.builder() - .data(Relationship.builder() - .id(SERVICE_OFFERING_ID) - .build()) - .build()) - .build()) - .schemas(Schemas.builder() - .serviceInstance(ServiceInstanceSchema.builder() - .create(Schema.builder() - .build()) - .build()) - .build()) - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceTest.java deleted file mode 100644 index 1eaf5f98e8..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import java.util.Arrays; -import java.util.List; - -import org.cloudfoundry.client.v3.LastOperation; -import org.cloudfoundry.client.v3.Relationship; -import org.cloudfoundry.client.v3.ToOneRelationship; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceResource; -import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; -import org.cloudfoundry.client.v3.serviceofferings.BrokerCatalog; -import org.cloudfoundry.client.v3.serviceofferings.Features; -import org.cloudfoundry.client.v3.serviceofferings.ServiceOfferingRelationships; -import org.cloudfoundry.client.v3.serviceofferings.ServiceOfferingResource; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceInstance; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceOperation; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceOperation.State; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceOperation.Type; - -class RawCloudServiceTest { - - private static final String NAME = "my-db"; - private static final String OFFERING_NAME = "postgresql"; - private static final String PLAN_NAME = "v9.4-small"; - private static final List TAGS = Arrays.asList("test-tag-1", "test-tag-2"); - private static final String LAST_OPERATION_TYPE_CREATE = "create"; - private static final String LAST_OPERATION_STATE_IN_PROGRESS = "in progress"; - private static final String LAST_OPERATION_STATE_SUCCEEDED = "succeeded"; - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedService(), buildRawService()); - } - - @Test - void testDeriveWithUserProvidedService() { - RawCloudEntityTest.testDerive(buildExpectedUserProvidedService(), buildRawUserProvidedService()); - } - - private static CloudServiceInstance buildExpectedService() { - return ImmutableCloudServiceInstance.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE) - .name(NAME) - .plan(PLAN_NAME) - .label(OFFERING_NAME) - .type(org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType.MANAGED) - .tags(TAGS) - .lastOperation(new ServiceOperation(Type.CREATE, "", State.IN_PROGRESS)) - .build(); - } - - private static CloudServiceInstance buildExpectedUserProvidedService() { - return ImmutableCloudServiceInstance.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE) - .name(NAME) - .type(org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType.USER_PROVIDED) - .tags(TAGS) - .lastOperation(new ServiceOperation(Type.CREATE, "", State.SUCCEEDED)) - .build(); - } - - private static RawCloudServiceInstance buildRawService() { - return ImmutableRawCloudServiceInstance.builder() - .resource(buildTestResource(false, LastOperation.builder() - .type(LAST_OPERATION_TYPE_CREATE) - .state(LAST_OPERATION_STATE_IN_PROGRESS) - .build())) - .servicePlan(RawCloudServicePlanTest.buildTestServicePlan(PLAN_NAME)) - .serviceOffering(buildTestServiceOffering()) - .build(); - } - - private static RawCloudServiceInstance buildRawUserProvidedService() { - return ImmutableRawCloudServiceInstance.builder() - .resource(buildTestResource(true, LastOperation.builder() - .type(LAST_OPERATION_TYPE_CREATE) - .state(LAST_OPERATION_STATE_SUCCEEDED) - .build())) - .build(); - } - - private static ServiceInstanceResource buildTestResource(boolean isUserProvided, LastOperation lastOperation) { - ServiceInstanceResource.Builder serviceInstanceResourceBuilder = ServiceInstanceResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .name(NAME) - .type(ServiceInstanceType.MANAGED) - .addAllTags(TAGS) - .lastOperation(lastOperation); - if (isUserProvided) { - serviceInstanceResourceBuilder.type(ServiceInstanceType.USER_PROVIDED); - } - return serviceInstanceResourceBuilder.build(); - } - - private static ServiceOfferingResource buildTestServiceOffering() { - return ServiceOfferingResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .available(RawCloudServiceOfferingTest.AVAILABLE) - .name(OFFERING_NAME) - .brokerCatalog(BrokerCatalog.builder() - .brokerCatalogId(RawCloudServiceOfferingTest.UNIQUE_ID) - .features(Features.builder() - .bindable(RawCloudServiceOfferingTest.BINDABLE) - .allowContextUpdates(RawCloudServiceOfferingTest.ALLOW_CONTEXT_UPDATES) - .bindingsRetrievable(RawCloudServiceOfferingTest.BINDINGS_RETRIEVABLE) - .instancesRetrievable(RawCloudServiceOfferingTest.INSTANCES_RETRIEVABLE) - .planUpdateable(RawCloudServiceOfferingTest.PLAN_UPDATEABLE) - .build()) - .build()) - .relationships(ServiceOfferingRelationships.builder() - .serviceBroker(ToOneRelationship.builder() - .data(Relationship.builder() - .id(RawCloudServiceOfferingTest.SERVICE_BROKER_GUID) - .build()) - .build()) - .build()) - .shareable(RawCloudServiceOfferingTest.SHAREABLE) - .description(RawCloudServiceOfferingTest.DESCRIPTION) - .documentationUrl(RawCloudServiceOfferingTest.DOCUMENTATION_URL) - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudStackTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudStackTest.java deleted file mode 100644 index ac4da5f8b2..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudStackTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.client.v3.stacks.Stack; -import org.cloudfoundry.client.v3.stacks.StackResource; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudStack; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudStack; - -class RawCloudStackTest { - - private static final String NAME = "cflinuxfs3"; - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedStack(), buildRawStack()); - } - - private static CloudStack buildExpectedStack() { - return ImmutableCloudStack.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_V3) - .name(NAME) - .build(); - } - - private static RawCloudStack buildRawStack() { - return ImmutableRawCloudStack.of(buildTestResource()); - } - - private static Stack buildTestResource() { - return StackResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .metadata(RawCloudEntityTest.V3_METADATA) - .name(NAME) - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudTaskTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudTaskTest.java deleted file mode 100644 index 9d69a9da4c..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudTaskTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.client.v3.tasks.Result; -import org.cloudfoundry.client.v3.tasks.Task; -import org.cloudfoundry.client.v3.tasks.TaskResource; -import org.cloudfoundry.client.v3.tasks.TaskState; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudTask; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudTask; - -class RawCloudTaskTest { - - private static final String NAME = "echo"; - private static final String COMMAND = "echo \"Hello!\""; - private static final int DISK_IN_MB = 128; - private static final int MEMORY_IN_MB = 256; - private static final String FAILURE_REASON = "blabla"; - private static final String DROPLET_ID = "8ff24465-823b-4a55-85ee-a680f2c743cd"; - private static final int SEQUENCE_ID = 3; - private static final TaskState STATE = TaskState.FAILED; - - private static final CloudTask.State EXPECTED_STATE = CloudTask.State.FAILED; - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedTask(), buildRawTask()); - } - - private static CloudTask buildExpectedTask() { - return ImmutableCloudTask.builder() - .metadata(RawCloudEntityTest.EXPECTED_METADATA_PARSED_FROM_V3_RESOURCE) - .name(NAME) - .command(COMMAND) - .limits(ImmutableCloudTask.ImmutableLimits.builder() - .disk(DISK_IN_MB) - .memory(MEMORY_IN_MB) - .build()) - .result(ImmutableCloudTask.ImmutableResult.builder() - .failureReason(FAILURE_REASON) - .build()) - .state(EXPECTED_STATE) - .build(); - } - - private static RawCloudTask buildRawTask() { - return ImmutableRawCloudTask.of(buildTestResource()); - } - - private static Task buildTestResource() { - return TaskResource.builder() - .id(RawCloudEntityTest.GUID_STRING) - .createdAt(RawCloudEntityTest.CREATED_AT_STRING) - .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) - .name(NAME) - .command(COMMAND) - .diskInMb(DISK_IN_MB) - .memoryInMb(MEMORY_IN_MB) - .state(STATE) - .result(buildTestResult()) - .dropletId(DROPLET_ID) - .sequenceId(SEQUENCE_ID) - .build(); - } - - private static Result buildTestResult() { - return Result.builder() - .failureReason(FAILURE_REASON) - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawInstancesInfoTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawInstancesInfoTest.java deleted file mode 100644 index 2051f7a207..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawInstancesInfoTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.client.v3.applications.GetApplicationProcessStatisticsResponse; -import org.cloudfoundry.client.v3.processes.ProcessState; -import org.cloudfoundry.client.v3.processes.ProcessStatisticsResource; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableInstanceInfo; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableInstancesInfo; -import org.cloudfoundry.multiapps.controller.client.facade.domain.InstanceState; -import org.cloudfoundry.multiapps.controller.client.facade.domain.InstancesInfo; -import org.junit.jupiter.api.Test; - -class RawInstancesInfoTest { - - @Test - void testDeriveWithTrueRoutable() { - RawCloudEntityTest.testDerive(buildExpectedInstancesInfo(true), buildActualInstancesInfo("true")); - } - - @Test - void testDeriveWithFalseRoutable() { - RawCloudEntityTest.testDerive(buildExpectedInstancesInfo(false), buildActualInstancesInfo("false")); - } - - private InstancesInfo buildExpectedInstancesInfo(boolean expectedRoutable) { - return ImmutableInstancesInfo.builder() - .addInstance(ImmutableInstanceInfo.builder() - .index(0) - .state(InstanceState.RUNNING) - .isRoutable(expectedRoutable) - .build()) - .build(); - } - - private RawInstancesInfo buildActualInstancesInfo(String routable) { - return ImmutableRawInstancesInfo.builder() - .processStatisticsResponse(getApplicationProcessStatisticsResponse(routable)) - .build(); - } - - private GetApplicationProcessStatisticsResponse getApplicationProcessStatisticsResponse(String routable) { - return GetApplicationProcessStatisticsResponse.builder() - .resource(ProcessStatisticsResource.builder() - .index(0) - .details("Instance is in running state") - .diskQuota(1024L) - .state(ProcessState.RUNNING) - .memoryQuota(1024L) - .type("web") - .uptime(9042L) - .fileDescriptorQuota(1024L) - .routable(routable) - .host("10.244.16.10") - .build()) - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawUserRoleTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawUserRoleTest.java deleted file mode 100644 index bc650d4603..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawUserRoleTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.adapters; - -import org.cloudfoundry.client.v3.Relationship; -import org.cloudfoundry.client.v3.ToOneRelationship; -import org.cloudfoundry.client.v3.roles.RoleRelationships; -import org.cloudfoundry.client.v3.roles.RoleResource; -import org.cloudfoundry.client.v3.roles.RoleType; -import org.junit.jupiter.api.Test; - -import org.cloudfoundry.multiapps.controller.client.facade.domain.UserRole; - -class RawUserRoleTest { - - private static final String USER_ROLE_GUID = "c94b4588-3431-11ed-a261-0242ac120002"; - private static final String USER_GUID = "328eaf4e-3432-11ed-a261-0242ac120002"; - private static final String DATE = "2022-01-01T13:35:11Z"; - private static final RoleType USER_ROLE_TYPE = RoleType.SPACE_DEVELOPER; - - @Test - void testDerive() { - RawCloudEntityTest.testDerive(buildExpectedUserRole(), buildActualUserRole()); - } - - private UserRole buildExpectedUserRole() { - return UserRole.SPACE_DEVELOPER; - } - - private RawUserRole buildActualUserRole() { - return ImmutableRawUserRole.builder() - .roleResource(buildRoleResource()) - .build(); - } - - private RoleResource buildRoleResource() { - return RoleResource.builder() - .createdAt(DATE) - .id(USER_ROLE_GUID) - .type(USER_ROLE_TYPE) - .relationships(RoleRelationships.builder() - .user(ToOneRelationship.builder() - .data(Relationship.builder() - .id(USER_GUID) - .build()) - .build()) - .build()) - .build(); - } - -} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientImplTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientImplTest.java deleted file mode 100644 index 0519cbecce..0000000000 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientImplTest.java +++ /dev/null @@ -1,309 +0,0 @@ -package org.cloudfoundry.multiapps.controller.client.facade.rest; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import java.util.stream.Stream; - -import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.client.v3.BuildpackData; -import org.cloudfoundry.client.v3.ClientV3Exception; -import org.cloudfoundry.client.v3.Lifecycle; -import org.cloudfoundry.client.v3.LifecycleType; -import org.cloudfoundry.client.v3.Pagination; -import org.cloudfoundry.client.v3.applications.ApplicationResource; -import org.cloudfoundry.client.v3.applications.ApplicationState; -import org.cloudfoundry.client.v3.applications.ApplicationsV3; -import org.cloudfoundry.client.v3.applications.GetApplicationRequest; -import org.cloudfoundry.client.v3.applications.GetApplicationResponse; -import org.cloudfoundry.client.v3.applications.ListApplicationsRequest; -import org.cloudfoundry.client.v3.applications.ListApplicationsResponse; -import org.cloudfoundry.client.v3.applications.UpdateApplicationRequest; -import org.cloudfoundry.client.v3.applications.UpdateApplicationResponse; -import org.cloudfoundry.client.v3.serviceofferings.GetServiceOfferingRequest; -import org.cloudfoundry.client.v3.serviceofferings.GetServiceOfferingResponse; -import org.cloudfoundry.client.v3.serviceofferings.ServiceOffering; -import org.cloudfoundry.client.v3.serviceofferings.ServiceOfferingsV3; -import org.cloudfoundry.client.v3.serviceplans.GetServicePlanRequest; -import org.cloudfoundry.client.v3.serviceplans.GetServicePlanResponse; -import org.cloudfoundry.client.v3.serviceplans.ServicePlan; -import org.cloudfoundry.client.v3.serviceplans.ServicePlansV3; -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.Mockito; -import org.mockito.MockitoAnnotations; -import org.springframework.http.HttpStatus; - -import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.RawCloudServiceOfferingTest; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.RawCloudServicePlanTest; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudSpace; - -import reactor.core.publisher.Mono; - -class CloudControllerRestClientImplTest { - - private static final String SERVICE_PLAN_GUID = "1803e5a7-40c7-438e-b2be-e2045c9b7cda"; - private static final String SERVICE_INSTANCE_GUID = "26949ebb-a624-35c0-000-1110a01f1880"; - private static final String SERVICE_OFFERING_GUID = "1803e5a7-40c7-438e-b2be-e2045c9b7cda"; - private static final String PLAN_NAME = "test-plan"; - private static final String OLD_APPLICATION_NAME = "old-app-name"; - private static final String NEW_APPLICATION_NAME = "new-app-name"; - private static final String APPLICATION_CREATED_AT = "2024-01-26T10:00:00"; - private static final UUID APPLICATION_GUID = UUID.randomUUID(); - private static final UUID SPACE_GUID = UUID.randomUUID(); - - @Mock - private CloudFoundryClient delegate; - private CloudControllerRestClientImpl controllerClient; - - @BeforeEach - public void setUp() throws Exception { - MockitoAnnotations.openMocks(this) - .close(); - controllerClient = new CloudControllerRestClientImpl(delegate); - } - - @Test - void testGetServiceOffering() { - GetServiceOfferingRequest request = GetServiceOfferingRequest.builder() - .serviceOfferingId(SERVICE_PLAN_GUID) - .build(); - GetServiceOfferingResponse response = GetServiceOfferingResponse.builder() - .from(RawCloudServiceOfferingTest.buildTestServiceOffering()) - .build(); - - ServiceOfferingsV3 serviceOfferingsV3 = Mockito.mock(ServiceOfferingsV3.class); - Mockito.when(delegate.serviceOfferingsV3()) - .thenReturn(serviceOfferingsV3); - Mockito.when(serviceOfferingsV3.get(request)) - .thenReturn(Mono.just(response)); - - ServiceOffering serviceOffering = controllerClient.getServiceOffering(SERVICE_PLAN_GUID) - .block(); - - assertEquals(response, serviceOffering); - } - - public static Stream testGetServiceOfferingWithError() { - return Stream.of( -// @formatter:off - Arguments.of(HttpStatus.FORBIDDEN.value(), - "403 Forbidden: Service offering with guid \"1803e5a7-40c7-438e-b2be-e2045c9b7cda\" is not available."), - Arguments.of(HttpStatus.NOT_FOUND.value(), - "404 Not Found: Service offering with guid \"1803e5a7-40c7-438e-b2be-e2045c9b7cda\" not found.") -// @formatter:on - ); - } - - @ParameterizedTest - @MethodSource - void testGetServiceOfferingWithError(int errorCode, String expectedErrorMessage) { - GetServiceOfferingRequest request = GetServiceOfferingRequest.builder() - .serviceOfferingId(SERVICE_OFFERING_GUID) - .build(); - ServiceOfferingsV3 serviceOfferingsV3 = Mockito.mock(ServiceOfferingsV3.class); - Mockito.when(delegate.serviceOfferingsV3()) - .thenReturn(serviceOfferingsV3); - Mockito.when(serviceOfferingsV3.get(request)) - .thenReturn(Mono.error(clientV3Exception(errorCode))); - Exception cloudControllerException = assertThrows(CloudOperationException.class, - () -> controllerClient.getServiceOffering(SERVICE_OFFERING_GUID) - .block()); - assertEquals(expectedErrorMessage, cloudControllerException.getMessage()); - } - - public static Stream testGetServicePlanWithError() { - return Stream.of( -// @formatter:off - Arguments.of(HttpStatus.FORBIDDEN.value(), - "403 Forbidden: Service plan with guid \"1803e5a7-40c7-438e-b2be-e2045c9b7cda\" is not available for service instance \"26949ebb-a624-35c0-000-1110a01f1880\"."), - Arguments.of(HttpStatus.NOT_FOUND.value(), - "404 Not Found: Service plan with guid \"1803e5a7-40c7-438e-b2be-e2045c9b7cda\" for service instance with name \"26949ebb-a624-35c0-000-1110a01f1880\" was not found.") -// @formatter:on - ); - } - - @ParameterizedTest - @MethodSource - void testGetServicePlanWithError(int errorCode, String expectedErrorMessage) { - GetServicePlanRequest request = GetServicePlanRequest.builder() - .servicePlanId(SERVICE_PLAN_GUID) - .build(); - - ServicePlansV3 servicePlans = Mockito.mock(ServicePlansV3.class); - Mockito.when(delegate.servicePlansV3()) - .thenReturn(servicePlans); - Mockito.when(servicePlans.get(request)) - .thenReturn(Mono.error(clientV3Exception(errorCode))); - - Exception cloudControllerException = assertThrows(CloudOperationException.class, - () -> controllerClient.getServicePlanResource(SERVICE_PLAN_GUID, - SERVICE_INSTANCE_GUID) - .block()); - assertEquals(expectedErrorMessage, cloudControllerException.getMessage()); - } - - @Test - void testGetServicePlanResource() { - GetServicePlanRequest request = GetServicePlanRequest.builder() - .servicePlanId(SERVICE_PLAN_GUID) - .build(); - GetServicePlanResponse response = GetServicePlanResponse.builder() - .from(RawCloudServicePlanTest.buildTestServicePlan(PLAN_NAME)) - .build(); - - ServicePlansV3 servicePlans = Mockito.mock(ServicePlansV3.class); - Mockito.when(delegate.servicePlansV3()) - .thenReturn(servicePlans); - Mockito.when(servicePlans.get(request)) - .thenReturn(Mono.just(response)); - - ServicePlan servicePlanResource = controllerClient.getServicePlanResource(SERVICE_PLAN_GUID, SERVICE_INSTANCE_GUID) - .block(); - - assertEquals(response, servicePlanResource); - } - - private static Stream testRenameApplication() { - return Stream.of( - // (1) Successful application rename - Arguments.of(Mono.just(UpdateApplicationResponse.builder() - .name(NEW_APPLICATION_NAME) - .id(APPLICATION_GUID.toString()) - .createdAt(APPLICATION_CREATED_AT) - .state(ApplicationState.STARTED) - .lifecycle(getApplicationLifecycle()) - .build()), - null, false), - // (2) Controller's API returns 503 and rename was actually done - Arguments.of(Mono.error(new ClientV3Exception(503, - List.of(org.cloudfoundry.client.v3.Error.builder() - .code(170015) - .title("CF-RunnerUnavailable") - .detail("Runner is unavailable") - .build()))), - Mono.just(GetApplicationResponse.builder() - .name(NEW_APPLICATION_NAME) - .id(APPLICATION_GUID.toString()) - .createdAt(APPLICATION_CREATED_AT) - .state(ApplicationState.STARTED) - .lifecycle(getApplicationLifecycle()) - .build()), - false), - // (3) Controller's API fails with 500 and application wasn't renamed - Arguments.of(Mono.error(new ClientV3Exception(500, Collections.emptyList())), null, true), - // (4) Controller's API returns 503 and while make request to get current application name the request fails - Arguments.of(Mono.error(new ClientV3Exception(503, - List.of(org.cloudfoundry.client.v3.Error.builder() - .code(170015) - .title("CF-RunnerUnavailable") - .detail("Runner is unavailable") - .build()))), - Mono.error(new ClientV3Exception(500, Collections.emptyList())), true), - // (5) Controller's API returns 503 and application wasn't renamed - Arguments.of(Mono.error(new ClientV3Exception(503, - List.of(org.cloudfoundry.client.v3.Error.builder() - .code(170015) - .title("CF-RunnerUnavailable") - .detail("Runner is unavailable") - .build()))), - Mono.just(GetApplicationResponse.builder() - .name(OLD_APPLICATION_NAME) - .id(APPLICATION_GUID.toString()) - .createdAt(APPLICATION_CREATED_AT) - .state(ApplicationState.STARTED) - .lifecycle(getApplicationLifecycle()) - .build()), - true)); - } - - private static Lifecycle getApplicationLifecycle() { - return Lifecycle.builder() - .type(LifecycleType.BUILDPACK) - .data(BuildpackData.builder() - .build()) - .build(); - } - - @ParameterizedTest - @MethodSource - void testRenameApplication(Mono updateApplicationResponse, - Mono getApplicationResponse, boolean expectedFailure) { - ApplicationsV3 applicationsV3 = Mockito.mock(ApplicationsV3.class); - var updateAppRequest = UpdateApplicationRequest.builder() - .applicationId(APPLICATION_GUID.toString()) - .name(NEW_APPLICATION_NAME) - .build(); - prepareClientDelegate(applicationsV3, updateAppRequest, updateApplicationResponse, getApplicationResponse); - - if (expectedFailure) { - assertThrows(ClientV3Exception.class, () -> controllerClient.rename(OLD_APPLICATION_NAME, NEW_APPLICATION_NAME)); - return; - } - controllerClient.rename(OLD_APPLICATION_NAME, NEW_APPLICATION_NAME); - - Mockito.verify(applicationsV3, Mockito.atMostOnce()) - .update(updateAppRequest); - } - - private void prepareClientDelegate(ApplicationsV3 applicationsV3, UpdateApplicationRequest updateAppRequest, - Mono updateApplicationResponse, - Mono getApplicationResponse) { - initControllerClientWithTargetSpace(SPACE_GUID); - - Mockito.when(delegate.applicationsV3()) - .thenReturn(applicationsV3); - var listAppsRequest = ListApplicationsRequest.builder() - .spaceId(SPACE_GUID.toString()) - .name(OLD_APPLICATION_NAME) - .page(1) - .build(); - - var listAppsResponse = ListApplicationsResponse.builder() - .resource(ApplicationResource.builder() - .name(OLD_APPLICATION_NAME) - .id(APPLICATION_GUID.toString()) - .createdAt(APPLICATION_CREATED_AT) - .state(ApplicationState.STARTED) - .lifecycle(getApplicationLifecycle()) - .build()) - .pagination(Pagination.builder() - .totalResults(1) - .totalPages(1) - .build()) - .build(); - Mockito.when(applicationsV3.list(listAppsRequest)) - .thenReturn(Mono.just(listAppsResponse)); - Mockito.when(applicationsV3.update(updateAppRequest)) - .thenReturn(updateApplicationResponse); - var getAppRequest = GetApplicationRequest.builder() - .applicationId(APPLICATION_GUID.toString()) - .build(); - - Mockito.when(applicationsV3.get(getAppRequest)) - .thenReturn(getApplicationResponse); - } - - private void initControllerClientWithTargetSpace(UUID spaceGuid) { - CloudSpace target = ImmutableCloudSpace.builder() - .metadata(ImmutableCloudMetadata.builder() - .guid(spaceGuid) - .build()) - .build(); - this.controllerClient = new CloudControllerRestClientImpl(delegate, target); - } - - private ClientV3Exception clientV3Exception(int statusCode) { - return new ClientV3Exception(statusCode, Collections.emptyList()); - } -} diff --git a/multiapps-controller-core/pom.xml b/multiapps-controller-core/pom.xml index a7f67cbb8a..49c37e727a 100644 --- a/multiapps-controller-core/pom.xml +++ b/multiapps-controller-core/pom.xml @@ -103,6 +103,14 @@ multiapps-controller-core-test test
+ + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + test + org.cloudfoundry.multiapps multiapps-controller-client diff --git a/multiapps-controller-core/src/main/java/module-info.java b/multiapps-controller-core/src/main/java/module-info.java index b00243ee13..d24aaceea8 100644 --- a/multiapps-controller-core/src/main/java/module-info.java +++ b/multiapps-controller-core/src/main/java/module-info.java @@ -45,8 +45,6 @@ requires transitive org.cloudfoundry.multiapps.controller.client; requires transitive org.cloudfoundry.multiapps.controller.persistence; requires transitive org.cloudfoundry.multiapps.mta; - - requires org.cloudfoundry.client; requires com.fasterxml.jackson.annotation; requires com.fasterxml.jackson.core; requires com.fasterxml.jackson.databind; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/CloudControllerClientFactory.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/CloudControllerClientFactory.java index 91fd2fe929..07a0a572b6 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/CloudControllerClientFactory.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/CloudControllerClientFactory.java @@ -68,8 +68,7 @@ public CloudControllerClient createClient(OAuth2AccessTokenWithAdditionalInfo to CloudCredentials credentials = createCredentials(token); oAuthClient.init(credentials); - var spaceClient = clientFactory.getCloudFoundryClientFactory() - .createSpaceClient(configuration.getControllerUrl(), oAuthClient, requestTags); + var spaceClient = clientFactory.createSpaceClient(configuration.getControllerUrl(), oAuthClient, requestTags); CloudSpace target = spaceClient.getSpace(UUID.fromString(spaceId)); CloudControllerRestClient controllerClient = clientFactory.createClient(configuration.getControllerUrl(), credentials, target, @@ -82,8 +81,7 @@ public CloudSpaceClient createSpaceClient(OAuth2AccessTokenWithAdditionalInfo to CloudCredentials credentials = createCredentials(token); oAuthClient.init(credentials); var requestTags = buildRequestTags(StringUtils.EMPTY); - return clientFactory.getCloudFoundryClientFactory() - .createSpaceClient(configuration.getControllerUrl(), oAuthClient, requestTags); + return clientFactory.createSpaceClient(configuration.getControllerUrl(), oAuthClient, requestTags); } public LogCacheClient createLogCacheClient(OAuth2AccessTokenWithAdditionalInfo token, String correlationId) { @@ -91,8 +89,7 @@ public LogCacheClient createLogCacheClient(OAuth2AccessTokenWithAdditionalInfo t CloudCredentials credentials = createCredentials(token); oAuthClient.init(credentials); var requestTags = buildRequestTags(correlationId); - return clientFactory.getCloudFoundryClientFactory() - .createLogCacheClient(configuration.getControllerUrl(), oAuthClient, requestTags); + return clientFactory.createLogCacheClient(configuration.getControllerUrl(), oAuthClient, requestTags); } private CloudCredentials createCredentials(OAuth2AccessTokenWithAdditionalInfo token) { diff --git a/multiapps-controller-process/src/main/java/module-info.java b/multiapps-controller-process/src/main/java/module-info.java index 6b87de4bd9..36fdbb9c69 100644 --- a/multiapps-controller-process/src/main/java/module-info.java +++ b/multiapps-controller-process/src/main/java/module-info.java @@ -22,8 +22,6 @@ requires transitive flowable.engine; requires transitive org.cloudfoundry.multiapps.controller.api; requires transitive org.cloudfoundry.multiapps.controller.core; - - requires org.cloudfoundry.client; requires com.fasterxml.jackson.core; requires com.fasterxml.jackson.databind; requires com.google.common; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/jobs/OrphanedDataCleaner.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/jobs/OrphanedDataCleaner.java index ef348f7181..73cd64bc99 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/jobs/OrphanedDataCleaner.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/jobs/OrphanedDataCleaner.java @@ -8,7 +8,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; -import org.cloudfoundry.multiapps.controller.client.facade.adapters.ImmutableCloudFoundryClientFactory; +import org.cloudfoundry.multiapps.controller.client.facade.rest.ImmutableCloudControllerRestClientFactory; import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudSpaceClient; import org.cloudfoundry.multiapps.controller.core.auditlogging.MtaConfigurationPurgerAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.OAuthClientFactory; @@ -97,7 +97,7 @@ protected void initSpaceClient() { CloudCredentials cloudCredentials = new CloudCredentials(configuration.getGlobalAuditorUser(), configuration.getGlobalAuditorPassword(), SecurityUtil.CLIENT_ID, SecurityUtil.CLIENT_SECRET, configuration.getGlobalAuditorOrigin()); - var clientFactory = ImmutableCloudFoundryClientFactory.builder() + var clientFactory = ImmutableCloudControllerRestClientFactory.builder() .connectTimeout(Duration.ofMinutes(5)) .responseTimeout(Duration.ofMinutes(5)) .connectionPoolSize(1) diff --git a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/upload/UploadFromUrlContext.java b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/upload/UploadFromUrlContext.java index c1ef04215d..18134dd785 100644 --- a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/upload/UploadFromUrlContext.java +++ b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/upload/UploadFromUrlContext.java @@ -4,7 +4,7 @@ import java.util.concurrent.atomic.AtomicLong; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.cloudfoundry.Nullable; +import org.cloudfoundry.multiapps.controller.client.facade.Nullable; import org.cloudfoundry.multiapps.controller.api.model.UserCredentials; import org.cloudfoundry.multiapps.controller.persistence.model.AsyncUploadJobEntry; import org.immutables.value.Value; diff --git a/pom.xml b/pom.xml index 87d54ee76e..8d82fefe5b 100644 --- a/pom.xml +++ b/pom.xml @@ -35,7 +35,6 @@ 2.22.2 4.33.0 5.1.0 - 5.16.0.RELEASE 1.3.7 4.2.17.Final 1.6.16 @@ -59,12 +58,12 @@ 1.28.0 3.3.2 1.3.6 + 0.12.6 2.70.0 0.134.0 12.35.0 1.0.4 4.0.1 - 6.3.0 multiapps-controller-client @@ -825,13 +824,6 @@ resilience4j-ratelimiter ${resilience4j.version} - - - - com.squareup.wire - wire-runtime - ${wire-runtime.version} - org.bouncycastle @@ -850,12 +842,6 @@ java-cfenv-jdbc ${java-cfenv.version} - - - org.cloudfoundry - cloudfoundry-client-reactor - ${cloudfoundry-client.version} - From f273ed55040882aa313a35985772a2e5b51c8a67 Mon Sep 17 00:00:00 2001 From: Krasimir Kargov Date: Mon, 10 Aug 2026 14:10:34 +0300 Subject: [PATCH 3/6] Adding reactor-netty back not completed LMCROSSITXSADEPLOY-3563 --- .../CloudControllerRestClientBuilder.java | 145 ++++++++++++++---- .../CloudControllerRestClientFactory.java | 74 ++++++--- .../rest/CloudControllerRestClientV3Impl.java | 15 +- .../facade/rest/CloudControllerV3Client.java | 82 +++++++++- .../facade/rest/PackagesV3Operations.java | 33 +++- .../client/facade/rest/ReactiveFanOut.java | 53 +++++++ .../rest/ServiceInstancesV3Operations.java | 7 +- .../facade/rest/ServiceKeysV3Operations.java | 10 +- .../CloudControllerV3ClientPageUriTest.java | 40 +++++ .../facade/rest/ReactiveFanOutTest.java | 108 +++++++++++++ 10 files changed, 494 insertions(+), 73 deletions(-) create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOut.java create mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3ClientPageUriTest.java create mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOutTest.java diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java index bcf7f5e322..65d75efb9e 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java @@ -1,57 +1,71 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; import java.net.URL; -import java.net.http.HttpClient; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Optional; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import org.springframework.http.HttpHeaders; -import org.springframework.http.client.JdkClientHttpRequestFactory; +import org.springframework.http.client.ReactorClientHttpRequestFactory; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.client.RestClient; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.WebClient; import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.channel.ChannelOption; +import io.netty.handler.ssl.SslContextBuilder; import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; /** * Builds the blocking Spring {@link RestClient} used by {@link CloudControllerRestClientV3Impl} to talk to the Cloud Controller v3 * REST API directly, without the OSS cf-java-client. *

- * The transport is the JDK {@link java.net.http.HttpClient} (via {@link JdkClientHttpRequestFactory}). This is deliberately chosen over - * reactor-netty: it is already available through {@code spring-web} and the JDK, so it pulls no extra transitive dependencies - * (no reactor-netty, no spring-webflux) into the deploy-service WAR — which is exactly the dependency-shedding this migration is about. - * The JDK client provides connection reuse (HTTP/2 multiplexing + keep-alive) and connect/read timeouts; TLS trust is configurable for - * self-signed landscapes. Every request carries a bearer token from {@link OAuthClient} (refreshed as needed), and non-2xx responses are - * mapped to {@code CloudOperationException} by the existing {@link CloudControllerResponseErrorHandler}. + * The transport is Reactor-Netty (via Spring's {@link ReactorClientHttpRequestFactory}, which lives in {@code spring-web} — NOT + * {@code spring-webflux}). This is the same HTTP engine the deploy-service ran on under the OSS client, so its proven behavior + * (connection pooling, TLS, timeouts, proxy handling on the target landscapes) is preserved; only the cf-java-client library on top of it + * was removed. The tuning knobs the OSS {@code DefaultConnectionContext} honored are restored here: pool size (via {@link ConnectionProvider}), + * connect timeout, and response timeout. The caller still consumes everything synchronously through {@link RestClient} — Reactor-Netty is + * used purely as the transport, exactly as before. + *

+ * Every request carries a bearer token from {@link OAuthClient} (refreshed as needed), and non-2xx responses are mapped to + * {@code CloudOperationException} by the existing {@link CloudControllerResponseErrorHandler}. */ public final class CloudControllerRestClientBuilder { private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofMinutes(1); + private static final int DEFAULT_CONNECTION_POOL_SIZE = 192; + private static final String CONNECTION_POOL_NAME = "cf-controller-client"; private CloudControllerRestClientBuilder() { } public static RestClient build(URL v3ApiUrl, OAuthClient oAuthClient, Map requestTags, TransportOptions options) { - JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(buildHttpClient(options)); - options.responseTimeout() - .ifPresent(requestFactory::setReadTimeout); + return buildRestClient(buildHttpClient(options), v3ApiUrl, oAuthClient, requestTags); + } + + /** + * Build a blocking {@link RestClient} on the given (already-configured) Reactor-Netty {@link HttpClient}. Sharing one + * {@code HttpClient} between the RestClient and the {@link #buildWebClient WebClient} means both ride the same connection pool, + * timeouts, TLS and proxy settings. + */ + public static RestClient buildRestClient(HttpClient httpClient, URL baseUrl, OAuthClient oAuthClient, + Map requestTags) { return RestClient.builder() - .baseUrl(v3ApiUrl.toString()) - .requestFactory(requestFactory) + .baseUrl(baseUrl.toString()) + .requestFactory(new ReactorClientHttpRequestFactory(httpClient)) // Pass a fully-formed converter list via the List overload — deliberately NOT the Consumer overload. // messageConverters(Consumer) first calls initMessageConverters() to build Spring's DEFAULT list and only then // hands it to the consumer; that default init eagerly constructs AllEncompassingFormHttpMessageConverter -> @@ -80,23 +94,86 @@ public static RestClient build(URL v3ApiUrl, OAuthClient oAuthClient, Map requestTags) { + return WebClient.builder() + .baseUrl(baseUrl.toString()) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .filter(authAndTagsFilter(oAuthClient, requestTags)) + .build(); + } + + private static ExchangeFilterFunction authAndTagsFilter(OAuthClient oAuthClient, Map requestTags) { + return (request, next) -> { + ClientRequest.Builder builder = ClientRequest.from(request); + String authorization = oAuthClient.getAuthorizationHeaderValue(); + if (authorization != null) { + builder.header(HttpHeaders.AUTHORIZATION, authorization); + } + requestTags.forEach(builder::header); + return next.exchange(builder.build()); + }; + } + + static HttpClient buildHttpClient(TransportOptions options) { + int poolSize = options.connectionPoolSize() + .orElse(DEFAULT_CONNECTION_POOL_SIZE); + ConnectionProvider connectionProvider = ConnectionProvider.builder(CONNECTION_POOL_NAME) + .maxConnections(poolSize) + .build(); + HttpClient httpClient = HttpClient.create(connectionProvider) + .followRedirect(true) + // Honor JVM standard proxy settings (-Dhttp(s).proxyHost/proxyPort/nonProxyHosts), matching the + // OSS DefaultConnectionContext's proxy support. No-op when no proxy is configured, so it is safe + // on direct-connect landscapes and required on proxied ones. + .proxyWithSystemProperties(); + int connectTimeoutMillis = (int) options.connectTimeout() + .orElse(DEFAULT_CONNECT_TIMEOUT) + .toMillis(); + httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMillis); + Optional responseTimeout = options.responseTimeout(); + if (responseTimeout.isPresent()) { + httpClient = httpClient.responseTimeout(responseTimeout.get()); + } + Optional handshakeTimeout = options.sslHandshakeTimeout(); if (options.trustSelfSignedCertificates()) { - builder.sslContext(buildTrustAllSslContext()); + httpClient = httpClient.secure(spec -> { + var sslBuilder = spec.sslContext(buildTrustAllSslContext()); + handshakeTimeout.ifPresent(sslBuilder::handshakeTimeout); + }); + } else if (handshakeTimeout.isPresent()) { + httpClient = httpClient.secure(spec -> { + var sslBuilder = spec.sslContext(defaultClientSslContext()); + sslBuilder.handshakeTimeout(handshakeTimeout.get()); + }); + } else { + httpClient = httpClient.secure(); + } + return httpClient; + } + + private static io.netty.handler.ssl.SslContext defaultClientSslContext() { + try { + return SslContextBuilder.forClient() + .build(); + } catch (javax.net.ssl.SSLException e) { + throw new IllegalStateException("An error occurred setting up the default SSLContext", e); } - return builder.build(); } - private static SSLContext buildTrustAllSslContext() { + private static io.netty.handler.ssl.SslContext buildTrustAllSslContext() { try { - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, new TrustManager[] { trustAllManager() }, new SecureRandom()); - return sslContext; - } catch (NoSuchAlgorithmException | KeyManagementException e) { + return SslContextBuilder.forClient() + .trustManager(trustAllManager()) + .build(); + } catch (javax.net.ssl.SSLException e) { throw new IllegalStateException("An error occurred setting up the SSLContext", e); } } @@ -123,11 +200,13 @@ public X509Certificate[] getAcceptedIssuers() { } /** - * The transport-tuning surface carried over from the OSS-backed factory, so operators keep the same knobs. {@code connectionPoolSize} - * is accepted for parity but the JDK client manages connection reuse internally; it is retained for a future backend swap. + * The transport-tuning surface, honored by the Reactor-Netty transport: {@code connectTimeout} and {@code responseTimeout} on the + * client, {@code sslHandshakeTimeout} on the TLS layer, {@code connectionPoolSize} on the {@link ConnectionProvider}. Mirrors the + * knobs the OSS {@code DefaultConnectionContext} exposed. */ public record TransportOptions(Optional connectTimeout, Optional responseTimeout, - Optional connectionPoolSize, boolean trustSelfSignedCertificates) { + Optional sslHandshakeTimeout, Optional connectionPoolSize, + boolean trustSelfSignedCertificates) { } } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java index fa394c5925..1a2607a32c 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java @@ -11,17 +11,20 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.client.RestClient; +import org.springframework.web.reactive.function.client.WebClient; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; -import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.Collections; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import reactor.netty.http.client.HttpClient; /** * Builds the in-house {@link CloudControllerRestClient} (and the auxiliary {@link CloudSpaceClient} / {@link LogCacheClient}) that talk to @@ -33,6 +36,10 @@ public abstract class CloudControllerRestClientFactory { private static final Logger LOGGER = LoggerFactory.getLogger(CloudControllerRestClientFactory.class); private final RestUtil restUtil = new RestUtil(); + // Per-host transport cache — mirrors the OSS connectionContextCache. One shared reactor-netty HttpClient (pool + TLS) and one root-link + // discovery per Cloud Controller host, reused across the CF / space / log-cache clients. The per-call OAuth token and request tags are + // NOT cached here; they are applied per RestClient/WebClient build (cheap), so caching the transport is safe. + private final Map hostTransports = new ConcurrentHashMap<>(); public abstract Optional getSslHandshakeTimeout(); @@ -64,28 +71,54 @@ public CloudControllerRestClient createClient(URL controllerUrl, CloudCredential public CloudControllerRestClient createClient(URL controllerUrl, CloudCredentials credentials, CloudSpace target, OAuthClient oAuthClient, Map requestTags) { oAuthClient.init(credentials); - URL v3ApiUrl = deriveV3ApiUrl(controllerUrl, requestTags); - RestClient restClient = buildRestClient(toOrigin(v3ApiUrl), oAuthClient, requestTags); - return new CloudControllerRestClientV3Impl(v3ApiUrl, oAuthClient, target, restClient); + HostTransport transport = hostTransport(controllerUrl, requestTags); + URL v3ApiUrl = transport.v3ApiUrl(); + URL origin = toOrigin(v3ApiUrl); + HttpClient httpClient = transport.httpClient(); + RestClient restClient = CloudControllerRestClientBuilder.buildRestClient(httpClient, origin, oAuthClient, requestTags); + WebClient webClient = CloudControllerRestClientBuilder.buildWebClient(httpClient, origin, oAuthClient, requestTags); + CloudControllerV3Client cc = new CloudControllerV3Client(restClient, webClient); + // Per-upload client factory: the app-bits upload (POST /v3/packages/{guid}/upload) must be bounded by the caller's + // per-MTA uploadTimeout, not the shared 900s response timeout. Build a dedicated RestClient whose response timeout is + // that uploadTimeout, mirroring the OSS startUpload's .timeout(uploadTimeout). Everything else (pool, connect, TLS) matches. + java.util.function.Function uploadRestClientFactory = uploadTimeout -> { + var uploadOptions = new CloudControllerRestClientBuilder.TransportOptions(getConnectTimeout(), Optional.of(uploadTimeout), + getSslHandshakeTimeout(), getConnectionPoolSize(), + shouldTrustSelfSignedCertificates()); + return CloudControllerRestClientBuilder.build(origin, oAuthClient, requestTags, uploadOptions); + }; + return new CloudControllerRestClientV3Impl(v3ApiUrl, oAuthClient, target, restClient, cc, uploadRestClientFactory); } public CloudSpaceClient createSpaceClient(URL controllerUrl, OAuthClient oAuthClient, Map requestTags) { - URL v3ApiUrl = deriveV3ApiUrl(controllerUrl, requestTags); - RestClient restClient = buildRestClient(toOrigin(v3ApiUrl), oAuthClient, requestTags); - return new CloudSpaceClient(new CloudControllerV3Client(restClient)); + HostTransport transport = hostTransport(controllerUrl, requestTags); + URL origin = toOrigin(transport.v3ApiUrl()); + RestClient restClient = CloudControllerRestClientBuilder.buildRestClient(transport.httpClient(), origin, oAuthClient, requestTags); + WebClient webClient = CloudControllerRestClientBuilder.buildWebClient(transport.httpClient(), origin, oAuthClient, requestTags); + return new CloudSpaceClient(new CloudControllerV3Client(restClient, webClient)); } public LogCacheClient createLogCacheClient(URL controllerUrl, OAuthClient oAuthClient, Map requestTags) { - URL logCacheUrl = deriveLogCacheUrl(controllerUrl, requestTags); - RestClient restClient = buildRestClient(logCacheUrl, oAuthClient, requestTags); + HostTransport transport = hostTransport(controllerUrl, requestTags); + RestClient restClient = CloudControllerRestClientBuilder.buildRestClient(transport.httpClient(), transport.logCacheUrl(), + oAuthClient, requestTags); return new LogCacheClient(restClient); } - private RestClient buildRestClient(URL baseUrl, OAuthClient oAuthClient, Map requestTags) { - var transportOptions = new CloudControllerRestClientBuilder.TransportOptions(getConnectTimeout(), getResponseTimeout(), - getConnectionPoolSize(), - shouldTrustSelfSignedCertificates()); - return CloudControllerRestClientBuilder.build(baseUrl, oAuthClient, requestTags, transportOptions); + // Get (or lazily build) the shared per-host transport: one reactor-netty HttpClient + the resolved v3 / log-cache URLs, discovered + // once per host. Keyed on the controller host so the CF / space / log-cache clients for the same landscape share the pool. + private HostTransport hostTransport(URL controllerUrl, Map requestTags) { + return hostTransports.computeIfAbsent(controllerUrl.getHost(), host -> { + HttpClient httpClient = CloudControllerRestClientBuilder.buildHttpClient(transportOptions()); + URL v3ApiUrl = deriveV3ApiUrl(controllerUrl, requestTags); + URL logCacheUrl = deriveLogCacheUrl(controllerUrl, requestTags); + return new HostTransport(httpClient, v3ApiUrl, logCacheUrl); + }); + } + + private CloudControllerRestClientBuilder.TransportOptions transportOptions() { + return new CloudControllerRestClientBuilder.TransportOptions(getConnectTimeout(), getResponseTimeout(), getSslHandshakeTimeout(), + getConnectionPoolSize(), shouldTrustSelfSignedCertificates()); } // scheme://host[:port] of the given URL, dropping any path (e.g. the trailing /v3). The RestClient baseUrl must be the origin only, @@ -157,10 +190,10 @@ private Map fetchRootLinks(URL controllerUrl, Map uploadRestClientFactory) { + this(v3ApiUrl, oAuthClient, target, restClient, new CloudControllerV3Client(restClient), uploadRestClientFactory); + } + + public CloudControllerRestClientV3Impl(URL v3ApiUrl, OAuthClient oAuthClient, CloudSpace target, RestClient restClient, + CloudControllerV3Client cc, + java.util.function.Function uploadRestClientFactory) { this.v3ApiUrl = v3ApiUrl; this.oAuthClient = oAuthClient; this.target = target; this.restClient = restClient; - this.cc = new CloudControllerV3Client(restClient); + this.cc = cc; this.buildsOps = new BuildsV3Operations(cc, target); this.domainsOps = new DomainsV3Operations(cc, target); this.eventsOps = new EventsV3Operations(cc, target); this.jobsOps = new JobsV3Operations(cc, target); - this.packagesOps = new PackagesV3Operations(cc, target); + this.packagesOps = new PackagesV3Operations(cc, target, uploadRestClientFactory); this.processesOps = new ProcessesV3Operations(cc, target); this.rolesOps = new RolesV3Operations(cc, target); this.routesOps = new RoutesV3Operations(cc, target); diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java index 2adab4c566..3c8b53b46f 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java @@ -15,6 +15,11 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestClient; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import org.springframework.web.util.UriComponentsBuilder; + +import reactor.core.publisher.Flux; /** * Low-level Cloud Controller v3 access shared by every operation of {@link CloudControllerRestClientV3Impl}. This is the reusable @@ -37,10 +42,22 @@ public class CloudControllerV3Client { private static final Duration JOB_POLL_MAX_INTERVAL = Duration.ofSeconds(15); private static final Duration DEFAULT_JOB_TIMEOUT = Duration.ofMinutes(5); + // Reactor's default flatMap concurrency — the in-flight cap the OSS PaginationUtils effectively used when fetching pages 2..N. + private static final int MAX_CONCURRENT_PAGES = 256; + private final RestClient restClient; + // Optional reactive client on the SAME reactor-netty transport, used only to fetch pages 2..N concurrently (as the OSS client did via + // Reactor flatMap). When null (e.g. the auxiliary space/log-cache clients that never paginate large sets), list() falls back to the + // sequential next.href walk. + private final WebClient webClient; public CloudControllerV3Client(RestClient restClient) { + this(restClient, null); + } + + public CloudControllerV3Client(RestClient restClient, WebClient webClient) { this.restClient = restClient; + this.webClient = webClient; } public T get(String uri, Class responseType) { @@ -66,12 +83,38 @@ public Optional getOptional(String uri, Class responseType) { } /** - * Walk every page of a v3 list endpoint, following {@code pagination.next.href} until it is {@code null}. The caller supplies the - * fully-formed first-page URI (path + query) and a type reference for the paged {@link V3ListResponse}. + * List every resource of a v3 list endpoint across all pages. Mirrors the OSS {@code PaginationUtils.requestClientV3Resources}: + * fetch page 1, read {@code pagination.total_pages}, then request pages {@code 2..N} concurrently (bounded by + * {@value #MAX_CONCURRENT_PAGES}) via the reactive {@link WebClient} on the shared transport, and concatenate the results in page + * order. This restores the intra-operation concurrency the OSS client had on large, multi-page listings. + *

+ * Falls back to the sequential {@code pagination.next.href} walk when no {@link WebClient} is configured or the response does not + * report {@code total_pages} — preserving correctness on any endpoint that omits it. */ public List list(String firstPageUri, ParameterizedTypeReference> pageType) { - List all = new ArrayList<>(); - String nextUri = firstPageUri; + String firstPageRelativeUri = toRelativeIfAbsolute(firstPageUri); + V3ListResponse firstPage = restClient.get() + .uri(firstPageRelativeUri) + .retrieve() + .body(pageType); + if (firstPage == null) { + return new ArrayList<>(); + } + Integer totalPages = firstPage.pagination() == null ? null + : firstPage.pagination() + .totalPages(); + if (webClient == null || totalPages == null || totalPages <= 1) { + return listSequentially(firstPage, pageType); + } + List all = new ArrayList<>(firstPage.resources()); + all.addAll(fetchRemainingPagesConcurrently(firstPageRelativeUri, totalPages, pageType)); + return all; + } + + // Sequential pagination via pagination.next.href — the fallback path (and the whole behaviour when no WebClient is set). + private List listSequentially(V3ListResponse firstPage, ParameterizedTypeReference> pageType) { + List all = new ArrayList<>(firstPage.resources()); + String nextUri = toRelativeIfAbsolute(firstPage.nextPageHref()); while (nextUri != null) { V3ListResponse page = restClient.get() .uri(nextUri) @@ -86,6 +129,37 @@ public List list(String firstPageUri, ParameterizedTypeReference List fetchRemainingPagesConcurrently(String firstPageRelativeUri, int totalPages, + ParameterizedTypeReference> pageType) { + return Flux.range(2, totalPages - 1) + .flatMapSequential(page -> webClient.get() + .uri(pageUri(firstPageRelativeUri, page)) + .retrieve() + .bodyToMono(pageType) + .onErrorMap(WebClientResponseException.class, this::toCloudOperationException), + MAX_CONCURRENT_PAGES) + .flatMapIterable(V3ListResponse::resources) + .collectList() + .block(); + } + + // Set/replace the "page" query parameter on the first-page URI, keeping per_page and every filter intact. Package-visible for testing. + static String pageUri(String firstPageRelativeUri, int page) { + return UriComponentsBuilder.fromUriString(firstPageRelativeUri) + .replaceQueryParam("page", page) + .build() + .toUriString(); + } + + private CloudOperationException toCloudOperationException(WebClientResponseException e) { + HttpStatus status = HttpStatus.valueOf(e.getStatusCode() + .value()); + return new CloudOperationException(status, status.getReasonPhrase(), e.getResponseBodyAsString(), e); + } + /** * Follow the {@code Location} header of a 202-Accepted async response to its job, then poll to completion. */ diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/PackagesV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/PackagesV3Operations.java index 689e2a9305..b336615131 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/PackagesV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/PackagesV3Operations.java @@ -28,6 +28,7 @@ import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; /** * CF v3 packages operations of the cf-java-client replacement. Reproduces the HTTP shape and domain mapping of the OSS @@ -53,10 +54,19 @@ public class PackagesV3Operations { private final CloudControllerV3Client cc; private final CloudSpace target; + // Builds a RestClient whose response timeout is the given per-upload timeout, so a stalled bits upload is bounded by the + // user-configured UPLOAD_TIMEOUT (mirrors the OSS .timeout(uploadTimeout)). May be null, in which case the shared client is used. + private final java.util.function.Function uploadRestClientFactory; public PackagesV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this(cc, target, null); + } + + public PackagesV3Operations(CloudControllerV3Client cc, CloudSpace target, + java.util.function.Function uploadRestClientFactory) { this.cc = cc; this.target = target; + this.uploadRestClientFactory = uploadRestClientFactory; } public CloudPackage getPackage(UUID packageGuid) { @@ -122,17 +132,26 @@ private CloudPackage startUpload(String applicationName, Path file, Duration upl MultiValueMap body = new LinkedMultiValueMap<>(); body.add("bits", new FileSystemResource(file)); - cc.getRestClient() - .post() - .uri("/v3/packages/{guid}/upload", packageGuid) - .contentType(MediaType.MULTIPART_FORM_DATA) - .body(body) - .retrieve() - .toBodilessEntity(); + // Bound the bits upload by the caller's per-MTA uploadTimeout via a dedicated RestClient whose response timeout is that value + // (see CloudControllerRestClientFactory). Fall back to the shared client if no factory/timeout was supplied. + RestClient uploadClient = resolveUploadClient(uploadTimeout); + uploadClient.post() + .uri("/v3/packages/{guid}/upload", packageGuid) + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(body) + .retrieve() + .toBodilessEntity(); return getPackage(packageGuid); } + private RestClient resolveUploadClient(Duration uploadTimeout) { + if (uploadRestClientFactory != null && uploadTimeout != null && !uploadTimeout.isZero() && !uploadTimeout.isNegative()) { + return uploadRestClientFactory.apply(uploadTimeout); + } + return cc.getRestClient(); + } + private CloudPackage createBitsPackage(UUID applicationGuid) { V3Package created = cc.getRestClient() .post() diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOut.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOut.java new file mode 100644 index 0000000000..04a36caa1a --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOut.java @@ -0,0 +1,53 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.util.List; +import java.util.function.Function; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +/** + * Restores the intra-operation concurrency the OSS cf-java-client had for free. The OSS client composed per-item work + * (e.g. fetching credentials for N service keys, or resolving the plan/offering of N service instances) as a reactive graph and let + * Reactor's {@code flatMap} run those calls concurrently (default concurrency 256) before a single {@code .block()} at the boundary. + * The synchronous {@link RestClient}-based rewrite would otherwise do the same work strictly sequentially ({@code stream().map(fetch)}). + *

+ * This helper reproduces exactly that pattern — {@code Flux.fromIterable(...).flatMap(...).block()} — reusing the existing + * blocking per-item mappers unchanged. Each blocking mapper is offloaded onto {@link Schedulers#boundedElastic()} so the calls + * run concurrently without pinning the caller's thread; {@code flatMap} bounds how many are in flight (Reactor's default 256, matching + * the OSS cap). Results are returned in input order ({@link Flux#flatMapSequential} preserves order while still subscribing + * eagerly), and the first error aborts the rest and propagates unwrapped — the same fail-fast semantics a {@code .block()} over a failed + * {@code flatMap} had in the OSS path. + *

+ * This uses only Reactor (a Spring library already on the classpath as the transport); it does not reintroduce cf-java-client. + */ +final class ReactiveFanOut { + + // Reactor's default flatMap concurrency — the in-flight cap the OSS client effectively used for per-item fan-out. + private static final int MAX_CONCURRENCY = 256; + + private ReactiveFanOut() { + } + + /** + * Apply {@code mapper} (a blocking function) to every item concurrently, bounded by {@value #MAX_CONCURRENCY} in-flight calls, and + * return the results in the same order as {@code items}. The single-element and empty cases short-circuit without touching the + * reactive machinery (the overwhelmingly common case for typical MTAs). + */ + static List mapConcurrently(List items, Function mapper) { + if (items.isEmpty()) { + return List.of(); + } + if (items.size() == 1) { + return List.of(mapper.apply(items.get(0))); + } + return Flux.fromIterable(items) + .flatMapSequential(item -> Mono.fromCallable(() -> mapper.apply(item)) + .subscribeOn(Schedulers.boundedElastic()), + MAX_CONCURRENCY) + .collectList() + .block(); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java index 1361ba9fb4..87d43c10f4 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java @@ -179,10 +179,9 @@ public List getServiceInstancesWithoutAuxiliaryContentByNa public List getServiceInstancesByMetadataLabelSelector(String labelSelector) { String uri = "/v3/service_instances?per_page=" + PER_PAGE + "&space_guids=" + getTargetSpaceGuid() + "&label_selector=" + labelSelector; - return cc.list(uri, SERVICE_INSTANCE_LIST_TYPE) - .stream() - .map(this::mapWithAuxiliaryContent) - .collect(Collectors.toList()); + // Each managed instance needs its plan + offering resolved via extra GETs (mapWithAuxiliaryContent). The OSS client ran this + // enrichment concurrently across the list; preserve that so a large label-selector result doesn't serialize the per-instance calls. + return ReactiveFanOut.mapConcurrently(cc.list(uri, SERVICE_INSTANCE_LIST_TYPE), this::mapWithAuxiliaryContent); } public List getServiceInstancesWithoutAuxiliaryContentByMetadataLabelSelector(String labelSelector) { diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceKeysV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceKeysV3Operations.java index 720db7bc3f..e6e422acfe 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceKeysV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceKeysV3Operations.java @@ -97,11 +97,11 @@ public List getServiceKeys(CloudServiceInstance serviceInstance } public List getServiceKeysWithCredentials(CloudServiceInstance serviceInstance) { - return listServiceKeyResources(serviceInstance.getGuid()).stream() - .map(key -> V3ServiceKeyMapper.toCloudServiceKey(key, serviceInstance, - getServiceKeyCredentials( - key.guid()))) - .toList(); + // Each key needs a separate GET /details for its credentials. The OSS client fetched these concurrently (reactive flatMap); do the + // same here so a service with many keys doesn't serialize N credential round-trips. + return ReactiveFanOut.mapConcurrently(listServiceKeyResources(serviceInstance.getGuid()), + key -> V3ServiceKeyMapper.toCloudServiceKey(key, serviceInstance, + getServiceKeyCredentials(key.guid()))); } // --- helpers ---------------------------------------------------------------------------------------------------------------- diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3ClientPageUriTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3ClientPageUriTest.java new file mode 100644 index 0000000000..c5e4eb67a8 --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3ClientPageUriTest.java @@ -0,0 +1,40 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class CloudControllerV3ClientPageUriTest { + + @Test + void testAddsPageWhenAbsentAndKeepsExistingQuery() { + String uri = CloudControllerV3Client.pageUri("/v3/apps?per_page=5000&space_guids=abc", 3); + + Assertions.assertTrue(uri.contains("page=3"), uri); + Assertions.assertTrue(uri.contains("per_page=5000"), uri); + Assertions.assertTrue(uri.contains("space_guids=abc"), uri); + } + + @Test + void testReplacesExistingPageParam() { + String uri = CloudControllerV3Client.pageUri("/v3/apps?per_page=5000&page=1", 4); + + Assertions.assertTrue(uri.contains("page=4"), uri); + Assertions.assertFalse(uri.contains("page=1"), uri); + // per_page must survive the replacement. + Assertions.assertTrue(uri.contains("per_page=5000"), uri); + } + + @Test + void testPreservesMultipleFilters() { + String first = "/v3/service_credential_bindings?per_page=5000&type=key&service_instance_guids=g1&names=n1"; + + String uri = CloudControllerV3Client.pageUri(first, 2); + + Assertions.assertTrue(uri.contains("page=2"), uri); + Assertions.assertTrue(uri.contains("type=key"), uri); + Assertions.assertTrue(uri.contains("service_instance_guids=g1"), uri); + Assertions.assertTrue(uri.contains("names=n1"), uri); + Assertions.assertTrue(uri.contains("per_page=5000"), uri); + } + +} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOutTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOutTest.java new file mode 100644 index 0000000000..7cdc626d5c --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOutTest.java @@ -0,0 +1,108 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.util.List; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.springframework.http.HttpStatus; + +class ReactiveFanOutTest { + + @Test + void testEmptyReturnsEmpty() { + Assertions.assertEquals(List.of(), ReactiveFanOut.mapConcurrently(List.of(), x -> x)); + } + + @Test + void testSingleElementShortCircuits() { + List result = ReactiveFanOut.mapConcurrently(List.of("a"), s -> s + "!"); + + Assertions.assertEquals(List.of("a!"), result); + } + + @Test + void testResultsPreserveInputOrderEvenWhenTasksFinishOutOfOrder() { + List input = IntStream.rangeClosed(1, 20) + .boxed() + .toList(); + + // Later items finish sooner (inverse sleep) — the result must still be in input order. + List result = ReactiveFanOut.mapConcurrently(input, i -> { + sleep((20 - i)); + return i * 10; + }); + + List expected = input.stream() + .map(i -> i * 10) + .toList(); + Assertions.assertEquals(expected, result); + } + + @Test + @Timeout(10) + void testWorkRunsConcurrently() throws Exception { + int n = 8; + // If the mapper ran sequentially, the barrier (which needs all n parties) would never trip and the test would time out. + CyclicBarrier barrier = new CyclicBarrier(n); + List input = IntStream.range(0, n) + .boxed() + .toList(); + + List result = ReactiveFanOut.mapConcurrently(input, i -> { + try { + barrier.await(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + return i; + }); + + Assertions.assertEquals(input, result); + } + + @Test + void testFirstFailurePropagatesUnwrapped() { + List input = List.of(1, 2, 3, 4); + + CloudOperationException thrown = Assertions.assertThrows(CloudOperationException.class, + () -> ReactiveFanOut.mapConcurrently(input, i -> { + if (i == 3) { + throw new CloudOperationException(HttpStatus.BAD_GATEWAY); + } + return i; + })); + + Assertions.assertEquals(HttpStatus.BAD_GATEWAY, thrown.getStatusCode()); + } + + @Test + void testMapperInvokedOncePerItem() { + List input = IntStream.range(0, 50) + .boxed() + .toList(); + AtomicInteger invocations = new AtomicInteger(); + + List result = ReactiveFanOut.mapConcurrently(input, i -> { + invocations.incrementAndGet(); + return i; + }); + + Assertions.assertEquals(input, result); + Assertions.assertEquals(input.size(), invocations.get()); + } + + private static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread() + .interrupt(); + } + } + +} From 2fd10a4e04d02501a6da9917ca41d2f2e7a5874f Mon Sep 17 00:00:00 2001 From: Krasimir Kargov Date: Thu, 13 Aug 2026 09:27:15 +0300 Subject: [PATCH 4/6] Adding more fixes for the migration LMCROSSITXSADEPLOY-3563 --- .../CloudControllerRestClientBuilder.java | 15 ++++-- .../CloudControllerRestClientFactory.java | 33 ++++++------ .../rest/CloudControllerRestClientV3Impl.java | 21 +++++--- .../rest/ServiceInstancesV3Operations.java | 51 +++++++++++++++++-- 4 files changed, 91 insertions(+), 29 deletions(-) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java index 65d75efb9e..e1cf3ba75c 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java @@ -28,6 +28,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; import reactor.netty.http.client.HttpClient; import reactor.netty.resources.ConnectionProvider; +import reactor.netty.resources.LoopResources; /** * Builds the blocking Spring {@link RestClient} used by {@link CloudControllerRestClientV3Impl} to talk to the Cloud Controller v3 @@ -138,6 +139,13 @@ static HttpClient buildHttpClient(TransportOptions options) { .orElse(DEFAULT_CONNECT_TIMEOUT) .toMillis(); httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMillis); + // Size the I/O event-loop pool from threadPoolSize, mirroring the OSS DefaultConnectionContext.threadPoolSize (which created a + // reactor-netty LoopResources the client ran on). When unset, reactor-netty uses its shared default LoopResources. + Optional threadPoolSize = options.threadPoolSize(); + if (threadPoolSize.isPresent()) { + LoopResources loopResources = LoopResources.create(CONNECTION_POOL_NAME + "-loop", threadPoolSize.get(), true); + httpClient = httpClient.runOn(loopResources); + } Optional responseTimeout = options.responseTimeout(); if (responseTimeout.isPresent()) { httpClient = httpClient.responseTimeout(responseTimeout.get()); @@ -201,12 +209,13 @@ public X509Certificate[] getAcceptedIssuers() { /** * The transport-tuning surface, honored by the Reactor-Netty transport: {@code connectTimeout} and {@code responseTimeout} on the - * client, {@code sslHandshakeTimeout} on the TLS layer, {@code connectionPoolSize} on the {@link ConnectionProvider}. Mirrors the - * knobs the OSS {@code DefaultConnectionContext} exposed. + * client, {@code sslHandshakeTimeout} on the TLS layer, {@code connectionPoolSize} on the {@link ConnectionProvider}, and + * {@code threadPoolSize} on the I/O event-loop ({@link LoopResources}). Mirrors the knobs the OSS {@code DefaultConnectionContext} + * exposed. */ public record TransportOptions(Optional connectTimeout, Optional responseTimeout, Optional sslHandshakeTimeout, Optional connectionPoolSize, - boolean trustSelfSignedCertificates) { + Optional threadPoolSize, boolean trustSelfSignedCertificates) { } } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java index 1a2607a32c..b3f0087d86 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java @@ -1,5 +1,16 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; import org.cloudfoundry.multiapps.controller.client.facade.CloudException; import org.cloudfoundry.multiapps.controller.client.facade.adapters.LogCacheClient; @@ -12,18 +23,6 @@ import org.slf4j.LoggerFactory; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.Collections; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - import reactor.netty.http.client.HttpClient; /** @@ -82,9 +81,12 @@ public CloudControllerRestClient createClient(URL controllerUrl, CloudCredential // per-MTA uploadTimeout, not the shared 900s response timeout. Build a dedicated RestClient whose response timeout is // that uploadTimeout, mirroring the OSS startUpload's .timeout(uploadTimeout). Everything else (pool, connect, TLS) matches. java.util.function.Function uploadRestClientFactory = uploadTimeout -> { + // Upload uses a one-off client with only its response timeout overridden; no dedicated event-loop pool (threadPoolSize empty) + // so it does not spin up its own LoopResources. var uploadOptions = new CloudControllerRestClientBuilder.TransportOptions(getConnectTimeout(), Optional.of(uploadTimeout), - getSslHandshakeTimeout(), getConnectionPoolSize(), - shouldTrustSelfSignedCertificates()); + getSslHandshakeTimeout(), getConnectionPoolSize(), + Optional.empty(), + shouldTrustSelfSignedCertificates()); return CloudControllerRestClientBuilder.build(origin, oAuthClient, requestTags, uploadOptions); }; return new CloudControllerRestClientV3Impl(v3ApiUrl, oAuthClient, target, restClient, cc, uploadRestClientFactory); @@ -118,7 +120,8 @@ private HostTransport hostTransport(URL controllerUrl, Map reque private CloudControllerRestClientBuilder.TransportOptions transportOptions() { return new CloudControllerRestClientBuilder.TransportOptions(getConnectTimeout(), getResponseTimeout(), getSslHandshakeTimeout(), - getConnectionPoolSize(), shouldTrustSelfSignedCertificates()); + getConnectionPoolSize(), getThreadPoolSize(), + shouldTrustSelfSignedCertificates()); } // scheme://host[:port] of the given URL, dropping any path (e.g. the trailing /v3). The RestClient baseUrl must be the origin only, diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientV3Impl.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientV3Impl.java index a21ad8b656..57a8b147e0 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientV3Impl.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientV3Impl.java @@ -606,12 +606,21 @@ public void stopApplication(String applicationName) { @Override public void rename(String applicationName, String newName) { UUID guid = getApplicationGuid(applicationName); - cc.getRestClient() - .patch() - .uri("/v3/apps/{guid}", guid) - .body(Map.of("name", newName)) - .retrieve() - .toBodilessEntity(); + try { + cc.getRestClient() + .patch() + .uri("/v3/apps/{guid}", guid) + .body(Map.of("name", newName)) + .retrieve() + .toBodilessEntity(); + } catch (CloudOperationException e) { + // Idempotent-rename tolerance (mirrors the OSS fallbackApplicationRename): the CC can answer 503 to a rename that in fact + // already took effect. If the app is already named newName, treat the rename as successful; otherwise propagate. + if (e.getStatusCode() == HttpStatus.SERVICE_UNAVAILABLE && newName.equals(getApplicationName(guid))) { + return; + } + throw e; + } } @Override diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java index 87d43c10f4..0bea35bc76 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java @@ -1,5 +1,6 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -9,6 +10,7 @@ import java.util.stream.Collectors; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.Messages; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEntity; import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; @@ -253,7 +255,7 @@ private CloudServiceInstance mapWithAuxiliaryContent(V3ServiceInstance resource) if (servicePlanGuid == null) { return V3ServiceInstanceMapper.toCloudServiceInstance(resource, null, null); } - ServicePlanNames names = resolvePlanAndOfferingNames(servicePlanGuid); + ServicePlanNames names = resolvePlanAndOfferingNames(servicePlanGuid, resource.name()); return V3ServiceInstanceMapper.toCloudServiceInstance(resource, names.planName(), names.offeringName()); } @@ -283,21 +285,60 @@ private String servicePlanGuidOf(V3ServiceInstance resource) { .guid(); } - // Mirrors the OSS getServicePlanResource + getServiceOffering zip: resolve the plan's own name and its offering's name. - private ServicePlanNames resolvePlanAndOfferingNames(String servicePlanGuid) { - V3ServicePlan plan = cc.get("/v3/service_plans/" + servicePlanGuid, V3ServicePlan.class); + // Mirrors the OSS getServicePlanResource + getServiceOffering zip: resolve the plan's own name and its offering's name. Reproduces the + // OSS friendly-error mapping too — a 403/404 on the plan or offering lookup becomes a specific, human-readable CloudOperationException + // (instead of the generic one) so operators get the same messages as before. + private ServicePlanNames resolvePlanAndOfferingNames(String servicePlanGuid, String serviceInstanceName) { + V3ServicePlan plan = getServicePlanForNameResolution(servicePlanGuid, serviceInstanceName); if (plan == null) { return new ServicePlanNames(null, null); } String offeringName = null; String offeringGuid = offeringGuidOf(plan); if (offeringGuid != null) { - V3ServiceOffering offering = cc.get("/v3/service_offerings/" + offeringGuid, V3ServiceOffering.class); + V3ServiceOffering offering = getServiceOfferingForNameResolution(offeringGuid); offeringName = offering == null ? null : offering.name(); } return new ServicePlanNames(plan.name(), offeringName); } + private V3ServicePlan getServicePlanForNameResolution(String servicePlanGuid, String serviceInstanceName) { + try { + return cc.get("/v3/service_plans/" + servicePlanGuid, V3ServicePlan.class); + } catch (CloudOperationException e) { + if (e.getStatusCode() == HttpStatus.FORBIDDEN) { + throw new CloudOperationException(HttpStatus.FORBIDDEN, HttpStatus.FORBIDDEN.getReasonPhrase(), + MessageFormat.format(Messages.SERVICE_PLAN_WITH_GUID_0_NOT_AVAILABLE_FOR_SERVICE_INSTANCE_1, + servicePlanGuid, serviceInstanceName), + e); + } + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), + MessageFormat.format(Messages.NO_SERVICE_PLAN_FOUND, servicePlanGuid, + serviceInstanceName), + e); + } + throw e; + } + } + + private V3ServiceOffering getServiceOfferingForNameResolution(String offeringGuid) { + try { + return cc.get("/v3/service_offerings/" + offeringGuid, V3ServiceOffering.class); + } catch (CloudOperationException e) { + if (e.getStatusCode() == HttpStatus.FORBIDDEN) { + throw new CloudOperationException(HttpStatus.FORBIDDEN, HttpStatus.FORBIDDEN.getReasonPhrase(), + MessageFormat.format(Messages.SERVICE_OFFERING_WITH_GUID_0_IS_NOT_AVAILABLE, offeringGuid), + e); + } + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), + MessageFormat.format(Messages.SERVICE_OFFERING_WITH_GUID_0_NOT_FOUND, offeringGuid), e); + } + throw e; + } + } + private String offeringGuidOf(V3ServicePlan plan) { if (plan.relationships() == null || plan.relationships() .serviceOffering() == null From 860fb5637316015d2e86f2365ec49f1905cf7c9f Mon Sep 17 00:00:00 2001 From: Krasimir Kargov Date: Thu, 20 Aug 2026 09:15:46 +0300 Subject: [PATCH 5/6] Fix test LMCROSSITXSADEPLOY-3563 --- .../process/util/OperationInFinalStateHandlerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 de3062a4e9..fc6f8aab18 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 @@ -5,7 +5,7 @@ import java.util.List; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.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; From 698462b31243ddaa2152f1941e454c007948e76b Mon Sep 17 00:00:00 2001 From: Krasimir Kargov Date: Thu, 3 Sep 2026 16:36:37 +0300 Subject: [PATCH 6/6] Bettering everything as a whole - simplifications and logic changes LMCROSSITXSADEPLOY-3563 --- .../src/main/java/module-info.java | 2 + .../multiapps/controller/Constants.java | 38 ++ .../multiapps/controller/Messages.java | 45 +- .../controller/client/facade/AllowNulls.java | 5 - .../facade/adapters/LogCacheClient.java | 9 +- .../client/facade/domain/HealthCheckType.java | 3 - .../client/facade/domain/InstanceState.java | 6 +- .../client/facade/domain/JobState.java | 12 +- .../client/facade/domain/LastOperation.java | 5 - .../client/facade/domain/Metadata.java | 17 +- .../ServiceCredentialBindingOperation.java | 9 +- .../facade/domain/ServiceInstanceType.java | 16 +- .../facade/domain/ServiceOperation.java | 9 +- .../client/facade/domain/UserRole.java | 13 +- .../facade/rest/ApplicationsV3Operations.java | 268 ++++++++++ .../facade/rest/BuildsV3Operations.java | 26 +- .../rest/ClientConfigurationOptions.java | 9 + .../CloudControllerRestClientBuilder.java | 162 +++--- .../CloudControllerRestClientFactory.java | 162 +++--- .../rest/CloudControllerRestClientV3Impl.java | 505 ++++++------------ .../facade/rest/CloudControllerV3Client.java | 156 +++--- .../rest/CloudControllerV3Endpoints.java | 88 +++ .../client/facade/rest/CloudSpaceClient.java | 41 +- .../facade/rest/DomainsV3Operations.java | 78 +-- .../facade/rest/EventsV3Operations.java | 42 +- .../facade/rest/HttpConnectionPair.java | 8 + .../client/facade/rest/JobsV3Operations.java | 16 +- .../facade/rest/PackagesV3Operations.java | 99 ++-- .../facade/rest/ProcessesV3Operations.java | 119 +++-- .../client/facade/rest/ReactiveFanOut.java | 30 +- .../client/facade/rest/RolesV3Operations.java | 20 +- .../facade/rest/RoutesV3Operations.java | 169 +++--- .../rest/ServiceBindingsV3Operations.java | 146 ++--- .../rest/ServiceBrokersV3Operations.java | 82 ++- .../rest/ServiceInstancesV3Operations.java | 201 +++---- .../facade/rest/ServiceKeysV3Operations.java | 131 +++-- .../rest/ServiceOfferingsV3Operations.java | 37 +- .../facade/rest/ServicePlansV3Operations.java | 76 ++- .../facade/rest/StacksV3Operations.java | 33 +- .../client/facade/rest/TasksV3Operations.java | 53 +- .../rest/resources/LogCacheReadResponse.java | 13 +- .../facade/rest/resources/V3Application.java | 4 - .../rest/resources/V3ApplicationMapper.java | 6 +- .../facade/rest/resources/V3AuditEvent.java | 4 - .../rest/resources/V3AuditEventMapper.java | 7 +- .../facade/rest/resources/V3BuildMapper.java | 7 +- .../facade/rest/resources/V3Domain.java | 7 - .../facade/rest/resources/V3DomainMapper.java | 4 - .../facade/rest/resources/V3Droplet.java | 2 - .../rest/resources/V3InstancesInfoMapper.java | 9 +- .../client/facade/rest/resources/V3Job.java | 3 - .../facade/rest/resources/V3JobMapper.java | 6 +- .../facade/rest/resources/V3ListResponse.java | 7 +- .../rest/resources/V3PackageMapper.java | 12 +- .../facade/rest/resources/V3Process.java | 20 - .../rest/resources/V3ProcessMapper.java | 6 +- .../rest/resources/V3ResourceMappers.java | 19 +- .../client/facade/rest/resources/V3Role.java | 3 - .../facade/rest/resources/V3RoleMapper.java | 4 - .../client/facade/rest/resources/V3Route.java | 3 - .../facade/rest/resources/V3RouteMapper.java | 20 +- .../resources/V3ServiceBindingMapper.java | 12 +- .../rest/resources/V3ServiceBroker.java | 2 - .../rest/resources/V3ServiceBrokerMapper.java | 9 - .../rest/resources/V3ServiceInstance.java | 5 +- .../resources/V3ServiceInstanceMapper.java | 34 +- .../rest/resources/V3ServiceKeyDetails.java | 3 - .../rest/resources/V3ServiceKeyMapper.java | 15 +- .../resources/V3ServiceOfferingMapper.java | 7 +- .../rest/resources/V3ServicePlanMapper.java | 10 +- .../client/facade/rest/resources/V3Space.java | 1 + .../facade/rest/resources/V3StackMapper.java | 4 - .../facade/rest/resources/V3TaskMapper.java | 5 +- .../CloudControllerClientIntegrationTest.java | 12 +- .../CloudControllerV3ClientPageUriTest.java | 7 +- .../facade/rest/ReactiveFanOutTest.java | 9 +- .../process/jobs/OrphanedDataCleaner.java | 18 +- ...ServiceCreateUpdateServiceActionsStep.java | 2 +- 78 files changed, 1602 insertions(+), 1665 deletions(-) create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/Constants.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ApplicationsV3Operations.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ClientConfigurationOptions.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Endpoints.java create mode 100644 multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/HttpConnectionPair.java diff --git a/multiapps-controller-client/src/main/java/module-info.java b/multiapps-controller-client/src/main/java/module-info.java index 111c91e054..dfd1045713 100644 --- a/multiapps-controller-client/src/main/java/module-info.java +++ b/multiapps-controller-client/src/main/java/module-info.java @@ -38,5 +38,7 @@ requires static java.compiler; requires static jakarta.inject; requires static org.immutables.value; + requires io.netty.codec; + requires java.desktop; } \ No newline at end of file diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/Constants.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/Constants.java new file mode 100644 index 0000000000..705fc88798 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/Constants.java @@ -0,0 +1,38 @@ +package org.cloudfoundry.multiapps.controller; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +public class Constants { + + public static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofMinutes(1); + public static final Duration JOB_POLL_MIN_INTERVAL = Duration.ofSeconds(1); + public static final Duration JOB_POLL_MAX_INTERVAL = Duration.ofSeconds(15); + public static final Duration DELETE_JOB_TIMEOUT = Duration.ofMinutes(5); + public static final Duration BINDING_OPERATIONS_TIMEOUT = Duration.ofMinutes(10); + public static final long PACKAGE_UPLOAD_JOB_POLLING_PERIOD = TimeUnit.SECONDS.toMillis(5); + + public static final int DEFAULT_CONNECTION_POOL_SIZE = 192; + public static final int UNDEFINED_PORT = -1; + public static final int MAX_CONCURRENT_PAGES = 256; + public static final int DEFAULT_CONCURRENT_TASKS = 256; + public static final int MAX_CHAR_LENGTH_FOR_PARAMS_IN_REQUEST = 4000; + + public static final String CONNECTION_POOL_NAME = "cf-controller-client"; + public static final String LOOP_RESOURCES_SUFFIX = "-loop-resources"; + public static final String CF_API_V3 = "/v3"; + public static final String HREF = "href"; + public static final String CLOUD_CONTROLLER_CF_ROOT_DOCUMENT_NAME = "cloud_controller_v3"; + public static final String LOG_CACHE_CF_ROOT_DOCUMENT_NAME = "log_cache"; + public static final String API_HOST_PREFIX = "api."; + public static final String LOG_CACHE_PREFIX = "log-cache."; + public static final String ROOT_DOCUMENT_LINKS_LIST = "links"; + + public static final String EMPTY_STRING = ""; + public static final String COLON = ":"; + public static final String PROTOCOL_SEPARATOR = "://"; + + public static final String PACKAGE_LINK = "package"; + public static final String WEB_PROCESS_TYPE = "web"; + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/Messages.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/Messages.java index af0d03d5f0..8705623590 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/Messages.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/Messages.java @@ -3,9 +3,52 @@ public class Messages { // INFO messages - public static final String WAITING_MS_BEFORE_RETRYING_WITH_TIMEOUT_OF_MS = "Waiting: {} ms before retrying with timeout of: {} ms"; + public static final String WAITING_MS_BEFORE_RETRYING_WITH_TIMEOUT_OF_MS = "Waiting: {} ms before retrying with timeout of: {} ms."; public static final String RATE_LIMITED_BY_CC_WAITING_S = "CC returned 429 with Retry-After: {} s. Waiting {} s (capped) before retrying."; public static final String RATE_LIMITED_BY_CC_NO_HEADER_WAITING_MS = "CC returned 429 without Retry-After header. Waiting {} ms before retrying."; public static final String RANDOM_WAIT_BEFORE_RETRY_MS = "Waiting {} ms (randomized) before retrying failed CC operation."; + // Exception messages + public static final String ERROR_OCCURRED_SETTING_UP_DEFAULT_SSL_CONTEXT = "An error occurred while setting up the default SSLContext."; + public static final String ERROR_OCCURRED_SETTING_UP_ALWAYS_APPROVING_SSL_CONTEXT = "An error occurred while setting up the always approving SSLContext."; + public static final String COULD_NOT_DETERMINE_API_ORIGIN_FROM_0 = "Could not determine the API origin from \"{0}\"."; + public static final String COULD_NOT_RESOLVE_URL_FROM_0_1 = "Could not resolve URL from \"{0}\" \"{1}\"."; + public static final String COULD_NOT_RESOLVE_LOG_CACHE_URL_FROM_0 = "Could not derive the log-cache URL from \"{0}\"."; + public static final String CF_ROOT_DOCUMENT_REQUEST_TO_0_RETURNED_1 = "CF root request to \"{0}\" returned \"{1}\"."; + public static final String INTERRUPTED_WHILE_CALLING_THE_CF_ROOT_URL_AT_0 = "Interrupted while calling the CF root URL at \"{0}\"."; + public static final String FAILED_TO_CALL_THE_CF_ROOT_AT_0_WITH_1 = "Failed to call the CF root at \"{0}\" with: \"{1}\"."; + public static final String SERVICE_KEY_0_NOT_FOUND = "Service key \"{0}\" not found."; + public static final String SERVICE_INSTANCE_0_NOT_FOUND = "Service instance \"{0}\" not found."; + public static final String SERVICE_BROKER_0_NOT_FOUND = "Service broker \"{0}\" not found."; + public static final String APPLICATION_0_NOT_FOUND = "Application \"{0}\" not found."; + public static final String DOMAIN_0_NOT_FOUND = "Domain \"{0}\" not found."; + public static final String SERVICE_PLAN_0_NOT_FOUND = "Service plan \"{0}\" not found."; + public static final String STACK_0_NOT_FOUND = "Stack \"{0}\" not found."; + public static final String ORGANISATION_0_NOT_FOUND = "Organization \"{0}\" not found."; + public static final String SPACE_WITH_GUID_0_NOT_FOUND = "Space with GUID \"{0}\" not found."; + public static final String APPLICATION_WITH_GUID_0_DOES_NOT_HAVE_A_DROPLET = "Application with guid \"{0}\" does not have a droplet"; + public static final String HOST_0_NOT_FOUND_FOR_DOMAIN_1 = "Host \"{0}\" not found for domain \"{1}\"."; + public static final String UNABLE_TO_0_WITHOUT_SPECIFYING_ORGANIZATION_AND_SPACE_TO_USE = "Unable to \"{0}\" without specifying organization and space to use."; + public static final String DOMAIN_0_NOT_FOUND_FOR_URI_1 = "Domain \"{0}\" not found for URI \"{1}\""; + public static final String SERVICE_BINDING_BETWEEN_SERVICE_WITH_GUID_0_AND_APPLICATION_WITH_GUID_1_NOT_FOUND = "Service binding between service with GUID \"{0}\" and application with GUID \"{1}\" not found."; + public static final String SERVICE_PLAN_WITH_GUID_0_NOT_AVAILABLE_FOR_SERVICE_INSTANCE_1 = "Service plan with guid \"{0}\" is not available for service instance \"{1}\"."; + public static final String NO_SERVICE_PLAN_FOUND = "Service plan with guid \"{0}\" for service instance with name \"{1}\" was not found."; + public static final String SERVICE_OFFERING_WITH_GUID_0_IS_NOT_AVAILABLE = "Service offering with guid \"{0}\" is not available."; + public static final String SERVICE_OFFERING_WITH_GUID_0_NOT_FOUND = "Service offering with guid \"{0}\" not found."; + public static final String TARGET_SPACE_REQUIRED_TO_CREATE_AN_APPLICATION = "A target space is required to create an application."; + public static final String JOB_0_DID_NOT_COMPLETE_WITHIN_1 = "Job \"{0}\" did not complete within \"{1}\""; + public static final String ORGANIZATION_WITH_GUID_0_NOT_FOUND = "Organization with GUID \"{0}\" not found."; + public static final String SPACE_0_NOT_FOUND_IN_ORGANIZATION_1 = "Space \"{0}\" not found in organization \"{1}\""; + public static final String INTERRUPTED_WHILE_POLLING_ASYNC_JOB_0 = "Interrupted while polling an async job: \"{0}\""; + public static final String UNKNOWN_JOB_STATE_0 = "Unknown job state: \"{0}\""; + public static final String UNKNOWN_SERVICE_INSTANCE_TYPE_0 = "Unknown service instance type: \"{0}\""; + public static final String UNKNOWN_USER_ROLE_0 = "Unknown user role: \"{0}\""; + + //Status texts + public static final String NOT_FOUND = "Not Found"; + public static final String JOB_TIMEOUT = "Job Timeout"; + public static final String JOB_FAILED = "Job Failed"; + public static final String BAD_REQUEST = "Bad Request"; + public static final String FORBIDDEN = "Forbidden"; + } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/AllowNulls.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/AllowNulls.java index 680efaa956..944e54edf6 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/AllowNulls.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/AllowNulls.java @@ -5,11 +5,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * Marks an Immutables collection attribute (typically a {@code Map}) as permitting {@code null} values. Project-owned replacement for - * {@code org.cloudfoundry.AllowNulls} (from cf-java-client) so the domain model no longer depends on the OSS client. Immutables - * recognises this marker by its simple name {@code AllowNulls}. - */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface AllowNulls { diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/LogCacheClient.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/LogCacheClient.java index 67d4bba810..304a80dff8 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/LogCacheClient.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/LogCacheClient.java @@ -21,13 +21,6 @@ import org.slf4j.LoggerFactory; import org.springframework.web.client.RestClient; -/** - * Fetches recent application logs from Log-Cache. Reimplemented on a plain synchronous Spring {@link RestClient} against Log-Cache's - * JSON {@code read} REST API ({@code GET /api/v1/read/{source_id}}), removing the previous dependency on the OSS cf-java-client - * ({@code org.cloudfoundry.logcache.v1.*} / {@code org.cloudfoundry.reactor.*}). The {@link RestClient} is pre-configured with the - * log-cache base URL, bearer-token auth and request tags by the factory; this class only builds the request and maps the response to - * {@link ApplicationLog}, preserving the previous behaviour exactly. - */ public class LogCacheClient { private static final Logger LOGGER = LoggerFactory.getLogger(LogCacheClient.class); @@ -47,6 +40,7 @@ public List getRecentLogs(UUID applicationGuid, LocalDateTime of if (response == null) { throw new CloudException(MessageFormat.format(Messages.FAILED_TO_FETCH_APP_LOGS_FOR_APP, applicationGuid)); } + LOGGER.info(Messages.APP_LOGS_WERE_FETCHED_SUCCESSFULLY); return response.batch() .stream() @@ -61,6 +55,7 @@ private LogCacheReadResponse readApplicationLogs(UUID applicationGuid, LocalDate var secondsInNanos = Duration.ofSeconds(instant.getEpochSecond()) .toNanos(); long startTime = secondsInNanos + instant.getNano() + 1; + return restClient.get() .uri(uriBuilder -> uriBuilder.path("/api/v1/read/{sourceId}") .queryParam("envelope_types", "LOG") diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/HealthCheckType.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/HealthCheckType.java index e618ee4b45..992fa758a3 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/HealthCheckType.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/HealthCheckType.java @@ -3,9 +3,6 @@ public enum HealthCheckType { HTTP, PORT, PROCESS, @Deprecated NONE; - /** - * The CF v3 wire value (lowercase), matching the OSS {@code HealthCheckType.getValue()} this enum replaces at call sites. - */ public String getValue() { return name().toLowerCase(); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstanceState.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstanceState.java index 90bd5f95d6..a5425130ed 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstanceState.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstanceState.java @@ -2,11 +2,7 @@ public enum InstanceState { CRASHED, DOWN, RUNNING, STARTING, UNKNOWN; - - /** - * Resolve from the CF v3 process {@code state} wire value (e.g. {@code RUNNING}). Takes the raw string rather than the OSS - * {@code ProcessState} enum, so the domain no longer depends on cf-java-client. Unknown/blank values map to {@link #UNKNOWN}. - */ + public static InstanceState valueOfWithDefault(String state) { if (state == null) { return UNKNOWN; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/JobState.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/JobState.java index ccb2f6521f..9c3445f45a 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/JobState.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/JobState.java @@ -1,14 +1,11 @@ package org.cloudfoundry.multiapps.controller.client.facade.domain; +import java.text.MessageFormat; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +import org.cloudfoundry.multiapps.controller.Messages; -/** - * State of an asynchronous Cloud Controller v3 job. Project-owned replacement for {@code org.cloudfoundry.client.v3.jobs.JobState}, - * so the domain model no longer depends on the OSS cf-java-client. JSON (de)serialization uses the lowercase wire value - * ({@code processing} / {@code polling} / {@code complete} / {@code failed}) via {@link JsonValue} / {@link JsonCreator} — matching the - * OSS enum so persisted models round-trip identically. - */ public enum JobState { PROCESSING("processing"), POLLING("polling"), COMPLETE("complete"), FAILED("failed"); @@ -31,6 +28,7 @@ public static JobState from(String value) { return state; } } - throw new IllegalArgumentException("Unknown job state: " + value); + + throw new IllegalArgumentException(MessageFormat.format(Messages.UNKNOWN_JOB_STATE_0, value)); } } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/LastOperation.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/LastOperation.java index 8aefe77484..0e43b64c12 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/LastOperation.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/LastOperation.java @@ -1,9 +1,4 @@ package org.cloudfoundry.multiapps.controller.client.facade.domain; -/** - * Project-owned view of a Cloud Controller v3 resource's {@code last_operation} (raw string fields), replacing - * {@code org.cloudfoundry.client.v3.LastOperation} so the domain no longer depends on the OSS cf-java-client. All fields are the raw CF - * wire values; any may be {@code null}. - */ public record LastOperation(String type, String state, String description, String createdAt, String updatedAt) { } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Metadata.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Metadata.java index 979e357d6d..342e1d2d44 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Metadata.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Metadata.java @@ -8,15 +8,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -/** - * Project-owned replacement for {@code org.cloudfoundry.client.v3.Metadata} (CF v3 resource labels + annotations), so the domain model no - * longer depends on the OSS cf-java-client. - *

- * It is a faithful drop-in: the fluent {@link Builder} exposes the same {@code label(k,v)} / {@code annotation(k,v)} / - * {@code labels(map)} / {@code annotations(map)} methods the codebase already calls, and JSON (de)serialization uses the same - * {@code {"labels": {...}, "annotations": {...}}} shape as the OSS type, so persisted models (e.g. {@code CloudEntity.getV3Metadata()}) - * round-trip identically. - */ public final class Metadata { private final Map labels; @@ -47,9 +38,11 @@ public boolean equals(Object o) { if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + Metadata metadata = (Metadata) o; return labels.equals(metadata.labels) && annotations.equals(metadata.annotations); } @@ -72,14 +65,12 @@ public static final class Builder { private Builder() { } - /** - * Copy labels and annotations from an existing {@link Metadata}. Mirrors the OSS builder's {@code from(Metadata)}. - */ public Builder from(Metadata metadata) { if (metadata != null) { this.labels.putAll(metadata.getLabels()); this.annotations.putAll(metadata.getAnnotations()); } + return this; } @@ -92,6 +83,7 @@ public Builder labels(Map labels) { if (labels != null) { this.labels.putAll(labels); } + return this; } @@ -108,6 +100,7 @@ public Builder annotations(Map annotations) { if (annotations != null) { this.annotations.putAll(annotations); } + return this; } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceCredentialBindingOperation.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceCredentialBindingOperation.java index ea06444480..73403202f1 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceCredentialBindingOperation.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceCredentialBindingOperation.java @@ -5,11 +5,10 @@ import java.time.format.DateTimeFormatter; import java.util.Arrays; -import org.immutables.value.Value; - import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.cloudfoundry.multiapps.controller.client.facade.Nullable; +import org.immutables.value.Value; @Value.Immutable @JsonSerialize(as = ImmutableServiceCredentialBindingOperation.class) @@ -35,6 +34,7 @@ public static ServiceCredentialBindingOperation from(LastOperation lastOperation String lastOperationDescription = lastOperation.description(); String lastOperationCreatedAt = lastOperation.createdAt(); String lastOperationUpdatedAt = lastOperation.updatedAt(); + return ImmutableServiceCredentialBindingOperation.builder() .type(ServiceCredentialBindingOperation.Type.fromString(lastOperationType)) .state(ServiceCredentialBindingOperation.State.fromString(lastOperationState)) @@ -54,8 +54,9 @@ public static Type fromString(String value) { .filter(type -> type.toString() .equals(value)) .findFirst() - .orElseThrow(() -> new IllegalArgumentException(MessageFormat.format("Illegal service binding operation type: \"{0}\"", - value))); + .orElseThrow( + () -> new IllegalArgumentException(MessageFormat.format("Illegal service binding operation type: \"{0}\"", + value))); } @Override diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceInstanceType.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceInstanceType.java index 90618abbf9..434443b306 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceInstanceType.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceInstanceType.java @@ -1,15 +1,11 @@ package org.cloudfoundry.multiapps.controller.client.facade.domain; +import java.text.MessageFormat; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +import org.cloudfoundry.multiapps.controller.Messages; -/** - * Type of a Cloud Controller v3 service instance. Project-owned replacement for - * {@code org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType}, so the domain model no longer depends on the OSS - * cf-java-client. Values mirror the CF v3 service instance {@code type} field, and JSON (de)serialization uses the wire value - * ({@code managed} / {@code user-provided}) via {@link JsonValue} / {@link JsonCreator} — matching the OSS enum's behaviour so persisted - * models round-trip identically. - */ public enum ServiceInstanceType { MANAGED("managed"), USER_PROVIDED("user-provided"); @@ -25,9 +21,6 @@ public String getValue() { return value; } - /** - * Resolve from the CF v3 wire value ({@code managed} / {@code user-provided}). Mirrors the OSS {@code ServiceInstanceType.from}. - */ @JsonCreator public static ServiceInstanceType from(String value) { for (ServiceInstanceType type : values()) { @@ -35,6 +28,7 @@ public static ServiceInstanceType from(String value) { return type; } } - throw new IllegalArgumentException("Unknown service instance type: " + value); + + throw new IllegalArgumentException(MessageFormat.format(Messages.UNKNOWN_SERVICE_INSTANCE_TYPE_0, value)); } } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceOperation.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceOperation.java index b799c4c3ed..7edff81be7 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceOperation.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceOperation.java @@ -83,10 +83,11 @@ public static ServiceOperation fromLastOperation(LastOperation lastOperation) { if (lastOperation == null || lastOperation.type() == null || lastOperation.state() == null) { return null; } - Type type = Type.fromString(lastOperation.type()); - State state = State.fromString(lastOperation.state()); - String description = lastOperation.description(); - return new ServiceOperation(type, description, state); + + Type operationType = Type.fromString(lastOperation.type()); + State operationState = State.fromString(lastOperation.state()); + String operationDescription = lastOperation.description(); + return new ServiceOperation(operationType, operationDescription, operationState); } @Override diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/UserRole.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/UserRole.java index ed0b02389f..8644a5cdf2 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/UserRole.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/UserRole.java @@ -1,9 +1,12 @@ package org.cloudfoundry.multiapps.controller.client.facade.domain; +import java.text.MessageFormat; import java.util.Arrays; import java.util.Map; import java.util.stream.Collectors; +import org.cloudfoundry.multiapps.controller.Messages; + public enum UserRole { ORGANIZATION_AUDITOR, @@ -16,17 +19,15 @@ public enum UserRole { private static final Map NAMES_TO_VALUES = Arrays.stream(values()) .collect(Collectors.toMap(UserRole::getName, - roleType -> roleType)); + roleType -> roleType)); - /** - * Resolve from the CF v3 role {@code type} wire value (e.g. {@code space_developer}). Takes the raw string rather than the OSS - * {@code RoleType} enum, so the domain no longer depends on cf-java-client. - */ public static UserRole fromRoleType(String roleTypeValue) { UserRole userRole = NAMES_TO_VALUES.get(roleTypeValue); + if (userRole == null) { - throw new IllegalArgumentException("Unknown user role: " + roleTypeValue); + throw new IllegalArgumentException(MessageFormat.format(Messages.UNKNOWN_USER_ROLE_0, roleTypeValue)); } + return userRole; } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ApplicationsV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ApplicationsV3Operations.java new file mode 100644 index 0000000000..12a19a3b8b --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ApplicationsV3Operations.java @@ -0,0 +1,268 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.text.MessageFormat; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.Messages; +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; +import org.cloudfoundry.multiapps.controller.client.facade.dto.ApplicationToCreateDto; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Application; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ApplicationMapper; +import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; + +public class ApplicationsV3Operations { + + private static final ParameterizedTypeReference> APPLICATION_PAGE = new ParameterizedTypeReference<>() { + }; + + private static final Duration DELETE_JOB_TIMEOUT = Duration.ofMinutes(5); + + private final CloudControllerV3Client cc; + private final CloudSpace target; + + public ApplicationsV3Operations(CloudControllerV3Client cc, CloudSpace target) { + this.cc = cc; + this.target = target; + } + + public UUID createApplication(ApplicationToCreateDto dto) { + if (target == null || target.getGuid() == null) { + throw new CloudOperationException(HttpStatus.BAD_REQUEST, Messages.BAD_REQUEST, + Messages.TARGET_SPACE_REQUIRED_TO_CREATE_AN_APPLICATION); + } + Map body = new HashMap<>(); + body.put("name", dto.getName()); + body.put("lifecycle", buildLifecycle(dto.getStaging())); + body.put("relationships", Map.of("space", Map.of("data", Map.of("guid", target.getGuid() + .toString())))); + if (dto.getEnv() != null) { + body.put("environment_variables", dto.getEnv()); + } + if (dto.getMetadata() != null) { + body.put("metadata", Map.of("labels", dto.getMetadata() + .getLabels(), + "annotations", dto.getMetadata() + .getAnnotations())); + } + V3Application created = cc.getRestClient() + .post() + .uri(CloudControllerV3Endpoints.APPS) + .body(body) + .retrieve() + .body(V3Application.class); + UUID appGuid = UUID.fromString(created.guid()); + Map scale = new HashMap<>(); + if (dto.getMemoryInMb() != null) { + scale.put("memory_in_mb", dto.getMemoryInMb()); + } + if (dto.getDiskQuotaInMb() != null) { + scale.put("disk_in_mb", dto.getDiskQuotaInMb()); + } + if (!scale.isEmpty()) { + scaleWebProcess(appGuid, scale); + } + return appGuid; + } + + private Map buildLifecycle(Staging staging) { + if (staging == null) { + return Map.of("type", "buildpack", "data", Map.of()); + } + if (staging.getDockerInfo() != null) { + return Map.of("type", "docker", "data", Map.of()); + } + String type = staging.getLifecycleType() != null ? staging.getLifecycleType() + .name() + .toLowerCase() + : "buildpack"; + Map data = new HashMap<>(); + if (staging.getBuildpacks() != null) { + data.put("buildpacks", staging.getBuildpacks()); + } + if (staging.getStackName() != null) { + data.put("stack", staging.getStackName()); + } + return Map.of("type", type, "data", data); + } + + public void deleteApplication(String applicationName) { + UUID applicationGuid = getApplicationGuid(applicationName); + var response = cc.getRestClient() + .delete() + .uri(CloudControllerV3Endpoints.APP_BY_GUID, applicationGuid) + .retrieve() + .toBodilessEntity(); + cc.followAsyncJob(response, DELETE_JOB_TIMEOUT); + } + + public CloudApplication getApplication(String applicationName) { + return getApplication(applicationName, true); + } + + public CloudApplication getApplication(String applicationName, boolean required) { + V3Application app = findApplicationByName(applicationName); + if (app == null) { + if (required) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.APPLICATION_0_NOT_FOUND, applicationName)); + } + return null; + } + return V3ApplicationMapper.toCloudApplication(app, target); + } + + public UUID getApplicationGuid(String applicationName) { + return getApplication(applicationName).getGuid(); + } + + public String getApplicationName(UUID applicationGuid) { + V3Application app = cc.get(CloudControllerV3Endpoints.APPS + "/" + applicationGuid, V3Application.class); + return app == null ? null : app.name(); + } + + public Map getApplicationEnvironment(UUID applicationGuid) { + V3Application.V3EnvironmentVariables env = cc.get(CloudControllerV3Endpoints.APPS + "/" + applicationGuid + + "/environment_variables", + V3Application.V3EnvironmentVariables.class); + return env == null || env.var() == null ? Map.of() : env.var(); + } + + public Map getApplicationEnvironment(String applicationName) { + return getApplicationEnvironment(getApplicationGuid(applicationName)); + } + + public List getApplications() { + return listApplications(applicationsQuery(null)).stream() + .map(app -> V3ApplicationMapper.toCloudApplication(app, target)) + .toList(); + } + + public List getApplicationsByMetadataLabelSelector(String labelSelector) { + String query = applicationsQuery(null); + if (labelSelector != null) { + query = query + CloudControllerV3Endpoints.AMPERSAND_LABEL_SELECTOR + labelSelector; + } + return listApplications(query).stream() + .map(app -> V3ApplicationMapper.toCloudApplication(app, target)) + .toList(); + } + + public void startApplication(String applicationName) { + UUID guid = getApplicationGuid(applicationName); + cc.getRestClient() + .post() + .uri(CloudControllerV3Endpoints.APP_START, guid) + .retrieve() + .toBodilessEntity(); + } + + public void stopApplication(String applicationName) { + UUID guid = getApplicationGuid(applicationName); + cc.getRestClient() + .post() + .uri(CloudControllerV3Endpoints.APP_STOP, guid) + .retrieve() + .toBodilessEntity(); + } + + public void rename(String applicationName, String newName) { + UUID guid = getApplicationGuid(applicationName); + try { + cc.getRestClient() + .patch() + .uri(CloudControllerV3Endpoints.APP_BY_GUID, guid) + .body(Map.of("name", newName)) + .retrieve() + .toBodilessEntity(); + } catch (CloudOperationException e) { + //the Cloud Controller can return 503 but the rename might have happened already + if (e.getStatusCode() == HttpStatus.SERVICE_UNAVAILABLE && newName.equals(getApplicationName(guid))) { + return; + } + throw e; + } + } + + public void updateApplicationInstances(String applicationName, int instances) { + scaleWebProcess(getApplicationGuid(applicationName), Map.of("instances", instances)); + } + + public void updateApplicationMemory(String applicationName, int memory) { + scaleWebProcess(getApplicationGuid(applicationName), Map.of("memory_in_mb", memory)); + } + + public void updateApplicationDiskQuota(String applicationName, int disk) { + scaleWebProcess(getApplicationGuid(applicationName), Map.of("disk_in_mb", disk)); + } + + public void updateApplicationEnv(String applicationName, Map env) { + UUID guid = getApplicationGuid(applicationName); + cc.getRestClient() + .patch() + .uri(CloudControllerV3Endpoints.APP_ENV_VARS, guid) + .body(Map.of("var", env)) + .retrieve() + .toBodilessEntity(); + } + + public void bindDropletToApp(UUID dropletGuid, UUID applicationGuid) { + cc.getRestClient() + .patch() + .uri(CloudControllerV3Endpoints.APP_CURRENT_DROPLET, applicationGuid) + .body(Map.of("data", Map.of("guid", dropletGuid.toString()))) + .retrieve() + .toBodilessEntity(); + } + + public void updateApplicationMetadata(UUID guid, Metadata metadata) { + cc.getRestClient() + .patch() + .uri(CloudControllerV3Endpoints.APP_BY_GUID, guid) + .body(Map.of("metadata", Map.of("labels", metadata.getLabels(), "annotations", metadata.getAnnotations()))) + .retrieve() + .toBodilessEntity(); + } + + private void scaleWebProcess(UUID applicationGuid, Map scaleBody) { + cc.getRestClient() + .post() + .uri(CloudControllerV3Endpoints.APP_WEB_PROCESS_SCALE, applicationGuid) + .body(scaleBody) + .retrieve() + .toBodilessEntity(); + } + + private V3Application findApplicationByName(String applicationName) { + List apps = listApplications(applicationsQuery(applicationName)); + return apps.isEmpty() ? null : apps.get(0); + } + + private List listApplications(String query) { + return cc.list(query, APPLICATION_PAGE); + } + + private String applicationsQuery(String name) { + StringBuilder query = new StringBuilder(CloudControllerV3Endpoints.APPS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE); + if (target != null && target.getGuid() != null) { + query.append(CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS) + .append(target.getGuid()); + } + if (name != null) { + query.append(CloudControllerV3Endpoints.AMPERSAND_NAMES) + .append(name); + } + return query.toString(); + } + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/BuildsV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/BuildsV3Operations.java index 2605aeb4a8..c9416c6bd0 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/BuildsV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/BuildsV3Operations.java @@ -5,33 +5,23 @@ import java.util.UUID; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudBuild; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Build; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3BuildMapper; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; import org.springframework.core.ParameterizedTypeReference; -/** - * CF v3 Builds operations for the in-house {@link CloudControllerRestClientV3Impl}. Reproduces the HTTP shape of the OSS - * {@code CloudControllerRestClientImpl} build methods (endpoints from {@code builds()} / {@code applicationsV3().listBuilds}), mapping the - * v3 wire model to the {@link CloudBuild} domain type via {@link V3BuildMapper}. - */ public class BuildsV3Operations { - private static final int MAX_PAGE_SIZE = 5000; - private final CloudControllerV3Client cc; - private final CloudSpace target; - public BuildsV3Operations(CloudControllerV3Client cc, CloudSpace target) { + public BuildsV3Operations(CloudControllerV3Client cc) { this.cc = cc; - this.target = target; } public CloudBuild createBuild(UUID packageGuid) { V3Build build = cc.getRestClient() .post() - .uri("/v3/builds") + .uri(CloudControllerV3Endpoints.BUILDS) .body(Map.of("package", Map.of("guid", packageGuid.toString()))) .retrieve() .body(V3Build.class); @@ -39,23 +29,25 @@ public CloudBuild createBuild(UUID packageGuid) { } public CloudBuild getBuild(UUID buildGuid) { - V3Build build = cc.get("/v3/builds/" + buildGuid, V3Build.class); + V3Build build = cc.get(CloudControllerV3Endpoints.BUILDS + "/" + buildGuid, V3Build.class); return V3BuildMapper.toCloudBuild(build); } public List getBuildsForApplication(UUID applicationGuid) { - String uri = "/v3/apps/" + applicationGuid + "/builds?per_page=" + MAX_PAGE_SIZE; + String uri = CloudControllerV3Endpoints.APPS + "/" + applicationGuid + "/builds" + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE; return cc.list(uri, new ParameterizedTypeReference>() { - }) + }) .stream() .map(V3BuildMapper::toCloudBuild) .toList(); } public List getBuildsForPackage(UUID packageGuid) { - String uri = "/v3/builds?package_guids=" + packageGuid + "&per_page=" + MAX_PAGE_SIZE; + String uri = CloudControllerV3Endpoints.BUILDS + CloudControllerV3Endpoints.QUERY_PACKAGE_GUIDS + packageGuid + + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE; return cc.list(uri, new ParameterizedTypeReference>() { - }) + }) .stream() .map(V3BuildMapper::toCloudBuild) .toList(); diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ClientConfigurationOptions.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ClientConfigurationOptions.java new file mode 100644 index 0000000000..5e9ef98e2d --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ClientConfigurationOptions.java @@ -0,0 +1,9 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.time.Duration; +import java.util.Optional; + +public record ClientConfigurationOptions(Optional connectionTimeout, Optional responseTimeout, + Optional sslHandshakeTimeout, Optional connectionPoolSize, + Optional threadPoolSize, boolean trustSelfSignedCertificates) { +} \ No newline at end of file diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java index e1cf3ba75c..754b658584 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientBuilder.java @@ -6,9 +6,16 @@ import java.util.List; import java.util.Map; import java.util.Optional; - +import javax.net.ssl.SSLException; import javax.net.ssl.X509TrustManager; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.channel.ChannelOption; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import org.cloudfoundry.multiapps.controller.Constants; +import org.cloudfoundry.multiapps.controller.Messages; +import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; import org.springframework.http.HttpHeaders; import org.springframework.http.client.ReactorClientHttpRequestFactory; import org.springframework.http.client.reactive.ReactorClientHttpConnector; @@ -17,91 +24,50 @@ import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.client.RestClient; -import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import io.netty.channel.ChannelOption; -import io.netty.handler.ssl.SslContextBuilder; -import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; import reactor.netty.http.client.HttpClient; import reactor.netty.resources.ConnectionProvider; import reactor.netty.resources.LoopResources; +import reactor.netty.tcp.SslProvider; -/** - * Builds the blocking Spring {@link RestClient} used by {@link CloudControllerRestClientV3Impl} to talk to the Cloud Controller v3 - * REST API directly, without the OSS cf-java-client. - *

- * The transport is Reactor-Netty (via Spring's {@link ReactorClientHttpRequestFactory}, which lives in {@code spring-web} — NOT - * {@code spring-webflux}). This is the same HTTP engine the deploy-service ran on under the OSS client, so its proven behavior - * (connection pooling, TLS, timeouts, proxy handling on the target landscapes) is preserved; only the cf-java-client library on top of it - * was removed. The tuning knobs the OSS {@code DefaultConnectionContext} honored are restored here: pool size (via {@link ConnectionProvider}), - * connect timeout, and response timeout. The caller still consumes everything synchronously through {@link RestClient} — Reactor-Netty is - * used purely as the transport, exactly as before. - *

- * Every request carries a bearer token from {@link OAuthClient} (refreshed as needed), and non-2xx responses are mapped to - * {@code CloudOperationException} by the existing {@link CloudControllerResponseErrorHandler}. - */ public final class CloudControllerRestClientBuilder { - private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofMinutes(1); - private static final int DEFAULT_CONNECTION_POOL_SIZE = 192; - private static final String CONNECTION_POOL_NAME = "cf-controller-client"; - private CloudControllerRestClientBuilder() { } - public static RestClient build(URL v3ApiUrl, OAuthClient oAuthClient, Map requestTags, TransportOptions options) { - return buildRestClient(buildHttpClient(options), v3ApiUrl, oAuthClient, requestTags); + public static RestClient build(URL v3ApiUrl, OAuthClient oAuthClient, Map requestTags, + ClientConfigurationOptions configOptions) { + return buildRestClient(buildHttpClient(configOptions), v3ApiUrl, oAuthClient, requestTags); } - /** - * Build a blocking {@link RestClient} on the given (already-configured) Reactor-Netty {@link HttpClient}. Sharing one - * {@code HttpClient} between the RestClient and the {@link #buildWebClient WebClient} means both ride the same connection pool, - * timeouts, TLS and proxy settings. - */ public static RestClient buildRestClient(HttpClient httpClient, URL baseUrl, OAuthClient oAuthClient, Map requestTags) { return RestClient.builder() .baseUrl(baseUrl.toString()) .requestFactory(new ReactorClientHttpRequestFactory(httpClient)) - // Pass a fully-formed converter list via the List overload — deliberately NOT the Consumer overload. - // messageConverters(Consumer) first calls initMessageConverters() to build Spring's DEFAULT list and only then - // hands it to the consumer; that default init eagerly constructs AllEncompassingFormHttpMessageConverter -> - // MappingJackson2YamlHttpMessageConverter -> Jackson2ObjectMapperBuilder.yaml(), which loads a YAMLFactory that is - // incompatible with this codebase's jackson-core (mixed Jackson 2/3 classpath) and throws VerifyError at load time - // — so clearing the list afterwards is too late. The List overload skips initMessageConverters() entirely. - // Converters: byte[] + String + JSON (from an ObjectMapper we control) for the v3 JSON APIs, plus a plain - // FormHttpMessageConverter for multipart/form-data (the POST /v3/packages/{guid}/upload app-bits upload). We use - // FormHttpMessageConverter, NOT AllEncompassingFormHttpMessageConverter, precisely because the latter is what - // triggers the YAML VerifyError above. .messageConverters(List.of(new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(), new FormHttpMessageConverter(), new MappingJackson2HttpMessageConverter(new ObjectMapper()))) .requestInterceptor((request, body, execution) -> { String authorization = oAuthClient.getAuthorizationHeaderValue(); + if (authorization != null) { request.getHeaders() .set(HttpHeaders.AUTHORIZATION, authorization); } - requestTags.forEach((key, value) -> request.getHeaders() - .set(key, value)); + + requestTags.forEach((String key, String value) -> request.getHeaders() + .set(key, value)); + return execution.execute(request, body); }) .defaultStatusHandler(new CloudControllerResponseErrorHandler()) .build(); } - /** - * Build a reactive {@link WebClient} on the same Reactor-Netty {@link HttpClient} as the RestClient, applying the identical - * per-request bearer auth (from {@link OAuthClient}, refreshed as needed) and request-tag headers. This is used only for the - * intra-operation concurrency spots (concurrent pagination) — the same mechanism the OSS client used — while the rest of the client - * stays on the blocking RestClient. Response error handling for these calls is done by the callers (they map WebClient errors the - * same way {@link CloudControllerResponseErrorHandler} does for the RestClient). - */ public static WebClient buildWebClient(HttpClient httpClient, URL baseUrl, OAuthClient oAuthClient, Map requestTags) { return WebClient.builder() @@ -113,90 +79,95 @@ public static WebClient buildWebClient(HttpClient httpClient, URL baseUrl, OAuth private static ExchangeFilterFunction authAndTagsFilter(OAuthClient oAuthClient, Map requestTags) { return (request, next) -> { - ClientRequest.Builder builder = ClientRequest.from(request); - String authorization = oAuthClient.getAuthorizationHeaderValue(); - if (authorization != null) { - builder.header(HttpHeaders.AUTHORIZATION, authorization); + ClientRequest.Builder requestBuilder = ClientRequest.from(request); + String authorizationValue = oAuthClient.getAuthorizationHeaderValue(); + + if (authorizationValue != null) { + requestBuilder.header(HttpHeaders.AUTHORIZATION, authorizationValue); } - requestTags.forEach(builder::header); - return next.exchange(builder.build()); + + requestTags.forEach((String key, String value) -> { + requestBuilder.header(key, value); + }); + + return next.exchange(requestBuilder.build()); }; } - static HttpClient buildHttpClient(TransportOptions options) { - int poolSize = options.connectionPoolSize() - .orElse(DEFAULT_CONNECTION_POOL_SIZE); - ConnectionProvider connectionProvider = ConnectionProvider.builder(CONNECTION_POOL_NAME) + static HttpClient buildHttpClient(ClientConfigurationOptions configOptions) { + int poolSize = configOptions.connectionPoolSize() + .orElse(Constants.DEFAULT_CONNECTION_POOL_SIZE); + ConnectionProvider connectionProvider = ConnectionProvider.builder(Constants.CONNECTION_POOL_NAME) .maxConnections(poolSize) .build(); HttpClient httpClient = HttpClient.create(connectionProvider) .followRedirect(true) - // Honor JVM standard proxy settings (-Dhttp(s).proxyHost/proxyPort/nonProxyHosts), matching the - // OSS DefaultConnectionContext's proxy support. No-op when no proxy is configured, so it is safe - // on direct-connect landscapes and required on proxied ones. .proxyWithSystemProperties(); - int connectTimeoutMillis = (int) options.connectTimeout() - .orElse(DEFAULT_CONNECT_TIMEOUT) - .toMillis(); + int connectTimeoutMillis = (int) configOptions.connectionTimeout() + .orElse(Constants.DEFAULT_CONNECT_TIMEOUT) + .toMillis(); httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMillis); - // Size the I/O event-loop pool from threadPoolSize, mirroring the OSS DefaultConnectionContext.threadPoolSize (which created a - // reactor-netty LoopResources the client ran on). When unset, reactor-netty uses its shared default LoopResources. - Optional threadPoolSize = options.threadPoolSize(); + + Optional threadPoolSize = configOptions.threadPoolSize(); if (threadPoolSize.isPresent()) { - LoopResources loopResources = LoopResources.create(CONNECTION_POOL_NAME + "-loop", threadPoolSize.get(), true); + LoopResources loopResources = LoopResources.create(Constants.CONNECTION_POOL_NAME + Constants.LOOP_RESOURCES_SUFFIX, + threadPoolSize.get(), true); httpClient = httpClient.runOn(loopResources); } - Optional responseTimeout = options.responseTimeout(); + + Optional responseTimeout = configOptions.responseTimeout(); if (responseTimeout.isPresent()) { httpClient = httpClient.responseTimeout(responseTimeout.get()); } - Optional handshakeTimeout = options.sslHandshakeTimeout(); - if (options.trustSelfSignedCertificates()) { - httpClient = httpClient.secure(spec -> { - var sslBuilder = spec.sslContext(buildTrustAllSslContext()); - handshakeTimeout.ifPresent(sslBuilder::handshakeTimeout); + + Optional handshakeTimeout = configOptions.sslHandshakeTimeout(); + if (configOptions.trustSelfSignedCertificates()) { + httpClient = httpClient.secure(sslSpecification -> { + SslProvider.Builder sslBuilder = sslSpecification.sslContext(buildTrustAllSslContext()); + if (handshakeTimeout.isPresent()) { + sslBuilder.handshakeTimeout(handshakeTimeout.get()); + } }); } else if (handshakeTimeout.isPresent()) { - httpClient = httpClient.secure(spec -> { - var sslBuilder = spec.sslContext(defaultClientSslContext()); + httpClient = httpClient.secure(sslSpecification -> { + SslProvider.Builder sslBuilder = sslSpecification.sslContext(buildDefaultClientSslContext()); sslBuilder.handshakeTimeout(handshakeTimeout.get()); }); } else { httpClient = httpClient.secure(); } + return httpClient; } - private static io.netty.handler.ssl.SslContext defaultClientSslContext() { + private static SslContext buildDefaultClientSslContext() { try { return SslContextBuilder.forClient() .build(); - } catch (javax.net.ssl.SSLException e) { - throw new IllegalStateException("An error occurred setting up the default SSLContext", e); + } catch (SSLException e) { + throw new IllegalStateException(Messages.ERROR_OCCURRED_SETTING_UP_DEFAULT_SSL_CONTEXT, e); } } - private static io.netty.handler.ssl.SslContext buildTrustAllSslContext() { + private static SslContext buildTrustAllSslContext() { try { return SslContextBuilder.forClient() - .trustManager(trustAllManager()) + .trustManager(getTrustAllCertificatesManager()) .build(); - } catch (javax.net.ssl.SSLException e) { - throw new IllegalStateException("An error occurred setting up the SSLContext", e); + } catch (SSLException e) { + throw new IllegalStateException(Messages.ERROR_OCCURRED_SETTING_UP_ALWAYS_APPROVING_SSL_CONTEXT, e); } } - private static X509TrustManager trustAllManager() { + private static X509TrustManager getTrustAllCertificatesManager() { return new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) { - // NOSONAR - self-signed certificate trust is opt-in via TransportOptions } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) { - // NOSONAR - self-signed certificate trust is opt-in via TransportOptions } @Override @@ -207,15 +178,4 @@ public X509Certificate[] getAcceptedIssuers() { }; } - /** - * The transport-tuning surface, honored by the Reactor-Netty transport: {@code connectTimeout} and {@code responseTimeout} on the - * client, {@code sslHandshakeTimeout} on the TLS layer, {@code connectionPoolSize} on the {@link ConnectionProvider}, and - * {@code threadPoolSize} on the I/O event-loop ({@link LoopResources}). Mirrors the knobs the OSS {@code DefaultConnectionContext} - * exposed. - */ - public record TransportOptions(Optional connectTimeout, Optional responseTimeout, - Optional sslHandshakeTimeout, Optional connectionPoolSize, - Optional threadPoolSize, boolean trustSelfSignedCertificates) { - } - } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java index b3f0087d86..32c8c45c33 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientFactory.java @@ -5,12 +5,16 @@ import java.net.URL; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.text.MessageFormat; import java.time.Duration; import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import org.cloudfoundry.multiapps.controller.Constants; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; import org.cloudfoundry.multiapps.controller.client.facade.CloudException; import org.cloudfoundry.multiapps.controller.client.facade.adapters.LogCacheClient; @@ -19,26 +23,16 @@ import org.cloudfoundry.multiapps.controller.client.facade.util.JsonUtil; import org.cloudfoundry.multiapps.controller.client.facade.util.RestUtil; import org.immutables.value.Value; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; import reactor.netty.http.client.HttpClient; -/** - * Builds the in-house {@link CloudControllerRestClient} (and the auxiliary {@link CloudSpaceClient} / {@link LogCacheClient}) that talk to - * the Cloud Controller v3 REST API directly through a blocking Spring {@link RestClient}, with no dependency on the OSS cf-java-client. - */ @Value.Immutable public abstract class CloudControllerRestClientFactory { - private static final Logger LOGGER = LoggerFactory.getLogger(CloudControllerRestClientFactory.class); - private final RestUtil restUtil = new RestUtil(); - // Per-host transport cache — mirrors the OSS connectionContextCache. One shared reactor-netty HttpClient (pool + TLS) and one root-link - // discovery per Cloud Controller host, reused across the CF / space / log-cache clients. The per-call OAuth token and request tags are - // NOT cached here; they are applied per RestClient/WebClient build (cheap), so caching the transport is safe. - private final Map hostTransports = new ConcurrentHashMap<>(); + + private final Map cachedHttpConnectionPair = new ConcurrentHashMap<>(); public abstract Optional getSslHandshakeTimeout(); @@ -63,139 +57,143 @@ public CloudControllerRestClient createClient(URL controllerUrl, CloudCredential } public CloudControllerRestClient createClient(URL controllerUrl, CloudCredentials credentials, CloudSpace target) { - return createClient(controllerUrl, credentials, target, createOAuthClient(controllerUrl, credentials.getOrigin()), + return createClient(controllerUrl, credentials, target, createOAuthClient(controllerUrl), Collections.emptyMap()); } public CloudControllerRestClient createClient(URL controllerUrl, CloudCredentials credentials, CloudSpace target, OAuthClient oAuthClient, Map requestTags) { oAuthClient.init(credentials); - HostTransport transport = hostTransport(controllerUrl, requestTags); - URL v3ApiUrl = transport.v3ApiUrl(); - URL origin = toOrigin(v3ApiUrl); - HttpClient httpClient = transport.httpClient(); - RestClient restClient = CloudControllerRestClientBuilder.buildRestClient(httpClient, origin, oAuthClient, requestTags); - WebClient webClient = CloudControllerRestClientBuilder.buildWebClient(httpClient, origin, oAuthClient, requestTags); - CloudControllerV3Client cc = new CloudControllerV3Client(restClient, webClient); - // Per-upload client factory: the app-bits upload (POST /v3/packages/{guid}/upload) must be bounded by the caller's - // per-MTA uploadTimeout, not the shared 900s response timeout. Build a dedicated RestClient whose response timeout is - // that uploadTimeout, mirroring the OSS startUpload's .timeout(uploadTimeout). Everything else (pool, connect, TLS) matches. - java.util.function.Function uploadRestClientFactory = uploadTimeout -> { - // Upload uses a one-off client with only its response timeout overridden; no dedicated event-loop pool (threadPoolSize empty) - // so it does not spin up its own LoopResources. - var uploadOptions = new CloudControllerRestClientBuilder.TransportOptions(getConnectTimeout(), Optional.of(uploadTimeout), + HttpConnectionPair httpConnectionPair = getCachedHttpConnectionPair(controllerUrl, requestTags); + URL v3ApiUrl = httpConnectionPair.v3ApiUrl(); + URL baseUrl = extractBaseUrl(v3ApiUrl); + + HttpClient httpClient = httpConnectionPair.httpClient(); + RestClient restClient = CloudControllerRestClientBuilder.buildRestClient(httpClient, baseUrl, oAuthClient, requestTags); + WebClient webClient = CloudControllerRestClientBuilder.buildWebClient(httpClient, baseUrl, oAuthClient, requestTags); + + CloudControllerV3Client client = new CloudControllerV3Client(restClient, webClient); + + Function uploadRestClientFactory = uploadTimeout -> { + ClientConfigurationOptions uploadOptions = new ClientConfigurationOptions(getConnectTimeout(), Optional.of(uploadTimeout), getSslHandshakeTimeout(), getConnectionPoolSize(), Optional.empty(), shouldTrustSelfSignedCertificates()); - return CloudControllerRestClientBuilder.build(origin, oAuthClient, requestTags, uploadOptions); + return CloudControllerRestClientBuilder.build(baseUrl, oAuthClient, requestTags, uploadOptions); }; - return new CloudControllerRestClientV3Impl(v3ApiUrl, oAuthClient, target, restClient, cc, uploadRestClientFactory); + + return new CloudControllerRestClientV3Impl(target, client, uploadRestClientFactory); } public CloudSpaceClient createSpaceClient(URL controllerUrl, OAuthClient oAuthClient, Map requestTags) { - HostTransport transport = hostTransport(controllerUrl, requestTags); - URL origin = toOrigin(transport.v3ApiUrl()); - RestClient restClient = CloudControllerRestClientBuilder.buildRestClient(transport.httpClient(), origin, oAuthClient, requestTags); - WebClient webClient = CloudControllerRestClientBuilder.buildWebClient(transport.httpClient(), origin, oAuthClient, requestTags); + HttpConnectionPair httpConnectionPair = getCachedHttpConnectionPair(controllerUrl, requestTags); + URL baseUrl = extractBaseUrl(httpConnectionPair.v3ApiUrl()); + + RestClient restClient = CloudControllerRestClientBuilder.buildRestClient(httpConnectionPair.httpClient(), baseUrl, oAuthClient, + requestTags); + WebClient webClient = CloudControllerRestClientBuilder.buildWebClient(httpConnectionPair.httpClient(), baseUrl, oAuthClient, + requestTags); + return new CloudSpaceClient(new CloudControllerV3Client(restClient, webClient)); } public LogCacheClient createLogCacheClient(URL controllerUrl, OAuthClient oAuthClient, Map requestTags) { - HostTransport transport = hostTransport(controllerUrl, requestTags); - RestClient restClient = CloudControllerRestClientBuilder.buildRestClient(transport.httpClient(), transport.logCacheUrl(), + HttpConnectionPair httpConnectionPair = getCachedHttpConnectionPair(controllerUrl, requestTags); + RestClient restClient = CloudControllerRestClientBuilder.buildRestClient(httpConnectionPair.httpClient(), + httpConnectionPair.logCacheUrl(), oAuthClient, requestTags); return new LogCacheClient(restClient); } - // Get (or lazily build) the shared per-host transport: one reactor-netty HttpClient + the resolved v3 / log-cache URLs, discovered - // once per host. Keyed on the controller host so the CF / space / log-cache clients for the same landscape share the pool. - private HostTransport hostTransport(URL controllerUrl, Map requestTags) { - return hostTransports.computeIfAbsent(controllerUrl.getHost(), host -> { - HttpClient httpClient = CloudControllerRestClientBuilder.buildHttpClient(transportOptions()); - URL v3ApiUrl = deriveV3ApiUrl(controllerUrl, requestTags); - URL logCacheUrl = deriveLogCacheUrl(controllerUrl, requestTags); - return new HostTransport(httpClient, v3ApiUrl, logCacheUrl); + private HttpConnectionPair getCachedHttpConnectionPair(URL controllerUrl, Map requestTags) { + return cachedHttpConnectionPair.computeIfAbsent(controllerUrl.getHost(), host -> { + HttpClient httpClient = CloudControllerRestClientBuilder.buildHttpClient(getClientConfigurationOptions()); + URL v3ApiUrl = resolveV3ApiUrl(controllerUrl, requestTags); + URL logCacheUrl = resolveLogCacheUrl(controllerUrl, requestTags); + return new HttpConnectionPair(httpClient, v3ApiUrl, logCacheUrl); }); } - private CloudControllerRestClientBuilder.TransportOptions transportOptions() { - return new CloudControllerRestClientBuilder.TransportOptions(getConnectTimeout(), getResponseTimeout(), getSslHandshakeTimeout(), - getConnectionPoolSize(), getThreadPoolSize(), - shouldTrustSelfSignedCertificates()); + private ClientConfigurationOptions getClientConfigurationOptions() { + return new ClientConfigurationOptions(getConnectTimeout(), getResponseTimeout(), getSslHandshakeTimeout(), + getConnectionPoolSize(), getThreadPoolSize(), + shouldTrustSelfSignedCertificates()); } - // scheme://host[:port] of the given URL, dropping any path (e.g. the trailing /v3). The RestClient baseUrl must be the origin only, - // because every operation path already carries the "/v3/..." prefix. - private URL toOrigin(URL url) { + private URL extractBaseUrl(URL url) { try { int port = url.getPort(); - String origin = url.getProtocol() + "://" + url.getHost() + (port == -1 ? "" : ":" + port); + String origin = url.getProtocol() + Constants.PROTOCOL_SEPARATOR + url.getHost() + (port == Constants.UNDEFINED_PORT + ? Constants.EMPTY_STRING + : Constants.COLON + port); return URI.create(origin) .toURL(); } catch (MalformedURLException e) { - throw new IllegalArgumentException("Could not derive the API origin from " + url, e); + throw new IllegalArgumentException(MessageFormat.format(Messages.COULD_NOT_DETERMINE_API_ORIGIN_FROM_0, url), e); } } - // Resolve the CF v3 API base URL from the CF root "cloud_controller_v3" link; fall back to controllerUrl + "/v3". - @SuppressWarnings("unchecked") - private URL deriveV3ApiUrl(URL controllerUrl, Map requestTags) { + private URL resolveV3ApiUrl(URL controllerUrl, Map requestTags) { try { - Map links = fetchRootLinks(controllerUrl, requestTags); - Map ccv3 = (Map) links.get("cloud_controller_v3"); - return URI.create((String) ccv3.get("href")) + Map rootDocumentLinks = getCloudControllerRootDocumentLinks(controllerUrl, requestTags); + Map cloudControllerUrlV3 = (Map) rootDocumentLinks.get( + Constants.CLOUD_CONTROLLER_CF_ROOT_DOCUMENT_NAME); + return URI.create((String) cloudControllerUrlV3.get(Constants.HREF)) .toURL(); } catch (Exception e) { - return fallbackUrl(controllerUrl, "/v3"); + return fallbackUrl(controllerUrl, Constants.CF_API_V3); } } - // Log-Cache base URL from the CF root "log_cache" link; fall back to the "log-cache" subdomain of the controller host. - @SuppressWarnings("unchecked") - private URL deriveLogCacheUrl(URL controllerUrl, Map requestTags) { + private URL resolveLogCacheUrl(URL controllerUrl, Map requestTags) { try { - Map links = fetchRootLinks(controllerUrl, requestTags); - Map logCache = (Map) links.get("log_cache"); - return URI.create((String) logCache.get("href")) + Map rootDocumentLinks = getCloudControllerRootDocumentLinks(controllerUrl, requestTags); + Map logCacheUrl = (Map) rootDocumentLinks.get(Constants.LOG_CACHE_CF_ROOT_DOCUMENT_NAME); + return URI.create((String) logCacheUrl.get(Constants.HREF)) .toURL(); } catch (Exception e) { String host = controllerUrl.getHost(); - String logCacheHost = host.startsWith("api.") ? "log-cache." + host.substring("api.".length()) : "log-cache." + host; + String logCacheHost = host.startsWith(Constants.API_HOST_PREFIX) ? Constants.LOG_CACHE_PREFIX + host.substring( + Constants.API_HOST_PREFIX.length()) : Constants.LOG_CACHE_PREFIX + host; try { - return URI.create(controllerUrl.getProtocol() + "://" + logCacheHost) + return URI.create(controllerUrl.getProtocol() + Constants.PROTOCOL_SEPARATOR + logCacheHost) .toURL(); } catch (MalformedURLException me) { - throw new CloudException("Could not derive the log-cache URL from " + controllerUrl, me); + throw new CloudException(MessageFormat.format(Messages.COULD_NOT_RESOLVE_LOG_CACHE_URL_FROM_0, controllerUrl), me); } } } @SuppressWarnings("unchecked") - private Map fetchRootLinks(URL controllerUrl, Map requestTags) { + private Map getCloudControllerRootDocumentLinks(URL controllerUrl, Map requestTags) { try { HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .GET() .uri(controllerUrl.toURI()) - .timeout(Duration.ofMinutes(1)); + .timeout(Constants.DEFAULT_CONNECT_TIMEOUT); requestTags.forEach(requestBuilder::header); - HttpResponse response = rootHttpClient().send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); + HttpResponse response = getSimpleHttpClient().send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() / 100 != 2) { - throw new CloudException("CF root request to " + controllerUrl + " returned " + response.statusCode()); + throw new CloudException( + MessageFormat.format(Messages.CF_ROOT_DOCUMENT_REQUEST_TO_0_RETURNED_1, controllerUrl, response.statusCode())); } + return (Map) JsonUtil.convertJsonToMap(response.body()) - .get("links"); + .get(Constants.ROOT_DOCUMENT_LINKS_LIST); } catch (InterruptedException e) { Thread.currentThread() .interrupt(); - throw new CloudException("Interrupted while calling the CF root at " + controllerUrl, e); + throw new CloudException(MessageFormat.format(Messages.INTERRUPTED_WHILE_CALLING_THE_CF_ROOT_URL_AT_0, controllerUrl), e); } catch (Exception e) { - throw new CloudException("Failed to call the CF root at " + controllerUrl + ": " + e.getMessage(), e); + throw new CloudException(MessageFormat.format(Messages.FAILED_TO_CALL_THE_CF_ROOT_AT_0_WITH_1, controllerUrl, e.getMessage()), + e); } } - private static java.net.http.HttpClient rootHttpClient() { + private static java.net.http.HttpClient getSimpleHttpClient() { return java.net.http.HttpClient.newBuilder() - .connectTimeout(Duration.ofMinutes(1)) + .connectTimeout(Constants.DEFAULT_CONNECT_TIMEOUT) .build(); } @@ -204,17 +202,13 @@ private URL fallbackUrl(URL controllerUrl, String pathSuffix) { return URI.create(controllerUrl.toString() + pathSuffix) .toURL(); } catch (MalformedURLException e) { - throw new IllegalArgumentException("Could not derive URL from " + controllerUrl + pathSuffix, e); + throw new IllegalArgumentException( + MessageFormat.format(Messages.COULD_NOT_RESOLVE_URL_FROM_0_1, controllerUrl, pathSuffix), e); } } - private OAuthClient createOAuthClient(URL controllerUrl, String origin) { + private OAuthClient createOAuthClient(URL controllerUrl) { return restUtil.createOAuthClientByControllerUrl(controllerUrl, shouldTrustSelfSignedCertificates()); } - // The cached, host-scoped transport: one shared reactor-netty HttpClient (pool + TLS + proxy) plus the CF root-link discovery results - // (v3 API + log-cache base URLs), resolved once per host. - private record HostTransport(HttpClient httpClient, URL v3ApiUrl, URL logCacheUrl) { - } - } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientV3Impl.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientV3Impl.java index 57a8b147e0..031d2c4960 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientV3Impl.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerRestClientV3Impl.java @@ -1,15 +1,16 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; -import java.net.URL; import java.nio.file.Path; +import java.text.MessageFormat; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.function.Function; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.UploadStatusCallback; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; @@ -31,103 +32,55 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.DockerInfo; import org.cloudfoundry.multiapps.controller.client.facade.domain.DropletInfo; import org.cloudfoundry.multiapps.controller.client.facade.domain.InstancesInfo; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.ServicePlanVisibility; import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; import org.cloudfoundry.multiapps.controller.client.facade.domain.Upload; import org.cloudfoundry.multiapps.controller.client.facade.domain.UserRole; import org.cloudfoundry.multiapps.controller.client.facade.dto.ApplicationToCreateDto; -import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; -import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Application; -import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ApplicationMapper; -import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; -import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; import org.springframework.web.client.RestClient; -/** - * A {@link CloudControllerRestClient} implementation that talks to the Cloud Controller v3 REST API directly through a blocking Spring - * {@link RestClient}, with no dependency on the OSS cf-java-client. - *

- * This is the target implementation of the "migrate off cf-java-client" PoC. It is selected in - * {@link CloudControllerRestClientFactory} via a feature flag; when the flag is off, the OSS-backed - * {@link CloudControllerRestClientImpl} is used instead, so the two can be A/B compared against the same live Cloud Foundry. - *

- * SCAFFOLD STATE: every operation currently throws {@link UnsupportedOperationException}. Operations are being filled in - * resource-group by resource-group per docs/cf-java-client-migration/02-endpoint-inventory.md. The shared machinery (pagination walker, - * async job poller, JSON model) lands first and is reused by every operation. - */ public class CloudControllerRestClientV3Impl implements CloudControllerRestClient { - // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). - private static final int DEFAULT_PAGE_SIZE = 5000; - // Matches the OSS impl's DELETE_JOB_TIMEOUT for async resource deletions. - private static final Duration DELETE_JOB_TIMEOUT = Duration.ofMinutes(5); - - private final URL v3ApiUrl; - private final OAuthClient oAuthClient; private final CloudSpace target; - private final RestClient restClient; - private final CloudControllerV3Client cc; - private final BuildsV3Operations buildsOps; - private final DomainsV3Operations domainsOps; - private final EventsV3Operations eventsOps; - private final JobsV3Operations jobsOps; - private final PackagesV3Operations packagesOps; - private final ProcessesV3Operations processesOps; - private final RolesV3Operations rolesOps; - private final RoutesV3Operations routesOps; - private final ServiceBindingsV3Operations serviceBindingsOps; - private final ServiceBrokersV3Operations serviceBrokersOps; - private final ServiceInstancesV3Operations serviceInstancesOps; - private final ServiceKeysV3Operations serviceKeysOps; - private final ServiceOfferingsV3Operations serviceOfferingsOps; - private final ServicePlansV3Operations servicePlansOps; - private final StacksV3Operations stacksOps; - private final TasksV3Operations tasksOps; - - public CloudControllerRestClientV3Impl(URL v3ApiUrl, OAuthClient oAuthClient, CloudSpace target, RestClient restClient) { - this(v3ApiUrl, oAuthClient, target, restClient, null); - } - - public CloudControllerRestClientV3Impl(URL v3ApiUrl, OAuthClient oAuthClient, CloudSpace target, RestClient restClient, - java.util.function.Function uploadRestClientFactory) { - this(v3ApiUrl, oAuthClient, target, restClient, new CloudControllerV3Client(restClient), uploadRestClientFactory); - } - - public CloudControllerRestClientV3Impl(URL v3ApiUrl, OAuthClient oAuthClient, CloudSpace target, RestClient restClient, - CloudControllerV3Client cc, - java.util.function.Function uploadRestClientFactory) { - this.v3ApiUrl = v3ApiUrl; - this.oAuthClient = oAuthClient; + + private final ApplicationsV3Operations applicationsOperations; + private final BuildsV3Operations buildsOperations; + private final DomainsV3Operations domainsOperations; + private final EventsV3Operations eventsOperations; + private final JobsV3Operations jobsOperations; + private final PackagesV3Operations packagesOperations; + private final ProcessesV3Operations processesOperations; + private final RolesV3Operations rolesOperations; + private final RoutesV3Operations routesOperations; + private final ServiceBindingsV3Operations serviceBindingsOperations; + private final ServiceBrokersV3Operations serviceBrokersOperations; + private final ServiceInstancesV3Operations serviceInstancesOperations; + private final ServiceKeysV3Operations serviceKeysOperations; + private final ServiceOfferingsV3Operations serviceOfferingsOperations; + private final StacksV3Operations stacksOperations; + private final TasksV3Operations tasksOperations; + + public CloudControllerRestClientV3Impl(CloudSpace target, CloudControllerV3Client cloudControllerClient, + Function uploadRestClientFactory) { this.target = target; - this.restClient = restClient; - this.cc = cc; - this.buildsOps = new BuildsV3Operations(cc, target); - this.domainsOps = new DomainsV3Operations(cc, target); - this.eventsOps = new EventsV3Operations(cc, target); - this.jobsOps = new JobsV3Operations(cc, target); - this.packagesOps = new PackagesV3Operations(cc, target, uploadRestClientFactory); - this.processesOps = new ProcessesV3Operations(cc, target); - this.rolesOps = new RolesV3Operations(cc, target); - this.routesOps = new RoutesV3Operations(cc, target); - this.serviceBindingsOps = new ServiceBindingsV3Operations(cc, target); - this.serviceBrokersOps = new ServiceBrokersV3Operations(cc, target); - this.serviceInstancesOps = new ServiceInstancesV3Operations(cc, target); - this.serviceKeysOps = new ServiceKeysV3Operations(cc, target); - this.serviceOfferingsOps = new ServiceOfferingsV3Operations(cc, target); - this.servicePlansOps = new ServicePlansV3Operations(cc, target); - this.stacksOps = new StacksV3Operations(cc, target); - this.tasksOps = new TasksV3Operations(cc, target); - } - - private static UnsupportedOperationException notImplemented() { - StackWalker.StackFrame caller = StackWalker.getInstance() - .walk(frames -> frames.skip(1) - .findFirst() - .orElse(null)); - String operation = caller == null ? "operation" : caller.getMethodName(); - return new UnsupportedOperationException( - "CloudControllerRestClientV3Impl." + operation + " is not implemented yet (cf-java-client migration PoC scaffold)."); + this.applicationsOperations = new ApplicationsV3Operations(cloudControllerClient, target); + this.buildsOperations = new BuildsV3Operations(cloudControllerClient); + this.domainsOperations = new DomainsV3Operations(cloudControllerClient, target); + this.eventsOperations = new EventsV3Operations(cloudControllerClient, target); + this.jobsOperations = new JobsV3Operations(cloudControllerClient); + this.packagesOperations = new PackagesV3Operations(cloudControllerClient, target, uploadRestClientFactory); + this.processesOperations = new ProcessesV3Operations(cloudControllerClient, target); + this.rolesOperations = new RolesV3Operations(cloudControllerClient); + this.routesOperations = new RoutesV3Operations(cloudControllerClient, target); + this.serviceBindingsOperations = new ServiceBindingsV3Operations(cloudControllerClient, target); + this.serviceBrokersOperations = new ServiceBrokersV3Operations(cloudControllerClient, target); + this.serviceInstancesOperations = new ServiceInstancesV3Operations(cloudControllerClient, target); + this.serviceKeysOperations = new ServiceKeysV3Operations(cloudControllerClient); + this.serviceOfferingsOperations = new ServiceOfferingsV3Operations(cloudControllerClient, target); + this.stacksOperations = new StacksV3Operations(cloudControllerClient); + this.tasksOperations = new TasksV3Operations(cloudControllerClient, target); } @Override @@ -137,454 +90,349 @@ public CloudSpace getTarget() { @Override public void addDomain(String domainName) { - domainsOps.addDomain(domainName); + domainsOperations.addDomain(domainName); } @Override public void addRoute(String host, String domainName, String path) { - routesOps.addRoute(host, domainName, path); + routesOperations.addRoute(host, domainName, path); } @Override public Optional bindServiceInstance(String bindingName, String applicationName, String serviceInstanceName) { - return serviceBindingsOps.bindServiceInstance(bindingName, applicationName, serviceInstanceName); + return serviceBindingsOperations.bindServiceInstance(bindingName, applicationName, serviceInstanceName); } @Override public Optional bindServiceInstance(String bindingName, String applicationName, String serviceInstanceName, Map parameters) { - return serviceBindingsOps.bindServiceInstance(bindingName, applicationName, serviceInstanceName, parameters); + return serviceBindingsOperations.bindServiceInstance(bindingName, applicationName, serviceInstanceName, parameters); } @Override public void createApplication(ApplicationToCreateDto dto) { - if (target == null || target.getGuid() == null) { - throw new CloudOperationException(HttpStatus.BAD_REQUEST, "Bad Request", "A target space is required to create an application."); - } - Map body = new java.util.HashMap<>(); - body.put("name", dto.getName()); - body.put("lifecycle", buildLifecycle(dto.getStaging())); - body.put("relationships", Map.of("space", Map.of("data", Map.of("guid", target.getGuid() - .toString())))); - if (dto.getEnv() != null) { - body.put("environment_variables", dto.getEnv()); - } - if (dto.getMetadata() != null) { - body.put("metadata", Map.of("labels", dto.getMetadata() - .getLabels(), - "annotations", dto.getMetadata() - .getAnnotations())); - } - V3Application created = cc.getRestClient() - .post() - .uri("/v3/apps") - .body(body) - .retrieve() - .body(V3Application.class); - UUID appGuid = UUID.fromString(created.guid()); - // Follow-up attributes, mirroring the OSS updateApplicationAttributes: staging/process, memory+disk scale, then routes. + applicationsOperations.createApplication(dto); + if (dto.getStaging() != null) { - processesOps.updateApplicationStaging(dto.getName(), dto.getStaging()); - } - Map scale = new java.util.HashMap<>(); - if (dto.getMemoryInMb() != null) { - scale.put("memory_in_mb", dto.getMemoryInMb()); - } - if (dto.getDiskQuotaInMb() != null) { - scale.put("disk_in_mb", dto.getDiskQuotaInMb()); - } - if (!scale.isEmpty()) { - scaleWebProcess(appGuid, scale); + updateApplicationStaging(dto.getName(), dto.getStaging()); } + if (dto.getRoutes() != null && !dto.getRoutes() .isEmpty()) { updateApplicationRoutes(dto.getName(), dto.getRoutes()); } } - // Builds the CF v3 lifecycle object: docker when DockerInfo is present, otherwise buildpack/cnb with buildpacks + stack. - private Map buildLifecycle(Staging staging) { - if (staging == null) { - return Map.of("type", "buildpack", "data", Map.of()); - } - if (staging.getDockerInfo() != null) { - return Map.of("type", "docker", "data", Map.of()); - } - String type = staging.getLifecycleType() != null ? staging.getLifecycleType() - .name() - .toLowerCase() - : "buildpack"; - Map data = new java.util.HashMap<>(); - if (staging.getBuildpacks() != null) { - data.put("buildpacks", staging.getBuildpacks()); - } - if (staging.getStackName() != null) { - data.put("stack", staging.getStackName()); - } - return Map.of("type", type, "data", data); - } - @Override public void createServiceInstance(CloudServiceInstance serviceInstance) { - serviceInstancesOps.createServiceInstance(serviceInstance); + serviceInstancesOperations.createServiceInstance(serviceInstance); } @Override public String createServiceBroker(CloudServiceBroker serviceBroker) { - return serviceBrokersOps.createServiceBroker(serviceBroker); + return serviceBrokersOperations.createServiceBroker(serviceBroker); } @Override public CloudServiceKey createAndFetchServiceKey(CloudServiceKey keyModel, String serviceInstanceName) { - return serviceKeysOps.createAndFetchServiceKey(keyModel, getServiceInstance(serviceInstanceName)); + return serviceKeysOperations.createAndFetchServiceKey(keyModel, getServiceInstance(serviceInstanceName)); } @Override public Optional createServiceKey(CloudServiceKey keyModel, String serviceInstanceName) { - return serviceKeysOps.createServiceKey(keyModel, getServiceInstance(serviceInstanceName)); + return serviceKeysOperations.createServiceKey(keyModel, getServiceInstance(serviceInstanceName)); } @Override public Optional createServiceKey(String serviceInstanceName, String serviceKeyName, Map parameters) { - return serviceKeysOps.createServiceKey(getServiceInstance(serviceInstanceName), serviceKeyName, parameters); + return serviceKeysOperations.createServiceKey(getServiceInstance(serviceInstanceName), serviceKeyName, parameters); } @Override public void createUserProvidedServiceInstance(CloudServiceInstance serviceInstance) { - serviceInstancesOps.createUserProvidedServiceInstance(serviceInstance); + serviceInstancesOperations.createUserProvidedServiceInstance(serviceInstance); } @Override public void deleteApplication(String applicationName) { - UUID applicationGuid = getApplicationGuid(applicationName); - var response = cc.getRestClient() - .delete() - .uri("/v3/apps/{guid}", applicationGuid) - .retrieve() - .toBodilessEntity(); - cc.followAsyncJob(response, DELETE_JOB_TIMEOUT); + applicationsOperations.deleteApplication(applicationName); } @Override public void deleteDomain(String domainName) { - domainsOps.deleteDomain(domainName); + domainsOperations.deleteDomain(domainName); } @Override public void deleteOrphanedRoutes() { - routesOps.deleteOrphanedRoutes(); + routesOperations.deleteOrphanedRoutes(); } @Override public void deleteRoute(String host, String domainName, String path) { - routesOps.deleteRoute(host, domainName, path); + routesOperations.deleteRoute(host, domainName, path); } @Override public void deleteServiceInstance(String serviceInstanceName) { - serviceInstancesOps.deleteServiceInstance(serviceInstanceName); + serviceInstancesOperations.deleteServiceInstance(serviceInstanceName); } @Override public void deleteServiceInstance(CloudServiceInstance serviceInstance) { - serviceInstancesOps.deleteServiceInstance(serviceInstance); + serviceInstancesOperations.deleteServiceInstance(serviceInstance); } @Override public String deleteServiceBroker(String name) { - return serviceBrokersOps.deleteServiceBroker(name); + return serviceBrokersOperations.deleteServiceBroker(name); } @Override public Optional deleteServiceBinding(String serviceInstanceName, String serviceKeyName) { - // Mirrors the OSS impl: resolve the named service key, then delete its binding by GUID. CloudServiceKey serviceKey = getServiceKey(serviceInstanceName, serviceKeyName); + if (serviceKey == null) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service key " + serviceKeyName + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.SERVICE_KEY_0_NOT_FOUND, serviceKeyName)); } + return deleteServiceBinding(serviceKey.getGuid()); } @Override public Optional deleteServiceBinding(UUID bindingGuid) { - return serviceBindingsOps.deleteServiceBinding(bindingGuid); + return serviceBindingsOperations.deleteServiceBinding(bindingGuid); } @Override public CloudApplication getApplication(String applicationName) { - return getApplication(applicationName, true); + return applicationsOperations.getApplication(applicationName); } @Override public CloudApplication getApplication(String applicationName, boolean required) { - V3Application app = findApplicationByName(applicationName); - if (app == null) { - if (required) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + applicationName + " not found."); - } - return null; - } - return V3ApplicationMapper.toCloudApplication(app, target); + return applicationsOperations.getApplication(applicationName, required); } @Override public UUID getApplicationGuid(String applicationName) { - return getApplication(applicationName).getGuid(); + return applicationsOperations.getApplicationGuid(applicationName); } @Override public String getApplicationName(UUID applicationGuid) { - V3Application app = cc.get("/v3/apps/" + applicationGuid, V3Application.class); - return app == null ? null : app.name(); + return applicationsOperations.getApplicationName(applicationGuid); } @Override public Map getApplicationEnvironment(UUID applicationGuid) { - V3Application.V3EnvironmentVariables env = cc.get("/v3/apps/" + applicationGuid + "/environment_variables", - V3Application.V3EnvironmentVariables.class); - return env == null || env.var() == null ? Map.of() : env.var(); + return applicationsOperations.getApplicationEnvironment(applicationGuid); } @Override public Map getApplicationEnvironment(String applicationName) { - return getApplicationEnvironment(getApplicationGuid(applicationName)); + return applicationsOperations.getApplicationEnvironment(applicationName); } @Override public List getApplications() { - return listApplications(applicationsQuery(null)).stream() - .map(app -> V3ApplicationMapper.toCloudApplication(app, target)) - .toList(); + return applicationsOperations.getApplications(); } @Override public List getApplicationsByMetadataLabelSelector(String labelSelector) { - String query = applicationsQuery(null); - if (labelSelector != null) { - query = query + "&label_selector=" + labelSelector; - } - return listApplications(query).stream() - .map(app -> V3ApplicationMapper.toCloudApplication(app, target)) - .toList(); - } - - private V3Application findApplicationByName(String applicationName) { - List apps = listApplications(applicationsQuery(applicationName)); - return apps.isEmpty() ? null : apps.get(0); - } - - private List listApplications(String query) { - return cc.list(query, new ParameterizedTypeReference>() { - }); - } - - // Applications in the target space, optionally filtered by exact name. - private String applicationsQuery(String name) { - StringBuilder query = new StringBuilder("/v3/apps?per_page=" + DEFAULT_PAGE_SIZE); - if (target != null && target.getGuid() != null) { - query.append("&space_guids=") - .append(target.getGuid()); - } - if (name != null) { - query.append("&names=") - .append(name); - } - return query.toString(); + return applicationsOperations.getApplicationsByMetadataLabelSelector(labelSelector); } @Override public List getApplicationEvents(String applicationName) { - return eventsOps.getApplicationEvents(applicationName); + return eventsOperations.getApplicationEvents(applicationName); } @Override public List getEventsByTarget(UUID uuid) { - return eventsOps.getEventsByTarget(uuid); + return eventsOperations.getEventsByTarget(uuid); } @Override public InstancesInfo getApplicationInstances(CloudApplication app) { - return processesOps.getApplicationInstances(app); + return processesOperations.getApplicationInstances(app); } @Override public InstancesInfo getApplicationInstances(UUID applicationGuid) { - return processesOps.getApplicationInstances(applicationGuid); + return processesOperations.getApplicationInstances(applicationGuid); } @Override public CloudProcess getApplicationProcess(UUID applicationGuid) { - return processesOps.getApplicationProcess(applicationGuid); + return processesOperations.getApplicationProcess(applicationGuid); } @Override public List getApplicationRoutes(UUID applicationGuid) { - return routesOps.getApplicationRoutes(applicationGuid); + return routesOperations.getApplicationRoutes(applicationGuid); } @Override public boolean getApplicationSshEnabled(UUID applicationGuid) { - return processesOps.getApplicationSshEnabled(applicationGuid); + return processesOperations.getApplicationSshEnabled(applicationGuid); } @Override public Map getApplicationFeatures(UUID applicationGuid) { - return processesOps.getApplicationFeatures(applicationGuid); + return processesOperations.getApplicationFeatures(applicationGuid); } @Override public CloudDomain getDefaultDomain() { - return domainsOps.getDefaultDomain(); + return domainsOperations.getDefaultDomain(); } @Override public List getDomains() { - return domainsOps.getDomains(); + return domainsOperations.getDomains(); } @Override public List getDomainsForOrganization() { - return domainsOps.getDomainsForOrganization(); + return domainsOperations.getDomainsForOrganization(); } @Override public List getEvents() { - return eventsOps.getEvents(); + return eventsOperations.getEvents(); } @Override public List getPrivateDomains() { - return domainsOps.getPrivateDomains(); + return domainsOperations.getPrivateDomains(); } @Override public List getRoutes(String domainName) { - return routesOps.getRoutes(domainName); + return routesOperations.getRoutes(domainName); } @Override public UUID getRequiredServiceInstanceGuid(String name) { - return serviceInstancesOps.getRequiredServiceInstanceGuid(name); + return serviceInstancesOperations.getRequiredServiceInstanceGuid(name); } @Override public CloudServiceInstance getServiceInstance(String serviceInstanceName) { - return serviceInstancesOps.getServiceInstance(serviceInstanceName); + return serviceInstancesOperations.getServiceInstance(serviceInstanceName); } @Override public CloudServiceInstance getServiceInstance(String serviceInstanceName, boolean required) { - return serviceInstancesOps.getServiceInstance(serviceInstanceName, required); + return serviceInstancesOperations.getServiceInstance(serviceInstanceName, required); } @Override public String getServiceInstanceName(UUID serviceInstanceGuid) { - return serviceInstancesOps.getServiceInstanceName(serviceInstanceGuid); + return serviceInstancesOperations.getServiceInstanceName(serviceInstanceGuid); } @Override public CloudServiceInstance getServiceInstanceWithoutAuxiliaryContent(String serviceInstanceName) { - return serviceInstancesOps.getServiceInstanceWithoutAuxiliaryContent(serviceInstanceName); + return serviceInstancesOperations.getServiceInstanceWithoutAuxiliaryContent(serviceInstanceName); } @Override public CloudServiceInstance getServiceInstanceWithoutAuxiliaryContent(String serviceInstanceName, boolean required) { - return serviceInstancesOps.getServiceInstanceWithoutAuxiliaryContent(serviceInstanceName, required); + return serviceInstancesOperations.getServiceInstanceWithoutAuxiliaryContent(serviceInstanceName, required); } @Override public CloudServiceBinding getServiceBinding(UUID serviceBindingGuid) { - return serviceBindingsOps.getServiceBinding(serviceBindingGuid); + return serviceBindingsOperations.getServiceBinding(serviceBindingGuid); } @Override public List getServiceAppBindings(UUID serviceInstanceGuid) { - return serviceBindingsOps.getServiceAppBindings(serviceInstanceGuid); + return serviceBindingsOperations.getServiceAppBindings(serviceInstanceGuid); } @Override public List getAppBindings(UUID applicationGuid) { - return serviceBindingsOps.getAppBindings(applicationGuid); + return serviceBindingsOperations.getAppBindings(applicationGuid); } @Override public List getServiceBindingsForApplication(UUID applicationId, UUID serviceInstanceGuid) { - return serviceBindingsOps.getServiceBindingsForApplication(applicationId, serviceInstanceGuid); + return serviceBindingsOperations.getServiceBindingsForApplication(applicationId, serviceInstanceGuid); } @Override public CloudServiceBroker getServiceBroker(String name) { - return serviceBrokersOps.getServiceBroker(name); + return serviceBrokersOperations.getServiceBroker(name); } @Override public CloudServiceBroker getServiceBroker(String name, boolean required) { - return serviceBrokersOps.getServiceBroker(name, required); + return serviceBrokersOperations.getServiceBroker(name, required); } @Override public List getServiceBrokers() { - return serviceBrokersOps.getServiceBrokers(); + return serviceBrokersOperations.getServiceBrokers(); } @Override public CloudServiceKey getServiceKey(String serviceInstanceName, String serviceKeyName) { - return serviceKeysOps.getServiceKey(getServiceInstance(serviceInstanceName), serviceKeyName); + return serviceKeysOperations.getServiceKey(getServiceInstance(serviceInstanceName), serviceKeyName); } @Override public List getServiceKeys(String serviceInstanceName) { - return serviceKeysOps.getServiceKeys(getServiceInstance(serviceInstanceName)); + return serviceKeysOperations.getServiceKeys(getServiceInstance(serviceInstanceName)); } @Override public List getServiceKeysWithCredentials(String serviceInstanceName) { - return serviceKeysOps.getServiceKeysWithCredentials(getServiceInstance(serviceInstanceName)); + return serviceKeysOperations.getServiceKeysWithCredentials(getServiceInstance(serviceInstanceName)); } @Override public List getServiceKeys(CloudServiceInstance serviceInstance) { - return serviceKeysOps.getServiceKeys(serviceInstance); + return serviceKeysOperations.getServiceKeys(serviceInstance); } @Override public List getServiceKeysWithCredentials(CloudServiceInstance serviceInstance) { - return serviceKeysOps.getServiceKeysWithCredentials(serviceInstance); + return serviceKeysOperations.getServiceKeysWithCredentials(serviceInstance); } @Override public List getServiceOfferings() { - return serviceOfferingsOps.getServiceOfferings(); + return serviceOfferingsOperations.getServiceOfferings(); } @Override public List getSharedDomains() { - return domainsOps.getSharedDomains(); + return domainsOperations.getSharedDomains(); } @Override public CloudStack getStack(String name) { - return stacksOps.getStack(name); + return stacksOperations.getStack(name); } @Override public CloudStack getStack(String name, boolean required) { - return stacksOps.getStack(name, required); + return stacksOperations.getStack(name, required); } @Override public List getStacks() { - return stacksOps.getStacks(); + return stacksOperations.getStacks(); } @Override public void startApplication(String applicationName) { - UUID guid = getApplicationGuid(applicationName); - cc.getRestClient() - .post() - .uri("/v3/apps/{guid}/actions/start", guid) - .retrieve() - .toBodilessEntity(); + applicationsOperations.startApplication(applicationName); } @Override @@ -595,257 +443,212 @@ public void restartApplication(String applicationName) { @Override public void stopApplication(String applicationName) { - UUID guid = getApplicationGuid(applicationName); - cc.getRestClient() - .post() - .uri("/v3/apps/{guid}/actions/stop", guid) - .retrieve() - .toBodilessEntity(); + applicationsOperations.stopApplication(applicationName); } @Override public void rename(String applicationName, String newName) { - UUID guid = getApplicationGuid(applicationName); - try { - cc.getRestClient() - .patch() - .uri("/v3/apps/{guid}", guid) - .body(Map.of("name", newName)) - .retrieve() - .toBodilessEntity(); - } catch (CloudOperationException e) { - // Idempotent-rename tolerance (mirrors the OSS fallbackApplicationRename): the CC can answer 503 to a rename that in fact - // already took effect. If the app is already named newName, treat the rename as successful; otherwise propagate. - if (e.getStatusCode() == HttpStatus.SERVICE_UNAVAILABLE && newName.equals(getApplicationName(guid))) { - return; - } - throw e; - } + applicationsOperations.rename(applicationName, newName); } @Override public void updateApplicationInstances(String applicationName, int instances) { - scaleWebProcess(getApplicationGuid(applicationName), Map.of("instances", instances)); + applicationsOperations.updateApplicationInstances(applicationName, instances); } @Override public void updateApplicationMemory(String applicationName, int memory) { - scaleWebProcess(getApplicationGuid(applicationName), Map.of("memory_in_mb", memory)); + applicationsOperations.updateApplicationMemory(applicationName, memory); } @Override public void updateApplicationDiskQuota(String applicationName, int disk) { - scaleWebProcess(getApplicationGuid(applicationName), Map.of("disk_in_mb", disk)); + applicationsOperations.updateApplicationDiskQuota(applicationName, disk); } @Override public void updateApplicationEnv(String applicationName, Map env) { - UUID guid = getApplicationGuid(applicationName); - cc.getRestClient() - .patch() - .uri("/v3/apps/{guid}/environment_variables", guid) - .body(Map.of("var", env)) - .retrieve() - .toBodilessEntity(); + applicationsOperations.updateApplicationEnv(applicationName, env); } @Override public void bindDropletToApp(UUID dropletGuid, UUID applicationGuid) { - cc.getRestClient() - .patch() - .uri("/v3/apps/{guid}/relationships/current_droplet", applicationGuid) - .body(Map.of("data", Map.of("guid", dropletGuid.toString()))) - .retrieve() - .toBodilessEntity(); - } - - private void scaleWebProcess(UUID applicationGuid, Map scaleBody) { - cc.getRestClient() - .post() - .uri("/v3/apps/{guid}/processes/web/actions/scale", applicationGuid) - .body(scaleBody) - .retrieve() - .toBodilessEntity(); + applicationsOperations.bindDropletToApp(dropletGuid, applicationGuid); } @Override public List unbindServiceInstance(String applicationName, String serviceInstanceName) { - return serviceBindingsOps.unbindServiceInstance(applicationName, serviceInstanceName); + return serviceBindingsOperations.unbindServiceInstance(applicationName, serviceInstanceName); } @Override public List unbindServiceInstance(UUID applicationGuid, UUID serviceInstanceGuid) { - return serviceBindingsOps.unbindServiceInstance(applicationGuid, serviceInstanceGuid); + return serviceBindingsOperations.unbindServiceInstance(applicationGuid, serviceInstanceGuid); } @Override public void updateApplicationStaging(String applicationName, Staging staging) { - processesOps.updateApplicationStaging(applicationName, staging); + processesOperations.updateApplicationStaging(applicationName, staging); } @Override public void updateApplicationRoutes(String applicationName, Set routes) { - routesOps.updateApplicationRoutes(applicationName, routes); + routesOperations.updateApplicationRoutes(applicationName, routes); } @Override public String updateServiceBroker(CloudServiceBroker serviceBroker) { - return serviceBrokersOps.updateServiceBroker(serviceBroker); + return serviceBrokersOperations.updateServiceBroker(serviceBroker); } @Override public void updateServicePlanVisibilityForBroker(String name, ServicePlanVisibility visibility) { - serviceBrokersOps.updateServicePlanVisibilityForBroker(name, visibility); + serviceBrokersOperations.updateServicePlanVisibilityForBroker(name, visibility); } @Override public void updateServicePlan(String serviceName, String planName) { - serviceInstancesOps.updateServicePlan(serviceName, planName); + serviceInstancesOperations.updateServicePlan(serviceName, planName); } @Override public void updateServiceParameters(String serviceName, Map parameters) { - serviceInstancesOps.updateServiceParameters(serviceName, parameters); + serviceInstancesOperations.updateServiceParameters(serviceName, parameters); } @Override public void updateServiceSyslogDrainUrl(String serviceName, String syslogDrainUrl) { - serviceInstancesOps.updateServiceSyslogDrainUrl(serviceName, syslogDrainUrl); + serviceInstancesOperations.updateServiceSyslogDrainUrl(serviceName, syslogDrainUrl); } @Override public void updateServiceTags(String serviceName, List tags) { - serviceInstancesOps.updateServiceTags(serviceName, tags); + serviceInstancesOperations.updateServiceTags(serviceName, tags); } @Override public CloudPackage asyncUploadApplication(String applicationName, Path file, UploadStatusCallback callback, Duration uploadTimeout) { - return packagesOps.asyncUploadApplication(applicationName, file, callback, uploadTimeout); + return packagesOperations.asyncUploadApplication(applicationName, file, callback, uploadTimeout); } @Override public Upload getUploadStatus(UUID packageGuid) { - return packagesOps.getUploadStatus(packageGuid); + return packagesOperations.getUploadStatus(packageGuid); } @Override public CloudTask getTask(UUID taskGuid) { - return tasksOps.getTask(taskGuid); + return tasksOperations.getTask(taskGuid); } @Override public List getTasks(String applicationName) { - return tasksOps.getTasks(applicationName); + return tasksOperations.getTasks(applicationName); } @Override public CloudTask runTask(String applicationName, CloudTask task) { - return tasksOps.runTask(applicationName, task); + return tasksOperations.runTask(applicationName, task); } @Override public CloudTask cancelTask(UUID taskGuid) { - return tasksOps.cancelTask(taskGuid); + return tasksOperations.cancelTask(taskGuid); } @Override public CloudBuild createBuild(UUID packageGuid) { - return buildsOps.createBuild(packageGuid); + return buildsOperations.createBuild(packageGuid); } @Override public CloudBuild getBuild(UUID packageGuid) { - return buildsOps.getBuild(packageGuid); + return buildsOperations.getBuild(packageGuid); } @Override public List getBuildsForApplication(UUID applicationGuid) { - return buildsOps.getBuildsForApplication(applicationGuid); + return buildsOperations.getBuildsForApplication(applicationGuid); } @Override public Map getServiceInstanceParameters(UUID guid) { - return serviceInstancesOps.getServiceInstanceParameters(guid); + return serviceInstancesOperations.getServiceInstanceParameters(guid); } @Override public Map getUserProvidedServiceInstanceParameters(UUID guid) { - return serviceInstancesOps.getUserProvidedServiceInstanceParameters(guid); + return serviceInstancesOperations.getUserProvidedServiceInstanceParameters(guid); } @Override public Map getServiceBindingParameters(UUID guid) { - return serviceBindingsOps.getServiceBindingParameters(guid); + return serviceBindingsOperations.getServiceBindingParameters(guid); } @Override public List getBuildsForPackage(UUID packageGuid) { - return buildsOps.getBuildsForPackage(packageGuid); + return buildsOperations.getBuildsForPackage(packageGuid); } @Override public List getServiceInstancesWithoutAuxiliaryContentByNames(List names) { - return serviceInstancesOps.getServiceInstancesWithoutAuxiliaryContentByNames(names); + return serviceInstancesOperations.getServiceInstancesWithoutAuxiliaryContentByNames(names); } @Override public List getServiceInstancesByMetadataLabelSelector(String labelSelector) { - return serviceInstancesOps.getServiceInstancesByMetadataLabelSelector(labelSelector); + return serviceInstancesOperations.getServiceInstancesByMetadataLabelSelector(labelSelector); } @Override public List getServiceInstancesWithoutAuxiliaryContentByMetadataLabelSelector(String labelSelector) { - return serviceInstancesOps.getServiceInstancesWithoutAuxiliaryContentByMetadataLabelSelector(labelSelector); + return serviceInstancesOperations.getServiceInstancesWithoutAuxiliaryContentByMetadataLabelSelector(labelSelector); } @Override public void updateApplicationMetadata(UUID guid, Metadata metadata) { - cc.getRestClient() - .patch() - .uri("/v3/apps/{guid}", guid) - .body(Map.of("metadata", Map.of("labels", metadata.getLabels(), "annotations", metadata.getAnnotations()))) - .retrieve() - .toBodilessEntity(); + applicationsOperations.updateApplicationMetadata(guid, metadata); } @Override public void updateServiceInstanceMetadata(UUID guid, Metadata metadata) { - serviceInstancesOps.updateServiceInstanceMetadata(guid, metadata); + serviceInstancesOperations.updateServiceInstanceMetadata(guid, metadata); } @Override public void updateServiceBindingMetadata(UUID guid, Metadata metadata) { - serviceBindingsOps.updateServiceBindingMetadata(guid, metadata); + serviceBindingsOperations.updateServiceBindingMetadata(guid, metadata); } @Override public DropletInfo getCurrentDropletForApplication(UUID applicationGuid) { - return processesOps.getCurrentDropletForApplication(applicationGuid); + return processesOperations.getCurrentDropletForApplication(applicationGuid); } @Override public CloudPackage getPackage(UUID packageGuid) { - return packagesOps.getPackage(packageGuid); + return packagesOperations.getPackage(packageGuid); } @Override public List getPackagesForApplication(UUID applicationGuid) { - return packagesOps.getPackagesForApplication(applicationGuid); + return packagesOperations.getPackagesForApplication(applicationGuid); } @Override public Set getUserRolesBySpaceAndUser(UUID spaceGuid, UUID userGuid) { - return rolesOps.getUserRolesBySpaceAndUser(spaceGuid, userGuid); + return rolesOperations.getUserRolesBySpaceAndUser(spaceGuid, userGuid); } @Override public CloudPackage createDockerPackage(UUID applicationGuid, DockerInfo dockerInfo) { - return packagesOps.createDockerPackage(applicationGuid, dockerInfo); + return packagesOperations.createDockerPackage(applicationGuid, dockerInfo); } @Override public CloudAsyncJob getAsyncJob(String jobId) { - return jobsOps.getAsyncJob(jobId); + return jobsOperations.getAsyncJob(jobId); } } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java index 3c8b53b46f..7ad492bafa 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Client.java @@ -1,54 +1,31 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; import java.net.URI; +import java.text.MessageFormat; import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import org.cloudfoundry.multiapps.controller.Constants; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudException; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Job; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; import org.springframework.web.util.UriComponentsBuilder; - import reactor.core.publisher.Flux; -/** - * Low-level Cloud Controller v3 access shared by every operation of {@link CloudControllerRestClientV3Impl}. This is the reusable - * machinery of the cf-java-client replacement: - *

    - *
  • typed {@code GET} / list helpers over the configured {@link RestClient} (error mapping is already attached to the RestClient via - * {@link CloudControllerResponseErrorHandler});
  • - *
  • a {@link #list(String, ParameterizedTypeReference) pagination walker} that follows {@code pagination.next.href} across pages;
  • - *
  • an {@link #waitForAsyncJob(String, Duration) async job poller} reproducing the OSS {@code JobV3Util} contract (exponential - * backoff 1s→15s, capped at a timeout, until COMPLETE/FAILED).
  • - *
- * It deals in the thin {@code resources.*} wire types; mapping those to the {@code domain.Cloud*} model happens in the per-resource - * operations that call this. - */ public class CloudControllerV3Client { - - // Mirrors the OSS JobV3Util backoff so async behaviour is unchanged. - private static final Duration JOB_POLL_MIN_INTERVAL = Duration.ofSeconds(1); - private static final Duration JOB_POLL_MAX_INTERVAL = Duration.ofSeconds(15); - private static final Duration DEFAULT_JOB_TIMEOUT = Duration.ofMinutes(5); - - // Reactor's default flatMap concurrency — the in-flight cap the OSS PaginationUtils effectively used when fetching pages 2..N. - private static final int MAX_CONCURRENT_PAGES = 256; - private final RestClient restClient; - // Optional reactive client on the SAME reactor-netty transport, used only to fetch pages 2..N concurrently (as the OSS client did via - // Reactor flatMap). When null (e.g. the auxiliary space/log-cache clients that never paginate large sets), list() falls back to the - // sequential next.href walk. + private final WebClient webClient; public CloudControllerV3Client(RestClient restClient) { @@ -67,10 +44,6 @@ public T get(String uri, Class responseType) { .body(responseType); } - /** - * GET a single resource, returning empty on a 404 rather than throwing — several callers rely on "not found" being a normal, - * non-exceptional outcome (the OSS impl expressed this with {@code onErrorResume} on NOT_FOUND). - */ public Optional getOptional(String uri, Class responseType) { try { return Optional.ofNullable(get(uri, responseType)); @@ -82,56 +55,52 @@ public Optional getOptional(String uri, Class responseType) { } } - /** - * List every resource of a v3 list endpoint across all pages. Mirrors the OSS {@code PaginationUtils.requestClientV3Resources}: - * fetch page 1, read {@code pagination.total_pages}, then request pages {@code 2..N} concurrently (bounded by - * {@value #MAX_CONCURRENT_PAGES}) via the reactive {@link WebClient} on the shared transport, and concatenate the results in page - * order. This restores the intra-operation concurrency the OSS client had on large, multi-page listings. - *

- * Falls back to the sequential {@code pagination.next.href} walk when no {@link WebClient} is configured or the response does not - * report {@code total_pages} — preserving correctness on any endpoint that omits it. - */ public List list(String firstPageUri, ParameterizedTypeReference> pageType) { String firstPageRelativeUri = toRelativeIfAbsolute(firstPageUri); + V3ListResponse firstPage = restClient.get() .uri(firstPageRelativeUri) .retrieve() .body(pageType); + if (firstPage == null) { return new ArrayList<>(); } + Integer totalPages = firstPage.pagination() == null ? null : firstPage.pagination() .totalPages(); + if (webClient == null || totalPages == null || totalPages <= 1) { return listSequentially(firstPage, pageType); } - List all = new ArrayList<>(firstPage.resources()); - all.addAll(fetchRemainingPagesConcurrently(firstPageRelativeUri, totalPages, pageType)); - return all; + + List resultFromAllPages = new ArrayList<>(firstPage.resources()); + resultFromAllPages.addAll(fetchRemainingPagesConcurrently(firstPageRelativeUri, totalPages, pageType)); + return resultFromAllPages; } - // Sequential pagination via pagination.next.href — the fallback path (and the whole behaviour when no WebClient is set). private List listSequentially(V3ListResponse firstPage, ParameterizedTypeReference> pageType) { - List all = new ArrayList<>(firstPage.resources()); + List resultFromAllPages = new ArrayList<>(firstPage.resources()); String nextUri = toRelativeIfAbsolute(firstPage.nextPageHref()); + while (nextUri != null) { - V3ListResponse page = restClient.get() - .uri(nextUri) - .retrieve() - .body(pageType); - if (page == null) { + V3ListResponse currentPage = restClient.get() + .uri(nextUri) + .retrieve() + .body(pageType); + + if (currentPage == null) { break; } - all.addAll(page.resources()); - nextUri = toRelativeIfAbsolute(page.nextPageHref()); + + resultFromAllPages.addAll(currentPage.resources()); + nextUri = toRelativeIfAbsolute(currentPage.nextPageHref()); } - return all; + + return resultFromAllPages; } - // Fetch pages 2..totalPages concurrently on the reactive transport, preserving page order (flatMapSequential), and flatten their - // resources. Errors propagate unwrapped, matching the RestClient error mapping (WebClient maps non-2xx to WebClientResponseException; - // we surface it as a CloudOperationException to keep callers' handling uniform). private List fetchRemainingPagesConcurrently(String firstPageRelativeUri, int totalPages, ParameterizedTypeReference> pageType) { return Flux.range(2, totalPages - 1) @@ -140,13 +109,12 @@ private List fetchRemainingPagesConcurrently(String firstPageRelativeUri, .retrieve() .bodyToMono(pageType) .onErrorMap(WebClientResponseException.class, this::toCloudOperationException), - MAX_CONCURRENT_PAGES) + Constants.MAX_CONCURRENT_PAGES) .flatMapIterable(V3ListResponse::resources) .collectList() .block(); } - // Set/replace the "page" query parameter on the first-page URI, keeping per_page and every filter intact. Package-visible for testing. static String pageUri(String firstPageRelativeUri, int page) { return UriComponentsBuilder.fromUriString(firstPageRelativeUri) .replaceQueryParam("page", page) @@ -157,53 +125,55 @@ static String pageUri(String firstPageRelativeUri, int page) { private CloudOperationException toCloudOperationException(WebClientResponseException e) { HttpStatus status = HttpStatus.valueOf(e.getStatusCode() .value()); + return new CloudOperationException(status, status.getReasonPhrase(), e.getResponseBodyAsString(), e); } - /** - * Follow the {@code Location} header of a 202-Accepted async response to its job, then poll to completion. - */ public void followAsyncJob(ResponseEntity acceptedResponse, Duration timeout) { URI location = acceptedResponse.getHeaders() .getLocation(); + if (location == null) { - return; // Operation completed synchronously (no job to poll). + return; // when location is null, the job has already completed and we can return immediately } - waitForAsyncJob(extractJobGuid(location.toString()), timeout); - } - public void waitForAsyncJob(String jobGuid) { - waitForAsyncJob(jobGuid, DEFAULT_JOB_TIMEOUT); + waitForAsyncJob(extractJobGuid(location.toString()), timeout); } - /** - * Poll {@code GET /v3/jobs/{guid}} until the job reaches a terminal state, with exponential backoff bounded by {@code timeout}. - * Throws {@link CloudOperationException} if the job fails, or if it does not complete within the timeout — matching the OSS - * {@code JobV3Util} contract. - */ public V3Job waitForAsyncJob(String jobGuid, Duration timeout) { - long deadlineNanos = System.nanoTime() + timeout.toNanos(); - Duration interval = JOB_POLL_MIN_INTERVAL; + long startTimeNanos = System.nanoTime(); + long timeoutNanos = timeout.toNanos(); + Duration interval = Constants.JOB_POLL_MIN_INTERVAL; + while (true) { - V3Job job = get("/v3/jobs/" + jobGuid, V3Job.class); - if (job != null && job.isTerminal()) { - if (job.isFailed()) { - throw jobFailed(job); + V3Job asyncJob = get("/v3/jobs/" + jobGuid, V3Job.class); + + if (asyncJob != null && asyncJob.isTerminal()) { + + if (asyncJob.isFailed()) { + throw jobFailed(asyncJob); } - return job; + + return asyncJob; } - if (System.nanoTime() >= deadlineNanos) { - throw new CloudOperationException(HttpStatus.GATEWAY_TIMEOUT, "Job timeout", - "Job " + jobGuid + " did not complete within " + timeout); + + long elapsedNanos = System.nanoTime() - startTimeNanos; + if (elapsedNanos >= timeoutNanos) { + throw new CloudOperationException(HttpStatus.GATEWAY_TIMEOUT, Messages.JOB_TIMEOUT, + MessageFormat.format(Messages.JOB_0_DID_NOT_COMPLETE_WITHIN_1, jobGuid, timeout)); } - sleep(interval); + + long remainingNanos = timeoutNanos - elapsedNanos; + Duration sleepDuration = interval.toNanos() > remainingNanos ? Duration.ofNanos(remainingNanos) : interval; + + sleep(sleepDuration); interval = nextInterval(interval); } } private static Duration nextInterval(Duration current) { Duration doubled = current.multipliedBy(2); - return doubled.compareTo(JOB_POLL_MAX_INTERVAL) > 0 ? JOB_POLL_MAX_INTERVAL : doubled; + return doubled.compareTo(Constants.JOB_POLL_MAX_INTERVAL) > 0 ? Constants.JOB_POLL_MAX_INTERVAL : doubled; } private static void sleep(Duration interval) { @@ -212,12 +182,13 @@ private static void sleep(Duration interval) { } catch (InterruptedException e) { Thread.currentThread() .interrupt(); - throw new CloudException("Interrupted while polling an async job", e); + throw new CloudException(MessageFormat.format(Messages.INTERRUPTED_WHILE_POLLING_ASYNC_JOB_0, e.getMessage()), e); } } private static CloudOperationException jobFailed(V3Job job) { String detail = "Job failed"; + if (job.errors() != null && !job.errors() .isEmpty()) { detail = job.errors() @@ -226,30 +197,37 @@ private static CloudOperationException jobFailed(V3Job job) { .reduce((a, b) -> a + "\n" + b) .orElse(detail); } - return new CloudOperationException(HttpStatus.UNPROCESSABLE_ENTITY, "Job failed", detail); + + return new CloudOperationException(HttpStatus.UNPROCESSABLE_ENTITY, Messages.JOB_FAILED, detail); } private static String extractJobGuid(String location) { - int idx = location.lastIndexOf("/v3/jobs/"); - if (idx < 0) { + int index = location.lastIndexOf("/v3/jobs/"); + + if (index < 0) { return location.substring(location.lastIndexOf('/') + 1); } - return location.substring(idx + "/v3/jobs/".length()); + + return location.substring(index + "/v3/jobs/".length()); } - // The next.href CF returns is absolute; RestClient has a baseUrl, so strip the origin to a path+query to keep requests baseUrl-relative. private String toRelativeIfAbsolute(String href) { if (href == null) { return null; } + URI uri = URI.create(href); + if (uri.isAbsolute()) { String pathAndQuery = uri.getRawPath(); + if (uri.getRawQuery() != null) { pathAndQuery = pathAndQuery + "?" + uri.getRawQuery(); } + return pathAndQuery; } + return href; } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Endpoints.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Endpoints.java new file mode 100644 index 0000000000..5cbce89a69 --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3Endpoints.java @@ -0,0 +1,88 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +/** + * The Cloud Controller v3 API surface used by the in-house CF client — resource roots, {@code {guid}} path templates, and query-parameter + * fragments — in one place, so the {@code V3Operations} classes reference named constants instead of inline "magic string" URLs. + *

+ * Two URI mechanisms are represented, matching how the operations classes issue requests: + *

    + *
  • Path templates (e.g. {@link #APP_BY_GUID}) contain Spring {@code {placeholder}}s expanded by {@code RestClient.uri(template, + * args...)} — the placeholders are intentionally kept literally.
  • + *
  • Roots + query fragments (e.g. {@link #APPS} + {@link #QUERY_PER_PAGE} + {@link #AMPERSAND_SPACE_GUIDS}) are concatenated by the caller + * into a query string. The fragments include their leading {@code ?}/{@code &} and trailing {@code =} exactly as the callers write them, + * so swapping a literal for a constant is a byte-identical substitution.
  • + *
+ */ +public final class CloudControllerV3Endpoints { + + private CloudControllerV3Endpoints() { + } + + public static final int DEFAULT_PAGE_SIZE = 5000; + + public static final String APPS = "/v3/apps"; + public static final String ROUTES = "/v3/routes"; + public static final String DOMAINS = "/v3/domains"; + public static final String SERVICE_INSTANCES = "/v3/service_instances"; + public static final String SERVICE_CREDENTIAL_BINDINGS = "/v3/service_credential_bindings"; + public static final String SERVICE_BROKERS = "/v3/service_brokers"; + public static final String SERVICE_OFFERINGS = "/v3/service_offerings"; + public static final String SERVICE_PLANS = "/v3/service_plans"; + public static final String PACKAGES = "/v3/packages"; + public static final String BUILDS = "/v3/builds"; + public static final String TASKS = "/v3/tasks"; + public static final String PROCESSES = "/v3/processes"; + public static final String STACKS = "/v3/stacks"; + public static final String AUDIT_EVENTS = "/v3/audit_events"; + public static final String JOBS = "/v3/jobs"; + public static final String ROLES = "/v3/roles"; + public static final String ORGANIZATIONS = "/v3/organizations"; + public static final String SPACES = "/v3/spaces"; + + public static final String APP_BY_GUID = "/v3/apps/{guid}"; + public static final String APP_START = "/v3/apps/{guid}/actions/start"; + public static final String APP_STOP = "/v3/apps/{guid}/actions/stop"; + public static final String APP_ENV_VARS = "/v3/apps/{guid}/environment_variables"; + public static final String APP_FEATURE = "/v3/apps/{guid}/features/{name}"; + public static final String APP_WEB_PROCESS_SCALE = "/v3/apps/{guid}/processes/web/actions/scale"; + public static final String APP_CURRENT_DROPLET = "/v3/apps/{guid}/relationships/current_droplet"; + public static final String APP_TASKS = "/v3/apps/{guid}/tasks"; + public static final String ROUTE_BY_GUID = "/v3/routes/{guid}"; + public static final String ROUTE_DESTINATIONS = "/v3/routes/{guid}/destinations"; + public static final String ROUTE_DESTINATION_BY_GUID = "/v3/routes/{routeGuid}/destinations/{destinationGuid}"; + public static final String DOMAIN_BY_GUID = "/v3/domains/{guid}"; + public static final String SERVICE_INSTANCE_BY_GUID = "/v3/service_instances/{guid}"; + public static final String SERVICE_BROKER_BY_GUID = "/v3/service_brokers/{guid}"; + public static final String SERVICE_CREDENTIAL_BINDING_BY_GUID = "/v3/service_credential_bindings/{guid}"; + public static final String SERVICE_PLAN_VISIBILITY = "/v3/service_plans/{guid}/visibility"; + public static final String PROCESS_BY_GUID = "/v3/processes/{guid}"; + public static final String TASK_CANCEL = "/v3/tasks/{guid}/actions/cancel"; + public static final String PACKAGE_UPLOAD = "/v3/packages/{guid}/upload"; + public static final String SPACE_UNMAPPED_ROUTES = "/v3/spaces/{guid}/routes?unmapped=true"; + + public static final String QUERY_PER_PAGE = "?per_page="; + public static final String QUERY_NAMES = "?names="; + public static final String QUERY_PACKAGE_GUIDS = "?package_guids="; + public static final String QUERY_APP_GUIDS = "?app_guids="; + public static final String QUERY_GUIDS = "?guids="; + public static final String QUERY_SERVICE_INSTANCE_GUIDS = "?service_instance_guids="; + public static final String QUERY_SERVICE_OFFERING_GUIDS = "?service_offering_guids="; + public static final String QUERY_SERVICE_BROKER_GUIDS = "?service_broker_guids="; + public static final String QUERY_ORGANIZATION_GUIDS = "?organization_guids="; + + public static final String AMPERSAND_PER_PAGE = "&per_page="; + public static final String AMPERSAND_SPACE_GUIDS = "&space_guids="; + public static final String AMPERSAND_NAMES = "&names="; + public static final String AMPERSAND_TYPE = "&type="; + public static final String AMPERSAND_LABEL_SELECTOR = "&label_selector="; + public static final String AMPERSAND_DOMAIN_GUIDS = "&domain_guids="; + public static final String AMPERSAND_SERVICE_INSTANCE_GUIDS = "&service_instance_guids="; + public static final String AMPERSAND_SERVICE_OFFERING_GUIDS = "&service_offering_guids="; + public static final String AMPERSAND_SERVICE_BROKER_NAMES = "&service_broker_names="; + public static final String AMPERSAND_APP_GUIDS = "&app_guids="; + public static final String AMPERSAND_USER_GUIDS = "&user_guids="; + public static final String AMPERSAND_TARGET_GUIDS = "&target_guids="; + public static final String AMPERSAND_HOSTS = "&hosts="; + public static final String AMPERSAND_PATHS = "&paths="; + +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudSpaceClient.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudSpaceClient.java index 2f860840b0..0644d62545 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudSpaceClient.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudSpaceClient.java @@ -1,8 +1,10 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; +import java.text.MessageFormat; import java.util.List; import java.util.UUID; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudOrganization; @@ -15,11 +17,6 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; -/** - * Resolves a target {@link CloudSpace} (space + its organization) by GUID or by org-name/space-name, talking directly to the CF v3 REST - * API through {@link CloudControllerV3Client}. Replaces the OSS cf-java-client-based implementation (which used {@code SpacesV3} / - * {@code OrganizationsV3}); behaviour and 404 semantics are preserved. - */ public class CloudSpaceClient { private static final List CHARS_TO_ENCODE = List.of(","); @@ -31,32 +28,44 @@ public CloudSpaceClient(CloudControllerV3Client cc) { } public CloudSpace getSpace(UUID spaceGuid) { - V3Space space = cc.getOptional("/v3/spaces/" + spaceGuid, V3Space.class) - .orElseThrow(() -> new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", - "Space with GUID " + spaceGuid + " not found.")); + V3Space space = cc.getOptional(CloudControllerV3Endpoints.SPACES + "/" + spaceGuid, V3Space.class) + .orElseThrow(() -> new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.SPACE_WITH_GUID_0_NOT_FOUND, + spaceGuid))); + String orgGuid = space.organizationGuid(); - V3Organization org = cc.getOptional("/v3/organizations/" + orgGuid, V3Organization.class) - .orElseThrow(() -> new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", - "Organization with GUID " + orgGuid + " not found.")); + V3Organization org = cc.getOptional(CloudControllerV3Endpoints.ORGANIZATIONS + "/" + orgGuid, V3Organization.class) + .orElseThrow(() -> new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format( + Messages.ORGANIZATION_WITH_GUID_0_NOT_FOUND, orgGuid))); + return mapToCloudSpace(space, org); } public CloudSpace getSpace(String organizationName, String spaceName) { - List orgs = cc.list("/v3/organizations?names=" + encodeAsQueryParam(organizationName), + List orgs = cc.list(CloudControllerV3Endpoints.ORGANIZATIONS + CloudControllerV3Endpoints.QUERY_NAMES + + encodeAsQueryParam(organizationName), new ParameterizedTypeReference>() { }); + if (orgs.isEmpty()) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Organization " + organizationName + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.ORGANISATION_0_NOT_FOUND, organizationName)); } + V3Organization org = orgs.get(0); - List spaces = cc.list("/v3/spaces?organization_guids=" + org.guid() + "&names=" + encodeAsQueryParam(spaceName), + List spaces = cc.list(CloudControllerV3Endpoints.SPACES + CloudControllerV3Endpoints.QUERY_ORGANIZATION_GUIDS + org.guid() + + CloudControllerV3Endpoints.AMPERSAND_NAMES + encodeAsQueryParam(spaceName), new ParameterizedTypeReference>() { }); + if (spaces.isEmpty()) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", - "Space " + spaceName + " not found in organization " + organizationName); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.SPACE_0_NOT_FOUND_IN_ORGANIZATION_1, spaceName, + organizationName)); } + return mapToCloudSpace(spaces.get(0), org); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/DomainsV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/DomainsV3Operations.java index f80b4c2ff9..f219954307 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/DomainsV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/DomainsV3Operations.java @@ -1,11 +1,13 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; -import java.time.Duration; +import java.text.MessageFormat; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; +import org.cloudfoundry.multiapps.controller.Constants; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudDomain; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEntity; @@ -18,27 +20,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; -/** - * CF v3 Domains operations of the in-house {@code CloudControllerRestClient}. Reproduces the HTTP shape, filtering and domain - * mapping of the OSS {@code CloudControllerRestClientImpl} domain methods on top of the shared {@link CloudControllerV3Client} machinery. - * - *

- * Endpoint map (see {@code docs/cf-java-client-migration/02-endpoint-inventory.md}): - *

- *
    - *
  • {@code addDomain} → {@code POST /v3/domains} (org-scoped, non-async)
  • - *
  • {@code deleteDomain} → {@code DELETE /v3/domains/{guid}} (async job)
  • - *
  • {@code getDefaultDomain} → {@code GET /v3/organizations/{guid}/domains/default}
  • - *
  • {@code getDomains} / {@code getSharedDomains} / {@code getPrivateDomains} → {@code GET /v3/domains} (paginated, filtered)
  • - *
  • {@code getDomainsForOrganization} → {@code GET /v3/organizations/{guid}/domains} (paginated)
  • - *
- */ public class DomainsV3Operations { - private static final int PER_PAGE = 5000; - // Mirrors the OSS DELETE_JOB_TIMEOUT (Duration.ofMinutes(5)) used for domain/route/service deletes. - private static final Duration DELETE_JOB_TIMEOUT = Duration.ofMinutes(5); - private static final ParameterizedTypeReference> DOMAIN_LIST_TYPE = new ParameterizedTypeReference<>() { }; @@ -50,50 +33,34 @@ public DomainsV3Operations(CloudControllerV3Client cc, CloudSpace target) { this.target = target; } - /** - * Create a private domain in the target organization, but only if a domain with that name does not already exist — matching the OSS - * {@code addDomain}, which no-ops when the domain is already present. - */ public void addDomain(String domainName) { assertSpaceProvided("add domain"); CloudDomain domain = findDomainByName(domainName); + if (domain == null) { doCreateDomain(domainName); } + } - /** - * Delete the (required) domain by name. Reproduces the OSS {@code deleteDomain}: a missing domain throws - * {@code CloudOperationException(NOT_FOUND)}, and the delete follows the async job to completion. - */ public void deleteDomain(String domainName) { assertSpaceProvided("delete domain"); CloudDomain domain = findDomainByName(domainName, true); doDeleteDomain(domain.getGuid()); } - /** - * {@code GET /v3/organizations/{guid}/domains/default} → {@link CloudDomain}. Non-paginated single fetch, mapping the OSS - * {@code getDefaultDomain}. - */ public CloudDomain getDefaultDomain() { - V3Domain domain = cc.get("/v3/organizations/" + getTargetOrganizationGuid() + "/domains/default", V3Domain.class); + V3Domain domain = cc.get(CloudControllerV3Endpoints.ORGANIZATIONS + "/" + getTargetOrganizationGuid() + "/domains/default", + V3Domain.class); return V3DomainMapper.toCloudDomain(domain); } - /** - * All domains visible to the target ({@code GET /v3/domains}). - */ public List getDomains() { return getAllDomains().stream() .map(V3DomainMapper::toCloudDomain) .collect(Collectors.toList()); } - /** - * Shared domains only — those whose {@code relationships.organization.data} is {@code null}, matching the OSS - * {@code getSharedDomainResources} filter. - */ public List getSharedDomains() { return getAllDomains().stream() .filter(domain -> !domain.isPrivate()) @@ -101,10 +68,6 @@ public List getSharedDomains() { .collect(Collectors.toList()); } - /** - * Private domains only — those whose {@code relationships.organization.data} is present, matching the OSS - * {@code getPrivateDomainResources} filter. - */ public List getPrivateDomains() { return getAllDomains().stream() .filter(V3Domain::isPrivate) @@ -112,13 +75,11 @@ public List getPrivateDomains() { .collect(Collectors.toList()); } - /** - * Domains available to the target organization ({@code GET /v3/organizations/{guid}/domains}), matching the OSS - * {@code getDomainsForOrganization}. - */ public List getDomainsForOrganization() { assertSpaceProvided("access organization domains"); - String uri = "/v3/organizations/" + getTargetOrganizationGuid() + "/domains?per_page=" + PER_PAGE; + String uri = CloudControllerV3Endpoints.ORGANIZATIONS + "/" + getTargetOrganizationGuid() + "/domains" + + CloudControllerV3Endpoints.QUERY_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE; + return cc.list(uri, DOMAIN_LIST_TYPE) .stream() .map(V3DomainMapper::toCloudDomain) @@ -126,19 +87,25 @@ public List getDomainsForOrganization() { } private List getAllDomains() { - return cc.list("/v3/domains?per_page=" + PER_PAGE, DOMAIN_LIST_TYPE); + return cc.list(CloudControllerV3Endpoints.DOMAINS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE, DOMAIN_LIST_TYPE); } private CloudDomain findDomainByName(String name, boolean required) { CloudDomain domain = findDomainByName(name); + if (domain == null && required) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Domain " + name + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.DOMAIN_0_NOT_FOUND, name)); } + return domain; } private CloudDomain findDomainByName(String name) { - String uri = "/v3/domains?names=" + name + "&per_page=" + PER_PAGE; + String uri = CloudControllerV3Endpoints.DOMAINS + CloudControllerV3Endpoints.QUERY_NAMES + name + + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE; + return cc.list(uri, DOMAIN_LIST_TYPE) .stream() .findFirst() @@ -149,7 +116,7 @@ private CloudDomain findDomainByName(String name) { private void doCreateDomain(String name) { cc.getRestClient() .post() - .uri("/v3/domains") + .uri(CloudControllerV3Endpoints.DOMAINS) .body(Map.of("name", name, "relationships", Map.of("organization", Map.of("data", Map.of("guid", getTargetOrganizationGuid().toString()))))) .retrieve() @@ -159,10 +126,10 @@ private void doCreateDomain(String name) { private void doDeleteDomain(UUID guid) { ResponseEntity response = cc.getRestClient() .delete() - .uri("/v3/domains/{guid}", guid.toString()) + .uri(CloudControllerV3Endpoints.DOMAIN_BY_GUID, guid.toString()) .retrieve() .toEntity(Void.class); - cc.followAsyncJob(response, DELETE_JOB_TIMEOUT); + cc.followAsyncJob(response, Constants.DELETE_JOB_TIMEOUT); } private UUID getTargetOrganizationGuid() { @@ -173,6 +140,7 @@ private UUID getGuid(CloudEntity entity) { if (entity == null || entity.getMetadata() == null) { return null; } + return entity.getMetadata() .getGuid(); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/EventsV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/EventsV3Operations.java index 1566597540..e348588f23 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/EventsV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/EventsV3Operations.java @@ -1,8 +1,10 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; +import java.text.MessageFormat; import java.util.List; import java.util.UUID; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEvent; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; @@ -13,23 +15,8 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; -/** - * CF v3 audit event operations for the cf-java-client replacement. Reproduces the HTTP shape and domain-mapping of the OSS - * {@code CloudControllerRestClientImpl} event methods: - *
    - *
  • {@code getEvents()} → {@code GET /v3/audit_events} (paginated);
  • - *
  • {@code getEventsByTarget(uuid)} → {@code GET /v3/audit_events?target_guids=} (paginated);
  • - *
  • {@code getApplicationEvents(name)} → resolve the app guid in the target space, then {@code getEventsByTarget}.
  • - *
- * Audit events are a global resource, so — like the OSS impl — the listings are not filtered by the target space (only by target guid where - * requested). Application-name resolution mirrors the OSS impl's {@code getRequiredApplicationGuid}: it queries the target space and throws - * {@link CloudOperationException} with {@link HttpStatus#NOT_FOUND} when the application does not exist. - */ public class EventsV3Operations { - // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). - private static final int DEFAULT_PAGE_SIZE = 5000; - private static final ParameterizedTypeReference> EVENT_PAGE = new ParameterizedTypeReference<>() { }; private static final ParameterizedTypeReference> APPLICATION_PAGE = new ParameterizedTypeReference<>() { @@ -44,14 +31,17 @@ public EventsV3Operations(CloudControllerV3Client cc, CloudSpace target) { } public List getEvents() { - return cc.list("/v3/audit_events?per_page=" + DEFAULT_PAGE_SIZE, EVENT_PAGE) + return cc.list(CloudControllerV3Endpoints.AUDIT_EVENTS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE, EVENT_PAGE) .stream() .map(V3AuditEventMapper::toCloudEvent) .toList(); } public List getEventsByTarget(UUID uuid) { - String query = "/v3/audit_events?per_page=" + DEFAULT_PAGE_SIZE + "&target_guids=" + uuid; + String query = CloudControllerV3Endpoints.AUDIT_EVENTS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE + CloudControllerV3Endpoints.AMPERSAND_TARGET_GUIDS + uuid; + return cc.list(query, EVENT_PAGE) .stream() .map(V3AuditEventMapper::toCloudEvent) @@ -60,23 +50,29 @@ public List getEventsByTarget(UUID uuid) { public List getApplicationEvents(String applicationName) { UUID applicationGuid = getRequiredApplicationGuid(applicationName); + return getEventsByTarget(applicationGuid); } - // Mirrors the OSS impl's getRequiredApplicationGuid: look up the app in the target space and 404 if it is absent. private UUID getRequiredApplicationGuid(String applicationName) { - StringBuilder query = new StringBuilder("/v3/apps?per_page=" + DEFAULT_PAGE_SIZE); + StringBuilder query = new StringBuilder(CloudControllerV3Endpoints.APPS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE); + if (target != null && target.getGuid() != null) { - query.append("&space_guids=") + query.append(CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS) .append(target.getGuid()); } - query.append("&names=") + + query.append(CloudControllerV3Endpoints.AMPERSAND_NAMES) .append(applicationName); List apps = cc.list(query.toString(), APPLICATION_PAGE); + if (apps.isEmpty()) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + applicationName + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.APPLICATION_0_NOT_FOUND, applicationName)); } - return UUID.fromString(apps.get(0) + + return UUID.fromString(apps.getFirst() .guid()); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/HttpConnectionPair.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/HttpConnectionPair.java new file mode 100644 index 0000000000..191f0e73ca --- /dev/null +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/HttpConnectionPair.java @@ -0,0 +1,8 @@ +package org.cloudfoundry.multiapps.controller.client.facade.rest; + +import java.net.URL; + +import reactor.netty.http.client.HttpClient; + +public record HttpConnectionPair(HttpClient httpClient, URL v3ApiUrl, URL logCacheUrl) { +} diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/JobsV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/JobsV3Operations.java index b2048dc6b3..0bb253ebe6 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/JobsV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/JobsV3Operations.java @@ -1,30 +1,20 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudAsyncJob; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Job; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3JobMapper; -/** - * CF v3 Jobs operations of the in-house {@code CloudControllerRestClient}. Reproduces the HTTP shape and domain mapping of the OSS - * {@code CloudControllerRestClientImpl#getAsyncJob} on top of the shared {@link CloudControllerV3Client} machinery. - */ public class JobsV3Operations { private final CloudControllerV3Client cc; - private final CloudSpace target; - public JobsV3Operations(CloudControllerV3Client cc, CloudSpace target) { + public JobsV3Operations(CloudControllerV3Client cc) { this.cc = cc; - this.target = target; } - /** - * {@code GET /v3/jobs/{guid}} → {@link CloudAsyncJob}. Non-paginated, synchronous fetch. A missing job yields a - * {@code CloudOperationException(NOT_FOUND)}, matching the OSS {@code fetch(...)} contract (which does not swallow 404s here). - */ public CloudAsyncJob getAsyncJob(String jobId) { - V3Job job = cc.get("/v3/jobs/" + jobId, V3Job.class); + V3Job job = cc.get(CloudControllerV3Endpoints.JOBS + "/" + jobId, V3Job.class); + return V3JobMapper.toCloudAsyncJob(job); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/PackagesV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/PackagesV3Operations.java index b336615131..d58fd4ba14 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/PackagesV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/PackagesV3Operations.java @@ -1,11 +1,18 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; import java.nio.file.Path; +import java.text.MessageFormat; import java.time.Duration; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; -import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.cloudfoundry.multiapps.controller.Constants; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.UploadStatusCallback; import org.cloudfoundry.multiapps.controller.client.facade.domain.BitsData; @@ -30,87 +37,77 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; -/** - * CF v3 packages operations of the cf-java-client replacement. Reproduces the HTTP shape and domain mapping of the OSS - * {@code CloudControllerRestClientImpl} package methods: - *
    - *
  • {@code getPackage(guid)} → {@code GET /v3/packages/{guid}};
  • - *
  • {@code getPackagesForApplication(appGuid)} → {@code GET /v3/apps/{guid}/packages} (paginated);
  • - *
  • {@code createDockerPackage(appGuid, dockerInfo)} → {@code POST /v3/packages} then re-fetch;
  • - *
  • {@code asyncUploadApplication(...)} → create a bits package ({@code POST /v3/packages}), - * upload the bits multipart ({@code POST /v3/packages/{guid}/upload}), then poll status in a background thread;
  • - *
  • {@code getUploadStatus(guid)} → derived from {@code getPackage(guid)}.
  • - *
- */ public class PackagesV3Operations { - // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). - private static final int DEFAULT_PAGE_SIZE = 5000; - // Mirrors the OSS CloudControllerRestClientImpl.PACKAGE_UPLOAD_JOB_POLLING_PERIOD. - private static final long PACKAGE_UPLOAD_JOB_POLLING_PERIOD = TimeUnit.SECONDS.toMillis(5); - private static final ParameterizedTypeReference> PACKAGE_PAGE = new ParameterizedTypeReference<>() { }; private final CloudControllerV3Client cc; private final CloudSpace target; - // Builds a RestClient whose response timeout is the given per-upload timeout, so a stalled bits upload is bounded by the - // user-configured UPLOAD_TIMEOUT (mirrors the OSS .timeout(uploadTimeout)). May be null, in which case the shared client is used. - private final java.util.function.Function uploadRestClientFactory; + + private final Function uploadRestClientFactory; public PackagesV3Operations(CloudControllerV3Client cc, CloudSpace target) { this(cc, target, null); } public PackagesV3Operations(CloudControllerV3Client cc, CloudSpace target, - java.util.function.Function uploadRestClientFactory) { + Function uploadRestClientFactory) { this.cc = cc; this.target = target; this.uploadRestClientFactory = uploadRestClientFactory; } public CloudPackage getPackage(UUID packageGuid) { - V3Package resource = cc.get("/v3/packages/" + packageGuid, V3Package.class); + V3Package resource = cc.get(CloudControllerV3Endpoints.PACKAGES + "/" + packageGuid, V3Package.class); + return resource == null ? null : V3PackageMapper.toCloudPackage(resource); } public List getPackagesForApplication(UUID applicationGuid) { - return cc.list("/v3/apps/" + applicationGuid + "/packages?per_page=" + DEFAULT_PAGE_SIZE, PACKAGE_PAGE) + return cc.list(CloudControllerV3Endpoints.APPS + "/" + applicationGuid + "/packages" + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE, PACKAGE_PAGE) .stream() .map(V3PackageMapper::toCloudPackage) .toList(); } public CloudPackage createDockerPackage(UUID applicationGuid, DockerInfo dockerInfo) { - java.util.Map data = new java.util.HashMap<>(); + Map data = new HashMap<>(); data.put("image", dockerInfo.getImage()); + DockerCredentials credentials = dockerInfo.getCredentials(); if (credentials != null) { if (credentials.getUsername() != null) { data.put("username", credentials.getUsername()); } + if (credentials.getPassword() != null) { data.put("password", credentials.getPassword()); } } + V3Package created = cc.getRestClient() .post() - .uri("/v3/packages") - .body(java.util.Map.of("type", "docker", "data", data, "relationships", - applicationRelationship(applicationGuid))) + .uri(CloudControllerV3Endpoints.PACKAGES) + .body(Map.of("type", "docker", "data", data, "relationships", + applicationRelationship(applicationGuid))) .retrieve() .body(V3Package.class); + return getPackage(UUID.fromString(created.guid())); } public Upload getUploadStatus(UUID packageGuid) { CloudPackage cloudPackage = getPackage(packageGuid); ErrorDetails errorDetails = null; + if (cloudPackage.getType() == CloudPackage.Type.BITS) { errorDetails = ImmutableErrorDetails.builder() .description(((BitsData) cloudPackage.getData()).getError()) .build(); } + return ImmutableUpload.builder() .status(cloudPackage.getStatus()) .errorDetails(errorDetails) @@ -132,11 +129,10 @@ private CloudPackage startUpload(String applicationName, Path file, Duration upl MultiValueMap body = new LinkedMultiValueMap<>(); body.add("bits", new FileSystemResource(file)); - // Bound the bits upload by the caller's per-MTA uploadTimeout via a dedicated RestClient whose response timeout is that value - // (see CloudControllerRestClientFactory). Fall back to the shared client if no factory/timeout was supplied. + RestClient uploadClient = resolveUploadClient(uploadTimeout); uploadClient.post() - .uri("/v3/packages/{guid}/upload", packageGuid) + .uri(CloudControllerV3Endpoints.PACKAGE_UPLOAD, packageGuid) .contentType(MediaType.MULTIPART_FORM_DATA) .body(body) .retrieve() @@ -149,38 +145,46 @@ private RestClient resolveUploadClient(Duration uploadTimeout) { if (uploadRestClientFactory != null && uploadTimeout != null && !uploadTimeout.isZero() && !uploadTimeout.isNegative()) { return uploadRestClientFactory.apply(uploadTimeout); } + return cc.getRestClient(); } private CloudPackage createBitsPackage(UUID applicationGuid) { V3Package created = cc.getRestClient() .post() - .uri("/v3/packages") - .body(java.util.Map.of("type", "bits", "relationships", applicationRelationship(applicationGuid))) + .uri(CloudControllerV3Endpoints.PACKAGES) + .body(Map.of("type", "bits", "relationships", applicationRelationship(applicationGuid))) .retrieve() .body(V3Package.class); + return getPackage(UUID.fromString(created.guid())); } - private static java.util.Map applicationRelationship(UUID applicationGuid) { - return java.util.Map.of("app", java.util.Map.of("data", java.util.Map.of("guid", applicationGuid.toString()))); + private static Map applicationRelationship(UUID applicationGuid) { + return Map.of("app", java.util.Map.of("data", java.util.Map.of("guid", applicationGuid.toString()))); } private UUID getRequiredApplicationGuid(String applicationName) { - StringBuilder query = new StringBuilder("/v3/apps?per_page=" + DEFAULT_PAGE_SIZE); + StringBuilder query = new StringBuilder(CloudControllerV3Endpoints.APPS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE); + if (target != null && target.getGuid() != null) { - query.append("&space_guids=") + query.append(CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS) .append(target.getGuid()); } - query.append("&names=") + + query.append(CloudControllerV3Endpoints.AMPERSAND_NAMES) .append(applicationName); List apps = cc.list(query.toString(), new ParameterizedTypeReference>() { }); - if (apps.isEmpty() || apps.get(0) + + if (apps.isEmpty() || apps.getFirst() .guid() == null) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + applicationName + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.APPLICATION_0_NOT_FOUND, applicationName)); } - return UUID.fromString(apps.get(0) + + return UUID.fromString(apps.getFirst() .guid()); } @@ -194,16 +198,19 @@ private void processAsyncUpload(CloudPackage cloudPackage, UploadStatusCallback Upload upload = getUploadStatus(cloudPackage.getGuid()); Status uploadStatus = upload.getStatus(); boolean unsubscribe = callback.onProgress(uploadStatus.toString()); + if (unsubscribe || isUploadReady(uploadStatus)) { return; } + if (hasUploadFailed(uploadStatus)) { callback.onError(upload.getErrorDetails() .getDescription()); return; } + try { - Thread.sleep(PACKAGE_UPLOAD_JOB_POLLING_PERIOD); + Thread.sleep(Constants.PACKAGE_UPLOAD_JOB_POLLING_PERIOD); } catch (InterruptedException e) { Thread.currentThread() .interrupt(); @@ -220,12 +227,8 @@ private boolean hasUploadFailed(Status status) { return status == Status.EXPIRED || status == Status.FAILED; } - /** - * Minimal wire-model of a v3 app resource, used only to resolve an application's GUID by name when starting an upload. The - * fully-featured app wire model lives in {@code V3Application}; this local record keeps the packages helper self-contained. - */ - @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) - private record V3App(@com.fasterxml.jackson.annotation.JsonProperty("guid") String guid) { + @JsonIgnoreProperties(ignoreUnknown = true) + private record V3App(@JsonProperty("guid") String guid) { } } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ProcessesV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ProcessesV3Operations.java index def82d3124..ed19e19222 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ProcessesV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ProcessesV3Operations.java @@ -1,11 +1,14 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; +import java.text.MessageFormat; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import org.cloudfoundry.multiapps.controller.Constants; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudProcess; @@ -24,29 +27,8 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; -/** - * CF v3 process-family operations for the cf-java-client replacement. Reproduces the HTTP shape and domain mapping of the - * OSS {@code CloudControllerRestClientImpl} process/instance/feature/droplet methods: - *
    - *
  • {@code getApplicationProcess(appGuid)} → {@code GET /v3/apps/{guid}/processes/web};
  • - *
  • {@code getApplicationInstances(app)} / {@code getApplicationInstances(appGuid)} → - * {@code GET /v3/apps/{guid}/processes/web/stats} (empty for a non-STARTED app);
  • - *
  • {@code getApplicationSshEnabled(appGuid)} → {@code GET /v3/apps/{guid}/ssh_enabled} (default {@code false});
  • - *
  • {@code getApplicationFeatures(appGuid)} → {@code GET /v3/apps/{guid}/features} (paginated);
  • - *
  • {@code getCurrentDropletForApplication(appGuid)} → {@code GET /v3/apps/{guid}/droplets/current} (404 → - * {@link CloudOperationException});
  • - *
  • {@code updateApplicationStaging(appName, staging)} → {@code PATCH /v3/apps/{guid}} (lifecycle) then update the web process - * ({@code PATCH /v3/apps/{guid}/features/{name}} per feature and {@code PATCH /v3/processes/{guid}}).
  • - *
- */ public class ProcessesV3Operations { - // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). - private static final int DEFAULT_PAGE_SIZE = 5000; - // The "package" link key in the current-droplet response; the package GUID is the last path segment of its href. - private static final String PACKAGE_LINK = "package"; - private static final String WEB_PROCESS_TYPE = "web"; - private final CloudControllerV3Client cc; private final CloudSpace target; @@ -56,8 +38,11 @@ public ProcessesV3Operations(CloudControllerV3Client cc, CloudSpace target) { } public CloudProcess getApplicationProcess(UUID applicationGuid) { - V3Process process = cc.get("/v3/apps/" + applicationGuid + "/processes/" + WEB_PROCESS_TYPE, V3Process.class); - return process == null ? null : V3ProcessMapper.toCloudProcess(process); + V3Process applicationProcess = cc.get( + CloudControllerV3Endpoints.APPS + "/" + applicationGuid + "/processes/" + Constants.WEB_PROCESS_TYPE, + V3Process.class); + + return applicationProcess == null ? null : V3ProcessMapper.toCloudProcess(applicationProcess); } public InstancesInfo getApplicationInstances(CloudApplication application) { @@ -65,6 +50,7 @@ public InstancesInfo getApplicationInstances(CloudApplication application) { .equals(CloudApplication.State.STARTED)) { return findApplicationInstances(application.getGuid()); } + return ImmutableInstancesInfo.builder() .instances(Collections.emptyList()) .build(); @@ -75,46 +61,60 @@ public InstancesInfo getApplicationInstances(UUID applicationGuid) { } private InstancesInfo findApplicationInstances(UUID applicationGuid) { - V3Process.V3ProcessStats stats = cc.get("/v3/apps/" + applicationGuid + "/processes/" + WEB_PROCESS_TYPE + "/stats", - V3Process.V3ProcessStats.class); + V3Process.V3ProcessStats stats = cc.get(CloudControllerV3Endpoints.APPS + "/" + applicationGuid + "/processes/" + + Constants.WEB_PROCESS_TYPE + "/stats", + V3Process.V3ProcessStats.class); + return V3InstancesInfoMapper.toInstancesInfo(stats); } public boolean getApplicationSshEnabled(UUID applicationGuid) { - V3Process.V3SshEnabled sshEnabled = cc.get("/v3/apps/" + applicationGuid + "/ssh_enabled", V3Process.V3SshEnabled.class); + V3Process.V3SshEnabled sshEnabled = cc.get(CloudControllerV3Endpoints.APPS + "/" + applicationGuid + "/ssh_enabled", + V3Process.V3SshEnabled.class); + if (sshEnabled == null || sshEnabled.enabled() == null) { return false; } + return sshEnabled.enabled(); } public Map getApplicationFeatures(UUID applicationGuid) { - List features = cc.list("/v3/apps/" + applicationGuid + "/features?per_page=" + DEFAULT_PAGE_SIZE, - new ParameterizedTypeReference>() { - }); + List applicationFeatures = cc.list(CloudControllerV3Endpoints.APPS + "/" + applicationGuid + "/features" + + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE, + new ParameterizedTypeReference>() { + }); + Map result = new HashMap<>(); - for (V3Process.V3AppFeature feature : features) { + for (V3Process.V3AppFeature feature : applicationFeatures) { result.put(feature.name(), feature.enabled()); } + return result; } public DropletInfo getCurrentDropletForApplication(UUID applicationGuid) { - V3Droplet droplet = cc.get("/v3/apps/" + applicationGuid + "/droplets/current", V3Droplet.class); - if (droplet == null) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not found", - "Application with guid " + applicationGuid + " does not have a droplet"); + V3Droplet currentDroplet = cc.get(CloudControllerV3Endpoints.APPS + "/" + applicationGuid + "/droplets/current", V3Droplet.class); + + if (currentDroplet == null) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.APPLICATION_WITH_GUID_0_DOES_NOT_HAVE_A_DROPLET, + applicationGuid)); } - return parseDropletInfo(droplet); + + return parseDropletInfo(currentDroplet); } private DropletInfo parseDropletInfo(V3Droplet droplet) { String packageUrl = droplet.links() - .get(PACKAGE_LINK) + .get(Constants.PACKAGE_LINK) .href(); + if (packageUrl.endsWith("/")) { packageUrl = packageUrl.substring(0, packageUrl.lastIndexOf("/")); } + String packageGuid = packageUrl.substring(packageUrl.lastIndexOf("/") + 1); return ImmutableDropletInfo.builder() .guid(UUID.fromString(droplet.guid())) @@ -124,54 +124,63 @@ private DropletInfo parseDropletInfo(V3Droplet droplet) { public void updateApplicationStaging(String applicationName, Staging staging) { UUID applicationGuid = getRequiredApplicationGuid(applicationName); + cc.getRestClient() .patch() - .uri("/v3/apps/{guid}", applicationGuid) + .uri(CloudControllerV3Endpoints.APP_BY_GUID, applicationGuid) .body(Map.of("lifecycle", buildApplicationLifecycle(staging))) .retrieve() .toBodilessEntity(); updateApplicationProcess(applicationGuid, staging); } - // Mirrors the OSS impl's buildApplicationLifecycle: Docker if DockerInfo present, otherwise a buildpack (or CNB) lifecycle. private Map buildApplicationLifecycle(Staging staging) { if (staging.getDockerInfo() != null) { return Map.of("type", "docker", "data", Map.of()); } + String lifecycleType = staging.getLifecycleType() != null ? staging.getLifecycleType() - .name() - .toLowerCase() - : "buildpack"; + .name() + .toLowerCase() + : "buildpack"; + if ("cnb".equals(lifecycleType) && (staging.getBuildpacks() == null || staging.getBuildpacks() .isEmpty())) { throw new IllegalArgumentException("Buildpacks are required for the CNB lifecycle type"); } + Map data = new HashMap<>(); if (staging.getStackName() != null) { data.put("stack", staging.getStackName()); } + if (staging.getBuildpacks() != null) { data.put("buildpacks", staging.getBuildpacks()); } + return Map.of("type", lifecycleType, "data", data); } - // Mirrors the OSS impl's updateApplicationProcess: toggle app features, then PATCH the web process command + health checks. private void updateApplicationProcess(UUID applicationGuid, Staging staging) { staging.getAppFeatures() .forEach((featureName, enabled) -> updateAppFeature(applicationGuid, featureName, enabled)); - V3Process process = cc.get("/v3/apps/" + applicationGuid + "/processes/" + WEB_PROCESS_TYPE, V3Process.class); + + V3Process process = cc.get(CloudControllerV3Endpoints.APPS + "/" + applicationGuid + "/processes/" + Constants.WEB_PROCESS_TYPE, + V3Process.class); + Map updateProcessBody = new HashMap<>(); updateProcessBody.put("command", staging.getCommand()); if (staging.getHealthCheckType() != null) { updateProcessBody.put("health_check", buildHealthCheck(staging)); } + if (staging.getReadinessHealthCheckType() != null) { updateProcessBody.put("readiness_health_check", buildReadinessHealthCheck(staging)); } + cc.getRestClient() .patch() - .uri("/v3/processes/{guid}", process.guid()) + .uri(CloudControllerV3Endpoints.PROCESS_BY_GUID, process.guid()) .body(updateProcessBody) .retrieve() .toBodilessEntity(); @@ -180,7 +189,7 @@ private void updateApplicationProcess(UUID applicationGuid, Staging staging) { private void updateAppFeature(UUID applicationGuid, String featureName, boolean enabled) { cc.getRestClient() .patch() - .uri("/v3/apps/{guid}/features/{name}", applicationGuid, featureName) + .uri(CloudControllerV3Endpoints.APP_FEATURE, applicationGuid, featureName) .body(Map.of("enabled", enabled)) .retrieve() .toBodilessEntity(); @@ -192,6 +201,7 @@ private Map buildHealthCheck(Staging staging) { putIfNotNull(data, "timeout", staging.getHealthCheckTimeout()); putIfNotNull(data, "invocation_timeout", staging.getInvocationTimeout()); putIfNotNull(data, "interval", staging.getHealthCheckInterval()); + Map healthCheck = new HashMap<>(); healthCheck.put("type", staging.getHealthCheckType()); healthCheck.put("data", data); @@ -203,6 +213,7 @@ private Map buildReadinessHealthCheck(Staging staging) { putIfNotNull(data, "invocation_timeout", staging.getReadinessHealthCheckInvocationTimeout()); putIfNotNull(data, "endpoint", staging.getReadinessHealthCheckHttpEndpoint()); putIfNotNull(data, "interval", staging.getReadinessHealthCheckInterval()); + Map readinessHealthCheck = new HashMap<>(); readinessHealthCheck.put("type", staging.getReadinessHealthCheckType()); readinessHealthCheck.put("data", data); @@ -215,22 +226,28 @@ private static void putIfNotNull(Map map, String key, Object val } } - // Mirrors the OSS impl's getRequiredApplicationGuid: look up the app in the target space and 404 if it is absent. private UUID getRequiredApplicationGuid(String applicationName) { - StringBuilder query = new StringBuilder("/v3/apps?per_page=" + DEFAULT_PAGE_SIZE); + StringBuilder query = new StringBuilder(CloudControllerV3Endpoints.APPS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE); + if (target != null && target.getGuid() != null) { - query.append("&space_guids=") + query.append(CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS) .append(target.getGuid()); } - query.append("&names=") + + query.append(CloudControllerV3Endpoints.AMPERSAND_NAMES) .append(applicationName); + List apps = cc.list(query.toString(), new ParameterizedTypeReference>() { }); + if (apps.isEmpty()) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + applicationName + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.APPLICATION_0_NOT_FOUND, applicationName)); } - return UUID.fromString(apps.get(0) + + return UUID.fromString(apps.getFirst() .guid()); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOut.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOut.java index 04a36caa1a..5743f7be7d 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOut.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOut.java @@ -3,49 +3,29 @@ import java.util.List; import java.util.function.Function; +import org.cloudfoundry.multiapps.controller.Constants; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; -/** - * Restores the intra-operation concurrency the OSS cf-java-client had for free. The OSS client composed per-item work - * (e.g. fetching credentials for N service keys, or resolving the plan/offering of N service instances) as a reactive graph and let - * Reactor's {@code flatMap} run those calls concurrently (default concurrency 256) before a single {@code .block()} at the boundary. - * The synchronous {@link RestClient}-based rewrite would otherwise do the same work strictly sequentially ({@code stream().map(fetch)}). - *

- * This helper reproduces exactly that pattern — {@code Flux.fromIterable(...).flatMap(...).block()} — reusing the existing - * blocking per-item mappers unchanged. Each blocking mapper is offloaded onto {@link Schedulers#boundedElastic()} so the calls - * run concurrently without pinning the caller's thread; {@code flatMap} bounds how many are in flight (Reactor's default 256, matching - * the OSS cap). Results are returned in input order ({@link Flux#flatMapSequential} preserves order while still subscribing - * eagerly), and the first error aborts the rest and propagates unwrapped — the same fail-fast semantics a {@code .block()} over a failed - * {@code flatMap} had in the OSS path. - *

- * This uses only Reactor (a Spring library already on the classpath as the transport); it does not reintroduce cf-java-client. - */ final class ReactiveFanOut { - // Reactor's default flatMap concurrency — the in-flight cap the OSS client effectively used for per-item fan-out. - private static final int MAX_CONCURRENCY = 256; - private ReactiveFanOut() { } - /** - * Apply {@code mapper} (a blocking function) to every item concurrently, bounded by {@value #MAX_CONCURRENCY} in-flight calls, and - * return the results in the same order as {@code items}. The single-element and empty cases short-circuit without touching the - * reactive machinery (the overwhelmingly common case for typical MTAs). - */ static List mapConcurrently(List items, Function mapper) { if (items.isEmpty()) { return List.of(); } + if (items.size() == 1) { - return List.of(mapper.apply(items.get(0))); + return List.of(mapper.apply(items.getFirst())); } + return Flux.fromIterable(items) .flatMapSequential(item -> Mono.fromCallable(() -> mapper.apply(item)) .subscribeOn(Schedulers.boundedElastic()), - MAX_CONCURRENCY) + Constants.DEFAULT_CONCURRENT_TASKS) .collectList() .block(); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RolesV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RolesV3Operations.java index 9a1102b6d6..4bf20566af 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RolesV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RolesV3Operations.java @@ -5,38 +5,28 @@ import java.util.UUID; import java.util.stream.Collectors; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.domain.UserRole; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Role; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3RoleMapper; import org.springframework.core.ParameterizedTypeReference; -/** - * CF v3 {@code Roles} operations for the in-house {@link CloudControllerRestClient} replacement. Implements the role-related methods of - * the interface by listing {@code GET /v3/roles} filtered by space and user, and mapping each wire {@link V3Role} to the project's - * {@link UserRole} domain enum via {@link V3RoleMapper}. - * - *

- * Mirrors the OSS {@code CloudControllerRestClientImpl#getUserRolesBySpaceAndUser}, which lists roles filtered by {@code space_id}, - * {@code user_id} and all role {@code types}, then collects the results into an {@link EnumSet}. - *

- */ public class RolesV3Operations { private static final ParameterizedTypeReference> ROLE_PAGE = new ParameterizedTypeReference<>() { }; private final CloudControllerV3Client cc; - private final CloudSpace target; - public RolesV3Operations(CloudControllerV3Client cc, CloudSpace target) { + public RolesV3Operations(CloudControllerV3Client cc) { this.cc = cc; - this.target = target; } public Set getUserRolesBySpaceAndUser(UUID spaceGuid, UUID userGuid) { - String uri = "/v3/roles?per_page=5000&space_guids=" + spaceGuid + "&user_guids=" + userGuid; + String uri = CloudControllerV3Endpoints.ROLES + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE + CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS + spaceGuid + + CloudControllerV3Endpoints.AMPERSAND_USER_GUIDS + userGuid; + return cc.list(uri, ROLE_PAGE) .stream() .map(V3RoleMapper::toUserRole) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RoutesV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RoutesV3Operations.java index ee9df786f2..c0363e6d90 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RoutesV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/RoutesV3Operations.java @@ -1,6 +1,6 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; -import java.time.Duration; +import java.text.MessageFormat; import java.util.List; import java.util.Map; import java.util.Objects; @@ -9,6 +9,10 @@ import java.util.UUID; import java.util.stream.Collectors; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.cloudfoundry.multiapps.controller.Constants; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudRoute; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; @@ -17,31 +21,12 @@ import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Route; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3RouteMapper; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -/** - * CF v3 routes operations of the cf-java-client replacement. Reproduces the HTTP shape, async handling and domain mapping of the - * OSS {@code CloudControllerRestClientImpl} route methods: - *
    - *
  • {@code addRoute(host, domainName, path)} → resolve the domain, then {@code POST /v3/routes} (idempotent: reuse an existing - * route if one already matches);
  • - *
  • {@code deleteRoute(host, domainName, path)} → resolve the route in the target space and {@code DELETE /v3/routes/{guid}} - * (async job), 404 if not found;
  • - *
  • {@code deleteOrphanedRoutes()} → {@code DELETE /v3/spaces/{guid}/routes?unmapped=true} (async job);
  • - *
  • {@code getRoutes(domainName)} → {@code GET /v3/routes?domain_guids=…&space_guids=…} (paginated);
  • - *
  • {@code getApplicationRoutes(appGuid)} → {@code GET /v3/apps/{guid}/routes} (paginated);
  • - *
  • {@code updateApplicationRoutes(appName, routes)} → diff current vs requested destinations, then insert/remove destinations.
  • - *
- * All listings are scoped to the target space exactly where the OSS impl scopes them. - */ public class RoutesV3Operations { private static final ParameterizedTypeReference> ROUTE_PAGE = new ParameterizedTypeReference<>() { @@ -49,11 +34,6 @@ public class RoutesV3Operations { private static final ParameterizedTypeReference> DOMAIN_PAGE = new ParameterizedTypeReference<>() { }; - // CF v3 caps per_page at 5000; matches the other V3 operations. - private static final int DEFAULT_PAGE_SIZE = 5000; - // Mirrors the OSS DELETE_JOB_TIMEOUT. - private static final Duration DELETE_JOB_TIMEOUT = Duration.ofMinutes(5); - private final CloudControllerV3Client cc; private final CloudSpace target; @@ -70,21 +50,24 @@ public void addRoute(String host, String domainName, String path) { public void deleteRoute(String host, String domainName, String path) { assertSpaceProvided("delete route for domain"); + UUID routeGuid = getRouteGuid(getRequiredDomainGuid(domainName), host, path); if (routeGuid == null) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", - "Host " + host + " not found for domain " + domainName + "."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.HOST_0_NOT_FOUND_FOR_DOMAIN_1, host, domainName)); } + doDeleteRoute(routeGuid); } public void deleteOrphanedRoutes() { ResponseEntity response = cc.getRestClient() .delete() - .uri("/v3/spaces/{guid}/routes?unmapped=true", getTargetSpaceGuid()) + .uri(CloudControllerV3Endpoints.SPACE_UNMAPPED_ROUTES, getTargetSpaceGuid()) .retrieve() .toEntity(Void.class); - cc.followAsyncJob(response, DELETE_JOB_TIMEOUT); + + cc.followAsyncJob(response, Constants.DELETE_JOB_TIMEOUT); } public List getRoutes(String domainName) { @@ -94,56 +77,70 @@ public List getRoutes(String domainName) { } public List getApplicationRoutes(UUID applicationGuid) { - return listRoutes("/v3/apps/" + applicationGuid + "/routes?per_page=" + DEFAULT_PAGE_SIZE).stream() - .map(route -> V3RouteMapper.toCloudRoute( - route, applicationGuid)) - .toList(); + return listRoutes(CloudControllerV3Endpoints.APPS + "/" + applicationGuid + "/routes" + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE).stream() + .map(route -> V3RouteMapper.toCloudRoute( + route, applicationGuid)) + .toList(); } public void updateApplicationRoutes(String applicationName, Set updatedRoutes) { UUID applicationGuid = getRequiredApplicationGuid(applicationName); + List appRoutes = listRoutes( - "/v3/apps/" + applicationGuid + "/routes?per_page=" + DEFAULT_PAGE_SIZE).stream() - .map(V3RouteMapper::toCloudRoute) - .toList(); + CloudControllerV3Endpoints.APPS + "/" + applicationGuid + "/routes" + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE).stream() + .map(V3RouteMapper::toCloudRoute) + .toList(); + Set outdatedRoutes = getOutdatedRoutes(applicationGuid, appRoutes, updatedRoutes); Set newRoutes = getNewRoutes(applicationGuid, appRoutes, updatedRoutes); + removeRoutes(outdatedRoutes, applicationGuid); addRoutes(newRoutes, applicationGuid); } - // --- route listing helpers ------------------------------------------------------------------- - private List findRoutesByDomainGuid(UUID domainGuid) { - StringBuilder query = new StringBuilder("/v3/routes?per_page=" + DEFAULT_PAGE_SIZE); - query.append("&domain_guids=") + StringBuilder query = new StringBuilder( + CloudControllerV3Endpoints.ROUTES + CloudControllerV3Endpoints.QUERY_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE); + + query.append(CloudControllerV3Endpoints.AMPERSAND_DOMAIN_GUIDS) .append(domainGuid); - query.append("&space_guids=") + + query.append(CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS) .append(getTargetSpaceGuid()); + return listRoutes(query.toString()).stream() .map(V3RouteMapper::toCloudRoute) .toList(); } private UUID getRouteGuid(UUID domainGuid, String host, String path) { - StringBuilder query = new StringBuilder("/v3/routes?per_page=" + DEFAULT_PAGE_SIZE); - query.append("&domain_guids=") + StringBuilder query = new StringBuilder( + CloudControllerV3Endpoints.ROUTES + CloudControllerV3Endpoints.QUERY_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE); + + query.append(CloudControllerV3Endpoints.AMPERSAND_DOMAIN_GUIDS) .append(domainGuid); - query.append("&space_guids=") + + query.append(CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS) .append(getTargetSpaceGuid()); + if (host != null) { - query.append("&hosts=") + query.append(CloudControllerV3Endpoints.AMPERSAND_HOSTS) .append(host); } + if (path != null) { - query.append("&paths=") + query.append(CloudControllerV3Endpoints.AMPERSAND_PATHS) .append(path); } + List routes = listRoutes(query.toString()); if (CollectionUtils.isEmpty(routes)) { return null; } - return UUID.fromString(routes.get(0) + + return UUID.fromString(routes.getFirst() .guid()); } @@ -151,15 +148,16 @@ private List listRoutes(String query) { return cc.list(query, ROUTE_PAGE); } - // --- add / bind / remove --------------------------------------------------------------------- - private void addRoutes(Set routes, UUID applicationGuid) { Map domains = getDomainsFromRoutes(routes); + for (CloudRoute route : routes) { validateDomainForRoute(route, domains); + UUID domainGuid = domains.get(route.getDomain() .getName()); UUID routeGuid = getOrAddRoute(domainGuid, route.getHost(), route.getPath()); + bindRoute(routeGuid, applicationGuid, route.getRequestedProtocol()); } } @@ -177,9 +175,11 @@ private void removeRoutes(Set routes, UUID applicationGuid) { private UUID getOrAddRoute(UUID domainGuid, String host, String path) { UUID routeGuid = getRouteGuid(domainGuid, host, path); + if (routeGuid == null) { routeGuid = doAddRoute(domainGuid, host, path); } + return routeGuid; } @@ -187,28 +187,30 @@ private UUID doAddRoute(UUID domainGuid, String host, String path) { assertSpaceProvided("add route"); V3Route created = cc.getRestClient() .post() - .uri("/v3/routes") + .uri(CloudControllerV3Endpoints.ROUTES) .body(Map.of("host", host == null ? "" : host, "path", path == null ? "" : path, "relationships", Map.of("domain", toOneRelationship(domainGuid), "space", toOneRelationship(getTargetSpaceGuid())))) .retrieve() .body(V3Route.class); + return UUID.fromString(created.guid()); } private void doDeleteRoute(UUID guid) { ResponseEntity response = cc.getRestClient() .delete() - .uri("/v3/routes/{guid}", guid) + .uri(CloudControllerV3Endpoints.ROUTE_BY_GUID, guid) .retrieve() .toEntity(Void.class); - cc.followAsyncJob(response, DELETE_JOB_TIMEOUT); + + cc.followAsyncJob(response, Constants.DELETE_JOB_TIMEOUT); } private void bindRoute(UUID routeGuid, UUID applicationGuid, String protocol) { cc.getRestClient() .post() - .uri("/v3/routes/{guid}/destinations", routeGuid) + .uri(CloudControllerV3Endpoints.ROUTE_DESTINATIONS, routeGuid) .body(Map.of("destinations", List.of(createDestination(applicationGuid, protocol)))) .retrieve() .toBodilessEntity(); @@ -217,7 +219,7 @@ private void bindRoute(UUID routeGuid, UUID applicationGuid, String protocol) { private void unbindRoute(UUID routeGuid, UUID destinationGuid) { cc.getRestClient() .delete() - .uri("/v3/routes/{routeGuid}/destinations/{destinationGuid}", routeGuid, destinationGuid) + .uri(CloudControllerV3Endpoints.ROUTE_DESTINATION_BY_GUID, routeGuid, destinationGuid) .retrieve() .toBodilessEntity(); } @@ -226,6 +228,7 @@ private Map createDestination(UUID applicationGuid, String proto if (protocol == null) { return Map.of("app", Map.of("guid", applicationGuid.toString())); } + return Map.of("app", Map.of("guid", applicationGuid.toString()), "protocol", protocol); } @@ -233,8 +236,6 @@ private static Map toOneRelationship(UUID guid) { return Map.of("data", Map.of("guid", guid.toString())); } - // --- diffing (mirror of the OSS impl) -------------------------------------------------------- - private Set getOutdatedRoutes(UUID applicationGuid, List currentRoutes, Set updatedRoutes) { return currentRoutes.stream() .filter(currentRoute -> isRouteOutdated(applicationGuid, currentRoute, updatedRoutes)) @@ -243,9 +244,11 @@ private Set getOutdatedRoutes(UUID applicationGuid, List private boolean isRouteOutdated(UUID applicationGuid, CloudRoute currentRoute, Set updatedRoutes) { Optional updatedRoute = findRoute(currentRoute.getUrl(), updatedRoutes); + if (updatedRoute.isEmpty()) { return true; } + return isProtocolChanged(applicationGuid, currentRoute, updatedRoute.get()); } @@ -257,9 +260,11 @@ private Set getNewRoutes(UUID applicationGuid, List curr private boolean isRouteUpdated(UUID applicationGuid, CloudRoute updatedRoute, List currentRoutes) { Optional currentRoute = findRoute(updatedRoute.getUrl(), currentRoutes); + if (currentRoute.isEmpty()) { return true; } + return isProtocolChanged(applicationGuid, currentRoute.get(), updatedRoute); } @@ -273,6 +278,7 @@ private boolean isProtocolChanged(UUID applicationGuid, CloudRoute currentRoute, if (updatedRoute.getRequestedProtocol() == null) { return false; } + return currentRoute.getDestinations() .stream() .filter(routeDestination -> Objects.equals(routeDestination.getApplicationGuid(), applicationGuid)) @@ -283,28 +289,35 @@ private boolean isProtocolChanged(UUID applicationGuid, CloudRoute currentRoute, private void validateDomainForRoute(CloudRoute route, Map existingDomains) { String domain = route.getDomain() .getName(); + if (!StringUtils.hasLength(domain) || !existingDomains.containsKey(domain)) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), - "Domain '" + domain + "' not found for URI " + route.getUrl()); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.DOMAIN_0_NOT_FOUND_FOR_URI_1, domain, route.getUrl())); } - } - // --- domain resolution (minimal inline lookups; the Domains group owns the full wire model) -- + } private UUID getRequiredDomainGuid(String name) { UUID guid = findDomainGuidByName(name); + if (guid == null) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Domain " + name + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.DOMAIN_0_NOT_FOUND, name)); } + return guid; } private UUID findDomainGuidByName(String name) { - List domains = cc.list("/v3/domains?names=" + name + "&per_page=" + DEFAULT_PAGE_SIZE, DOMAIN_PAGE); + List domains = cc.list(CloudControllerV3Endpoints.DOMAINS + CloudControllerV3Endpoints.QUERY_NAMES + name + + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE, + DOMAIN_PAGE); + if (CollectionUtils.isEmpty(domains)) { return null; } - return UUID.fromString(domains.get(0) + + return UUID.fromString(domains.getFirst() .guid()); } @@ -314,31 +327,39 @@ private Map getDomainsFromRoutes(Set routes) { .getName()) .filter(StringUtils::hasLength) .collect(Collectors.toSet()); + if (domainNames.isEmpty()) { return Map.of(); } + String names = String.join(",", domainNames); - return cc.list("/v3/domains?names=" + names + "&per_page=" + DEFAULT_PAGE_SIZE, DOMAIN_PAGE) + return cc.list(CloudControllerV3Endpoints.DOMAINS + CloudControllerV3Endpoints.QUERY_NAMES + names + + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE, DOMAIN_PAGE) .stream() .collect(Collectors.toMap(V3Domain::name, domain -> UUID.fromString(domain.guid()))); } - // --- application resolution ------------------------------------------------------------------ - private UUID getRequiredApplicationGuid(String applicationName) { - StringBuilder query = new StringBuilder("/v3/apps?per_page=" + DEFAULT_PAGE_SIZE); - query.append("&names=") + StringBuilder query = new StringBuilder( + CloudControllerV3Endpoints.APPS + CloudControllerV3Endpoints.QUERY_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE); + + query.append(CloudControllerV3Endpoints.AMPERSAND_NAMES) .append(applicationName); + if (target != null && target.getGuid() != null) { - query.append("&space_guids=") + query.append(CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS) .append(target.getGuid()); } + List apps = cc.list(query.toString(), new ParameterizedTypeReference>() { }); + if (CollectionUtils.isEmpty(apps)) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + applicationName + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.APPLICATION_0_NOT_FOUND, applicationName)); } - return UUID.fromString(apps.get(0) + + return UUID.fromString(apps.getFirst() .guid()); } @@ -348,13 +369,11 @@ private UUID getTargetSpaceGuid() { private void assertSpaceProvided(String operation) { if (target == null) { - throw new IllegalArgumentException("Unable to " + operation + " without specifying organization and space to use."); + throw new IllegalArgumentException( + MessageFormat.format(Messages.UNABLE_TO_0_WITHOUT_SPECIFYING_ORGANIZATION_AND_SPACE_TO_USE, operation)); } } - /** - * Minimal wire-model of a CF v3 application resource — only the {@code guid} needed to resolve an application by name. - */ @JsonIgnoreProperties(ignoreUnknown = true) private record V3AppRef(@JsonProperty("guid") String guid) { } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBindingsV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBindingsV3Operations.java index 25e04c2831..5124d1aec4 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBindingsV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBindingsV3Operations.java @@ -1,6 +1,7 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; import java.net.URI; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -8,10 +9,13 @@ import java.util.Optional; import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBinding; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Application; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceBinding; @@ -21,38 +25,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.util.CollectionUtils; -/** - * CF v3 service credential bindings (application bindings) operations of the in-house {@code CloudControllerRestClient}. - * Reproduces the HTTP shape, filtering, async handling and domain mapping of the OSS {@code CloudControllerRestClientImpl} binding - * methods on top of the shared {@link CloudControllerV3Client} machinery. - * - *

- * Endpoint map (see {@code docs/cf-java-client-migration/02-endpoint-inventory.md}): - *

- *
    - *
  • {@code bindServiceInstance} → {@code POST /v3/service_credential_bindings} (type=app, async job)
  • - *
  • {@code unbindServiceInstance} / {@code deleteServiceBinding} → {@code DELETE /v3/service_credential_bindings/{guid}} (async - * job)
  • - *
  • {@code getServiceBinding} → {@code GET /v3/service_credential_bindings?guids={guid}} (single)
  • - *
  • {@code getServiceAppBindings} → {@code GET /v3/service_credential_bindings?service_instance_guids=…&type=app} (paginated)
  • - *
  • {@code getAppBindings} → {@code GET /v3/service_credential_bindings?app_guids=…} (paginated)
  • - *
  • {@code getServiceBindingsForApplication} → {@code GET /v3/service_credential_bindings?app_guids=…&service_instance_guids=…} - * (paginated)
  • - *
  • {@code getServiceBindingParameters} → {@code GET /v3/service_credential_bindings/{guid}/parameters}
  • - *
  • {@code updateServiceBindingMetadata} → {@code PATCH /v3/service_credential_bindings/{guid}}
  • - *
- * - *

- * As in the OSS impl, the async create/delete operations do not block on the job: they return the job GUID (extracted from the - * {@code Location} header of the 202 response) so the caller can poll it. This matches - * {@code delegate.serviceBindingsV3().create(...).getJobId()} / {@code .delete(...).block()} which yield the job id without waiting. - *

- */ public class ServiceBindingsV3Operations { - // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). - private static final int PER_PAGE = 5000; - private static final ParameterizedTypeReference> BINDING_LIST_TYPE = new ParameterizedTypeReference<>() { }; private static final ParameterizedTypeReference> APPLICATION_LIST_TYPE = new ParameterizedTypeReference<>() { @@ -80,22 +54,25 @@ public Optional bindServiceInstance(String bindingName, String applicati body.put("type", "app"); body.put("relationships", Map.of("app", toOneRelationship(applicationGuid), "service_instance", toOneRelationship(serviceInstanceGuid))); + if (!CollectionUtils.isEmpty(parameters)) { body.put("parameters", parameters); } ResponseEntity response = cc.getRestClient() .post() - .uri("/v3/service_credential_bindings") + .uri(CloudControllerV3Endpoints.SERVICE_CREDENTIAL_BINDINGS) .body(body) .retrieve() .toBodilessEntity(); - return extractJobGuid(response); + + return extractJobGuidFromResponseHeader(response); } public List unbindServiceInstance(String applicationName, String serviceInstanceName) { UUID applicationGuid = getRequiredApplicationGuid(applicationName); UUID serviceInstanceGuid = getRequiredServiceInstanceGuid(serviceInstanceName); + return doUnbindServiceInstance(applicationGuid, serviceInstanceGuid); } @@ -108,7 +85,9 @@ public Optional deleteServiceBinding(UUID bindingGuid) { } public CloudServiceBinding getServiceBinding(UUID serviceBindingGuid) { - String uri = "/v3/service_credential_bindings?guids=" + serviceBindingGuid + "&per_page=" + PER_PAGE; + String uri = CloudControllerV3Endpoints.SERVICE_CREDENTIAL_BINDINGS + CloudControllerV3Endpoints.QUERY_GUIDS + serviceBindingGuid + + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE; + return cc.list(uri, BINDING_LIST_TYPE) .stream() .findFirst() @@ -117,24 +96,34 @@ public CloudServiceBinding getServiceBinding(UUID serviceBindingGuid) { } public List getServiceAppBindings(UUID serviceInstanceGuid) { - String uri = "/v3/service_credential_bindings?service_instance_guids=" + serviceInstanceGuid + "&type=app&per_page=" + PER_PAGE; + String uri = CloudControllerV3Endpoints.SERVICE_CREDENTIAL_BINDINGS + CloudControllerV3Endpoints.QUERY_SERVICE_INSTANCE_GUIDS + + serviceInstanceGuid + CloudControllerV3Endpoints.AMPERSAND_TYPE + "app" + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE; + return listBindings(uri); } public List getAppBindings(UUID applicationGuid) { - String uri = "/v3/service_credential_bindings?app_guids=" + applicationGuid + "&per_page=" + PER_PAGE; + String uri = CloudControllerV3Endpoints.SERVICE_CREDENTIAL_BINDINGS + CloudControllerV3Endpoints.QUERY_APP_GUIDS + applicationGuid + + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE; + return listBindings(uri); } public List getServiceBindingsForApplication(UUID applicationId, UUID serviceInstanceGuid) { - String uri = "/v3/service_credential_bindings?app_guids=" + applicationId + "&service_instance_guids=" + serviceInstanceGuid - + "&per_page=" + PER_PAGE; + String uri = CloudControllerV3Endpoints.SERVICE_CREDENTIAL_BINDINGS + CloudControllerV3Endpoints.QUERY_APP_GUIDS + applicationId + + CloudControllerV3Endpoints.AMPERSAND_SERVICE_INSTANCE_GUIDS + serviceInstanceGuid + + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE; + return listBindings(uri); } public Map getServiceBindingParameters(UUID guid) { @SuppressWarnings("unchecked") - Map parameters = cc.get("/v3/service_credential_bindings/" + guid + "/parameters", Map.class); + Map parameters = cc.get(CloudControllerV3Endpoints.SERVICE_CREDENTIAL_BINDINGS + "/" + guid + "/parameters", + Map.class); + return parameters; } @@ -142,9 +131,10 @@ public void updateServiceBindingMetadata(UUID guid, Metadata metadata) { Map metadataBody = new HashMap<>(); metadataBody.put("labels", metadata.getLabels()); metadataBody.put("annotations", metadata.getAnnotations()); + cc.getRestClient() .patch() - .uri("/v3/service_credential_bindings/{guid}", guid) + .uri(CloudControllerV3Endpoints.SERVICE_CREDENTIAL_BINDING_BY_GUID, guid) .body(Map.of("metadata", metadataBody)) .retrieve() .toBodilessEntity(); @@ -157,20 +147,24 @@ private List listBindings(String uri) { .toList(); } - // Mirrors the OSS doUnbindServiceInstance: resolve the app<->service bindings and 404 when none exist, then delete each. private List doUnbindServiceInstance(UUID applicationGuid, UUID serviceInstanceGuid) { List serviceBindingGuids = getServiceBindingGuids(applicationGuid, serviceInstanceGuid); + if (serviceBindingGuids.isEmpty()) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", - "Service binding between service with GUID " + serviceInstanceGuid - + " and application with GUID " + applicationGuid + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, MessageFormat.format( + Messages.SERVICE_BINDING_BETWEEN_SERVICE_WITH_GUID_0_AND_APPLICATION_WITH_GUID_1_NOT_FOUND, serviceInstanceGuid, + applicationGuid)); } + return doDeleteServiceBindings(serviceBindingGuids); } private List getServiceBindingGuids(UUID applicationGuid, UUID serviceInstanceGuid) { - String uri = "/v3/service_credential_bindings?app_guids=" + applicationGuid + "&service_instance_guids=" + serviceInstanceGuid - + "&per_page=" + PER_PAGE; + String uri = CloudControllerV3Endpoints.SERVICE_CREDENTIAL_BINDINGS + CloudControllerV3Endpoints.QUERY_APP_GUIDS + applicationGuid + + CloudControllerV3Endpoints.AMPERSAND_SERVICE_INSTANCE_GUIDS + serviceInstanceGuid + + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE; + return cc.list(uri, BINDING_LIST_TYPE) .stream() .map(binding -> UUID.fromString(binding.guid())) @@ -180,6 +174,7 @@ private List getServiceBindingGuids(UUID applicationGuid, UUID serviceInst private List doDeleteServiceBindings(List guids) { List jobIds = new ArrayList<>(); List errors = new ArrayList<>(); + for (UUID guid : guids) { try { deleteSingleServiceBinding(guid).ifPresent(jobIds::add); @@ -187,6 +182,7 @@ private List doDeleteServiceBindings(List guids) { errors.add(e); } } + throwOnErrors(errors); return jobIds; } @@ -194,29 +190,33 @@ private List doDeleteServiceBindings(List guids) { private Optional deleteSingleServiceBinding(UUID guid) { ResponseEntity response = cc.getRestClient() .delete() - .uri("/v3/service_credential_bindings/{guid}", guid) + .uri(CloudControllerV3Endpoints.SERVICE_CREDENTIAL_BINDING_BY_GUID, guid) .retrieve() .toBodilessEntity(); - return extractJobGuid(response); + + return extractJobGuidFromResponseHeader(response); } private void throwOnErrors(List errors) { if (errors.isEmpty()) { return; } - CloudOperationException first = errors.get(0); + + CloudOperationException firstException = errors.getFirst(); errors.subList(1, errors.size()) - .forEach(first::addSuppressed); - throw first; + .forEach(firstException::addSuppressed); + + throw firstException; } - // Extract the job GUID from the Location header of a 202-Accepted response (path ends in /v3/jobs/{guid}); empty if synchronous. - private Optional extractJobGuid(ResponseEntity response) { + private Optional extractJobGuidFromResponseHeader(ResponseEntity response) { URI location = response.getHeaders() .getLocation(); + if (location == null) { return Optional.empty(); } + String path = location.getPath(); return Optional.of(path.substring(path.lastIndexOf('/') + 1)); } @@ -225,45 +225,55 @@ private Map toOneRelationship(UUID guid) { return Map.of("data", Map.of("guid", guid.toString())); } - // Mirrors the OSS getRequiredApplicationGuid: look up the app in the target space and 404 if it is absent. private UUID getRequiredApplicationGuid(String applicationName) { - StringBuilder query = new StringBuilder("/v3/apps?per_page=" + PER_PAGE); + StringBuilder query = new StringBuilder( + CloudControllerV3Endpoints.APPS + CloudControllerV3Endpoints.QUERY_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE); + if (target != null && target.getGuid() != null) { - query.append("&space_guids=") + query.append(CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS) .append(target.getGuid()); } - query.append("&names=") + + query.append(CloudControllerV3Endpoints.AMPERSAND_NAMES) .append(applicationName); + List apps = cc.list(query.toString(), APPLICATION_LIST_TYPE); + if (apps.isEmpty()) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + applicationName + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.APPLICATION_0_NOT_FOUND, applicationName)); } - return UUID.fromString(apps.get(0) + + return UUID.fromString(apps.getFirst() .guid()); } - // Mirrors the OSS getRequiredServiceInstanceGuid: look up the service instance in the target space and 404 if it is absent. private UUID getRequiredServiceInstanceGuid(String name) { - StringBuilder query = new StringBuilder("/v3/service_instances?per_page=" + PER_PAGE); + StringBuilder query = new StringBuilder(CloudControllerV3Endpoints.SERVICE_INSTANCES + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE); + if (target != null && target.getGuid() != null) { - query.append("&space_guids=") + query.append(CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS) .append(target.getGuid()); } - query.append("&names=") + + query.append(CloudControllerV3Endpoints.AMPERSAND_NAMES) .append(name); List instances = cc.list(query.toString(), new ParameterizedTypeReference>() { }); + if (instances.isEmpty()) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service instance " + name + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.SERVICE_INSTANCE_0_NOT_FOUND, name)); } - return UUID.fromString(instances.get(0) + + return UUID.fromString(instances.getFirst() .guid()); } - // Minimal wire model to resolve a service instance GUID by name without depending on the (separately-owned) ServiceInstances group. - @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) - private record V3ServiceInstanceRef(@com.fasterxml.jackson.annotation.JsonProperty("guid") String guid) { + @JsonIgnoreProperties(ignoreUnknown = true) + private record V3ServiceInstanceRef(@JsonProperty("guid") String guid) { } } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBrokersV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBrokersV3Operations.java index 8a7acff08a..6b1db34c3e 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBrokersV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceBrokersV3Operations.java @@ -1,11 +1,15 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; import java.net.URI; +import java.text.MessageFormat; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBroker; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; @@ -18,35 +22,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * CF v3 service broker operations of the in-house {@code CloudControllerRestClient}. Reproduces the HTTP shape, filtering, - * async job handling and domain mapping of the OSS {@code CloudControllerRestClientImpl} service-broker methods on top of the shared - * {@link CloudControllerV3Client} machinery. - * - *

- * Endpoint map (see {@code docs/cf-java-client-migration/02-endpoint-inventory.md}): - *

- *
    - *
  • {@code createServiceBroker} → {@code POST /v3/service_brokers} (async job; returns the job GUID)
  • - *
  • {@code deleteServiceBroker} → {@code DELETE /v3/service_brokers/{guid}} (async job; returns the job GUID)
  • - *
  • {@code getServiceBroker(name[, required])} → {@code GET /v3/service_brokers?names=} (paginated, first match)
  • - *
  • {@code getServiceBrokers} → {@code GET /v3/service_brokers} (paginated)
  • - *
  • {@code updateServiceBroker} → {@code PATCH /v3/service_brokers/{guid}} (async job; returns the job GUID or {@code null})
  • - *
  • {@code updateServicePlanVisibilityForBroker} → per plan {@code PATCH /v3/service_plans/{guid}/visibility}
  • - *
- * - *

- * As in the OSS impl, {@code createServiceBroker}/{@code deleteServiceBroker}/{@code updateServiceBroker} return the async job GUID - * without waiting for the job to complete — the caller polls the job separately. - *

- */ public class ServiceBrokersV3Operations { - private static final int PER_PAGE = 5000; - private static final ParameterizedTypeReference> BROKER_PAGE = new ParameterizedTypeReference<>() { }; private static final ParameterizedTypeReference> OFFERING_PAGE = new ParameterizedTypeReference<>() { @@ -69,13 +46,14 @@ public String createServiceBroker(CloudServiceBroker serviceBroker) { body.put("name", serviceBroker.getName()); body.put("url", serviceBroker.getUrl()); body.put("authentication", basicAuthentication(serviceBroker)); + if (serviceBroker.getSpaceGuid() != null) { body.put("relationships", Map.of("space", Map.of("data", Map.of("guid", serviceBroker.getSpaceGuid())))); } ResponseEntity response = cc.getRestClient() .post() - .uri("/v3/service_brokers") + .uri(CloudControllerV3Endpoints.SERVICE_BROKERS) .body(body) .retrieve() .toBodilessEntity(); @@ -86,11 +64,13 @@ public String deleteServiceBroker(String name) { CloudServiceBroker broker = getServiceBroker(name); UUID guid = broker.getMetadata() .getGuid(); + ResponseEntity response = cc.getRestClient() .delete() - .uri("/v3/service_brokers/{guid}", guid.toString()) + .uri(CloudControllerV3Endpoints.SERVICE_BROKER_BY_GUID, guid.toString()) .retrieve() .toBodilessEntity(); + return extractJobGuid(response); } @@ -100,14 +80,18 @@ public CloudServiceBroker getServiceBroker(String name) { public CloudServiceBroker getServiceBroker(String name, boolean required) { CloudServiceBroker serviceBroker = findServiceBrokerByName(name); + if (serviceBroker == null && required) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service broker " + name + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.SERVICE_BROKER_0_NOT_FOUND, name)); } + return serviceBroker; } public List getServiceBrokers() { - return cc.list("/v3/service_brokers?per_page=" + PER_PAGE, BROKER_PAGE) + return cc.list(CloudControllerV3Endpoints.SERVICE_BROKERS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE, BROKER_PAGE) .stream() .map(V3ServiceBrokerMapper::toCloudServiceBroker) .toList(); @@ -125,11 +109,11 @@ public String updateServiceBroker(CloudServiceBroker serviceBroker) { ResponseEntity response = cc.getRestClient() .patch() - .uri("/v3/service_brokers/{guid}", brokerGuid.toString()) + .uri(CloudControllerV3Endpoints.SERVICE_BROKER_BY_GUID, brokerGuid.toString()) .body(body) .retrieve() .toBodilessEntity(); - // A synchronous update returns 200 with no Location header (no job); an async update returns 202 with a job GUID. + return extractJobGuid(response); } @@ -137,13 +121,17 @@ public void updateServicePlanVisibilityForBroker(String name, ServicePlanVisibil CloudServiceBroker broker = getServiceBroker(name); UUID brokerGuid = broker.getMetadata() .getGuid(); + for (UUID servicePlanGuid : findServicePlanGuidsByBrokerGuid(brokerGuid)) { updateServicePlanVisibility(servicePlanGuid, visibility); } } private CloudServiceBroker findServiceBrokerByName(String name) { - return cc.list("/v3/service_brokers?names=" + name + "&per_page=" + PER_PAGE, BROKER_PAGE) + return cc.list( + CloudControllerV3Endpoints.SERVICE_BROKERS + CloudControllerV3Endpoints.QUERY_NAMES + name + + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE, BROKER_PAGE) .stream() .findFirst() .map(V3ServiceBrokerMapper::toCloudServiceBroker) @@ -158,8 +146,10 @@ private List findServicePlanGuidsByBrokerGuid(UUID brokerGuid) { } private List findServiceOfferingGuidsByBrokerGuid(UUID brokerGuid) { - String uri = "/v3/service_offerings?service_broker_guids=" + brokerGuid + "&space_guids=" + getTargetSpaceGuid() + "&per_page=" - + PER_PAGE; + String uri = CloudControllerV3Endpoints.SERVICE_OFFERINGS + CloudControllerV3Endpoints.QUERY_SERVICE_BROKER_GUIDS + brokerGuid + + CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS + getTargetSpaceGuid() + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE; + return cc.list(uri, OFFERING_PAGE) .stream() .map(ServiceOfferingRef::guid) @@ -168,7 +158,10 @@ private List findServiceOfferingGuidsByBrokerGuid(UUID brokerGuid) { } private List findServicePlanGuidsByOfferingGuid(UUID serviceOfferingGuid) { - String uri = "/v3/service_plans?service_offering_guids=" + serviceOfferingGuid + "&per_page=" + PER_PAGE; + String uri = + CloudControllerV3Endpoints.SERVICE_PLANS + CloudControllerV3Endpoints.QUERY_SERVICE_OFFERING_GUIDS + serviceOfferingGuid + + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE; + return cc.list(uri, PLAN_PAGE) .stream() .map(ServicePlanRef::guid) @@ -177,11 +170,9 @@ private List findServicePlanGuidsByOfferingGuid(UUID serviceOfferingGuid) } private void updateServicePlanVisibility(UUID servicePlanGuid, ServicePlanVisibility visibility) { - // ServicePlanVisibility.toString() yields the lowercase CF visibility type (public/admin/organization), - // matching the OSS Visibility.from(visibility.toString()) mapping. cc.getRestClient() .patch() - .uri("/v3/service_plans/{guid}/visibility", servicePlanGuid.toString()) + .uri(CloudControllerV3Endpoints.SERVICE_PLAN_VISIBILITY, servicePlanGuid.toString()) .body(Map.of("type", visibility.toString())) .retrieve() .toBodilessEntity(); @@ -202,19 +193,22 @@ private String getTargetSpaceGuid() { .toString(); } - // A 202-Accepted async response carries the job in its Location header (/v3/jobs/{guid}); a synchronous 200 has none. private static String extractJobGuid(ResponseEntity response) { URI location = response.getHeaders() .getLocation(); + if (location == null) { return null; } + String value = location.toString(); - int idx = value.lastIndexOf("/v3/jobs/"); - if (idx < 0) { + int index = value.lastIndexOf("/v3/jobs/"); + + if (index < 0) { return value.substring(value.lastIndexOf('/') + 1); } - return value.substring(idx + "/v3/jobs/".length()); + + return value.substring(index + "/v3/jobs/".length()); } @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java index 0bea35bc76..e004dcc6be 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceInstancesV3Operations.java @@ -9,12 +9,13 @@ import java.util.UUID; import java.util.stream.Collectors; +import org.cloudfoundry.multiapps.controller.Constants; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; -import org.cloudfoundry.multiapps.controller.client.facade.Messages; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudEntity; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceInstance; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceInstanceMapper; @@ -24,42 +25,8 @@ import org.springframework.http.HttpStatus; import org.springframework.util.Assert; -/** - * CF v3 service instance operations of the in-house {@code CloudControllerRestClient}. Reproduces the HTTP shape, filtering, - * space-scoping and domain-mapping of the OSS {@code CloudControllerRestClientImpl} service-instance methods on top of the shared - * {@link CloudControllerV3Client} machinery. - * - *

- * Endpoint map (see {@code docs/cf-java-client-migration/02-endpoint-inventory.md}): - *

- *
    - *
  • {@code createServiceInstance} → {@code POST /v3/service_instances} (type=managed)
  • - *
  • {@code createUserProvidedServiceInstance} → {@code POST /v3/service_instances} (type=user-provided)
  • - *
  • {@code deleteServiceInstance} → {@code DELETE /v3/service_instances/{guid}}
  • - *
  • {@code getServiceInstance} / {@code getRequiredServiceInstanceGuid} / {@code getServiceInstanceWithoutAuxiliaryContent} → - * {@code GET /v3/service_instances?space_guids=…&names=…} (paginated)
  • - *
  • {@code getServiceInstanceName} → {@code GET /v3/service_instances/{guid}}
  • - *
  • {@code getServiceInstanceParameters} → {@code GET /v3/service_instances/{guid}/parameters}
  • - *
  • {@code getUserProvidedServiceInstanceParameters} → {@code GET /v3/service_instances/{guid}/credentials}
  • - *
  • {@code getServiceInstancesWithoutAuxiliaryContentByNames} → {@code GET /v3/service_instances?space_guids=…&names=…} (batched)
  • - *
  • {@code getServiceInstancesByMetadataLabelSelector} (+ WithoutAuxiliaryContent) → - * {@code GET /v3/service_instances?space_guids=…&label_selector=…} (paginated)
  • - *
  • {@code updateServicePlan} / {@code updateServiceParameters} / {@code updateServiceTags} / {@code updateServiceSyslogDrainUrl} / - * {@code updateServiceInstanceMetadata} → {@code PATCH /v3/service_instances/{guid}}
  • - *
- * - *

- * Async note: for managed instances CF returns 202 for create/update/delete, but the OSS impl does not follow the job — it returns - * as soon as the request is accepted. This helper preserves that behaviour (no {@code followAsyncJob} on these calls). - *

- */ public class ServiceInstancesV3Operations { - // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). - private static final int PER_PAGE = 5000; - // Mirrors the OSS MAX_CHAR_LENGTH_FOR_PARAMS_IN_REQUEST used to batch the "byNames" lookups. - private static final int MAX_CHAR_LENGTH_FOR_PARAMS_IN_REQUEST = 4000; - private static final ParameterizedTypeReference> SERVICE_INSTANCE_LIST_TYPE = new ParameterizedTypeReference<>() { }; @@ -87,7 +54,7 @@ public void createServiceInstance(CloudServiceInstance serviceInstance) { cc.getRestClient() .post() - .uri("/v3/service_instances") + .uri(CloudControllerV3Endpoints.SERVICE_INSTANCES) .body(body) .retrieve() .toBodilessEntity(); @@ -109,7 +76,7 @@ public void createUserProvidedServiceInstance(CloudServiceInstance serviceInstan cc.getRestClient() .post() - .uri("/v3/service_instances") + .uri(CloudControllerV3Endpoints.SERVICE_INSTANCES) .body(body) .retrieve() .toBodilessEntity(); @@ -126,9 +93,12 @@ public void deleteServiceInstance(CloudServiceInstance serviceInstance) { public UUID getRequiredServiceInstanceGuid(String name) { V3ServiceInstance resource = findServiceInstanceResourceByName(name); + if (resource == null) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service instance " + name + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.SERVICE_INSTANCE_0_NOT_FOUND, name)); } + return UUID.fromString(resource.guid()); } @@ -138,11 +108,14 @@ public CloudServiceInstance getServiceInstance(String serviceInstanceName) { public CloudServiceInstance getServiceInstance(String serviceInstanceName, boolean required) { CloudServiceInstance serviceInstance = findServiceInstanceByName(serviceInstanceName); + return getServiceInstanceIfRequired(serviceInstanceName, serviceInstance, required); } public String getServiceInstanceName(UUID serviceInstanceGuid) { - V3ServiceInstance serviceInstance = cc.get("/v3/service_instances/" + serviceInstanceGuid, V3ServiceInstance.class); + V3ServiceInstance serviceInstance = cc.get(CloudControllerV3Endpoints.SERVICE_INSTANCES + "/" + serviceInstanceGuid, + V3ServiceInstance.class); + return serviceInstance == null ? null : serviceInstance.name(); } @@ -152,43 +125,51 @@ public CloudServiceInstance getServiceInstanceWithoutAuxiliaryContent(String ser public CloudServiceInstance getServiceInstanceWithoutAuxiliaryContent(String serviceInstanceName, boolean required) { V3ServiceInstance resource = findServiceInstanceResourceByName(serviceInstanceName); + CloudServiceInstance serviceInstance = resource == null ? null : V3ServiceInstanceMapper.toCloudServiceInstance(resource, null, null); + return getServiceInstanceIfRequired(serviceInstanceName, serviceInstance, required); } public Map getServiceInstanceParameters(UUID guid) { - return getParameters("/v3/service_instances/" + guid + "/parameters"); + return getParameters(CloudControllerV3Endpoints.SERVICE_INSTANCES + "/" + guid + "/parameters"); } public Map getUserProvidedServiceInstanceParameters(UUID guid) { - return getParameters("/v3/service_instances/" + guid + "/credentials"); + return getParameters(CloudControllerV3Endpoints.SERVICE_INSTANCES + "/" + guid + "/credentials"); } public List getServiceInstancesWithoutAuxiliaryContentByNames(List names) { List allServiceInstances = new ArrayList<>(); - for (List batch : toBatches(names, MAX_CHAR_LENGTH_FOR_PARAMS_IN_REQUEST)) { - String uri = "/v3/service_instances?per_page=" + PER_PAGE + "&space_guids=" + getTargetSpaceGuid() + "&names=" - + String.join(",", batch); + + for (List batch : toBatches(names, Constants.MAX_CHAR_LENGTH_FOR_PARAMS_IN_REQUEST)) { + String uri = CloudControllerV3Endpoints.SERVICE_INSTANCES + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE + CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS + getTargetSpaceGuid() + + CloudControllerV3Endpoints.AMPERSAND_NAMES + String.join(",", batch); + cc.list(uri, SERVICE_INSTANCE_LIST_TYPE) .stream() .map(V3ServiceInstanceMapper::toCloudServiceInstanceWithoutAuxiliaryContent) .forEach(allServiceInstances::add); } + return allServiceInstances; } public List getServiceInstancesByMetadataLabelSelector(String labelSelector) { - String uri = "/v3/service_instances?per_page=" + PER_PAGE + "&space_guids=" + getTargetSpaceGuid() + "&label_selector=" - + labelSelector; - // Each managed instance needs its plan + offering resolved via extra GETs (mapWithAuxiliaryContent). The OSS client ran this - // enrichment concurrently across the list; preserve that so a large label-selector result doesn't serialize the per-instance calls. - return ReactiveFanOut.mapConcurrently(cc.list(uri, SERVICE_INSTANCE_LIST_TYPE), this::mapWithAuxiliaryContent); + String uri = CloudControllerV3Endpoints.SERVICE_INSTANCES + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE + CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS + getTargetSpaceGuid() + + CloudControllerV3Endpoints.AMPERSAND_LABEL_SELECTOR + labelSelector; + + return ReactiveFanOut.mapConcurrently(cc.list(uri, SERVICE_INSTANCE_LIST_TYPE), this::mapWithSupportingContent); } public List getServiceInstancesWithoutAuxiliaryContentByMetadataLabelSelector(String labelSelector) { - String uri = "/v3/service_instances?per_page=" + PER_PAGE + "&space_guids=" + getTargetSpaceGuid() + "&label_selector=" - + labelSelector; + String uri = CloudControllerV3Endpoints.SERVICE_INSTANCES + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE + CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS + getTargetSpaceGuid() + + CloudControllerV3Endpoints.AMPERSAND_LABEL_SELECTOR + labelSelector; + return cc.list(uri, SERVICE_INSTANCE_LIST_TYPE) .stream() .map(resource -> V3ServiceInstanceMapper.toCloudServiceInstance(resource, null, null)) @@ -197,9 +178,11 @@ public List getServiceInstancesWithoutAuxiliaryContentByMe public void updateServicePlan(String serviceName, String planName) { CloudServiceInstance service = getServiceInstance(serviceName); + if (service.isUserProvided()) { return; } + UUID planGuid = findPlanGuidForService(service, planName); patchServiceInstance(service.getGuid(), Map.of("relationships", Map.of("service_plan", toOneRelationship(planGuid.toString())))); } @@ -217,9 +200,11 @@ public void updateServiceTags(String serviceName, List tags) { public void updateServiceSyslogDrainUrl(String serviceName, String syslogDrainUrl) { CloudServiceInstance service = getServiceInstanceWithoutAuxiliaryContent(serviceName); + if (!service.isUserProvided()) { return; } + String updatedSyslogDrain = hasText(syslogDrainUrl) ? syslogDrainUrl : ""; patchServiceInstance(service.getGuid(), Map.of("syslog_drain_url", updatedSyslogDrain)); } @@ -232,35 +217,42 @@ public void updateServiceInstanceMetadata(UUID guid, Metadata metadata) { private CloudServiceInstance getServiceInstanceIfRequired(String serviceInstanceName, CloudServiceInstance serviceInstance, boolean required) { if (serviceInstance == null && required) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service instance " + serviceInstanceName + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.SERVICE_INSTANCE_0_NOT_FOUND, serviceInstanceName)); } + return serviceInstance; } - // Mirrors the OSS findServiceInstanceByName -> zipWithAuxiliaryServiceInstanceContent: for managed instances the plan and offering - // names are resolved and included; user-provided instances are mapped as-is. private CloudServiceInstance findServiceInstanceByName(String name) { V3ServiceInstance resource = findServiceInstanceResourceByName(name); + if (resource == null) { return null; } - return mapWithAuxiliaryContent(resource); + + return mapWithSupportingContent(resource); } - private CloudServiceInstance mapWithAuxiliaryContent(V3ServiceInstance resource) { + private CloudServiceInstance mapWithSupportingContent(V3ServiceInstance resource) { if (isUserProvided(resource)) { return V3ServiceInstanceMapper.toCloudServiceInstance(resource, null, null); } + String servicePlanGuid = servicePlanGuidOf(resource); if (servicePlanGuid == null) { return V3ServiceInstanceMapper.toCloudServiceInstance(resource, null, null); } + ServicePlanNames names = resolvePlanAndOfferingNames(servicePlanGuid, resource.name()); return V3ServiceInstanceMapper.toCloudServiceInstance(resource, names.planName(), names.offeringName()); } private V3ServiceInstance findServiceInstanceResourceByName(String name) { - String uri = "/v3/service_instances?per_page=" + PER_PAGE + "&space_guids=" + getTargetSpaceGuid() + "&names=" + name; + String uri = CloudControllerV3Endpoints.SERVICE_INSTANCES + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE + CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS + getTargetSpaceGuid() + + CloudControllerV3Endpoints.AMPERSAND_NAMES + name; + return cc.list(uri, SERVICE_INSTANCE_LIST_TYPE) .stream() .findFirst() @@ -279,62 +271,70 @@ private String servicePlanGuidOf(V3ServiceInstance resource) { .data() == null) { return null; } + return resource.relationships() .servicePlan() .data() .guid(); } - // Mirrors the OSS getServicePlanResource + getServiceOffering zip: resolve the plan's own name and its offering's name. Reproduces the - // OSS friendly-error mapping too — a 403/404 on the plan or offering lookup becomes a specific, human-readable CloudOperationException - // (instead of the generic one) so operators get the same messages as before. private ServicePlanNames resolvePlanAndOfferingNames(String servicePlanGuid, String serviceInstanceName) { V3ServicePlan plan = getServicePlanForNameResolution(servicePlanGuid, serviceInstanceName); + if (plan == null) { return new ServicePlanNames(null, null); } + String offeringName = null; String offeringGuid = offeringGuidOf(plan); + if (offeringGuid != null) { V3ServiceOffering offering = getServiceOfferingForNameResolution(offeringGuid); offeringName = offering == null ? null : offering.name(); } + return new ServicePlanNames(plan.name(), offeringName); } private V3ServicePlan getServicePlanForNameResolution(String servicePlanGuid, String serviceInstanceName) { try { - return cc.get("/v3/service_plans/" + servicePlanGuid, V3ServicePlan.class); + return cc.get(CloudControllerV3Endpoints.SERVICE_PLANS + "/" + servicePlanGuid, V3ServicePlan.class); } catch (CloudOperationException e) { if (e.getStatusCode() == HttpStatus.FORBIDDEN) { - throw new CloudOperationException(HttpStatus.FORBIDDEN, HttpStatus.FORBIDDEN.getReasonPhrase(), - MessageFormat.format(Messages.SERVICE_PLAN_WITH_GUID_0_NOT_AVAILABLE_FOR_SERVICE_INSTANCE_1, - servicePlanGuid, serviceInstanceName), + throw new CloudOperationException(HttpStatus.FORBIDDEN, Messages.FORBIDDEN, + MessageFormat.format( + Messages.SERVICE_PLAN_WITH_GUID_0_NOT_AVAILABLE_FOR_SERVICE_INSTANCE_1, + servicePlanGuid, serviceInstanceName), e); } + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, MessageFormat.format(Messages.NO_SERVICE_PLAN_FOUND, servicePlanGuid, serviceInstanceName), e); } + throw e; } } private V3ServiceOffering getServiceOfferingForNameResolution(String offeringGuid) { try { - return cc.get("/v3/service_offerings/" + offeringGuid, V3ServiceOffering.class); + return cc.get(CloudControllerV3Endpoints.SERVICE_OFFERINGS + "/" + offeringGuid, V3ServiceOffering.class); } catch (CloudOperationException e) { if (e.getStatusCode() == HttpStatus.FORBIDDEN) { - throw new CloudOperationException(HttpStatus.FORBIDDEN, HttpStatus.FORBIDDEN.getReasonPhrase(), - MessageFormat.format(Messages.SERVICE_OFFERING_WITH_GUID_0_IS_NOT_AVAILABLE, offeringGuid), + throw new CloudOperationException(HttpStatus.FORBIDDEN, Messages.FORBIDDEN, + MessageFormat.format(Messages.SERVICE_OFFERING_WITH_GUID_0_IS_NOT_AVAILABLE, + offeringGuid), e); } + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, MessageFormat.format(Messages.SERVICE_OFFERING_WITH_GUID_0_NOT_FOUND, offeringGuid), e); } + throw e; } } @@ -347,61 +347,73 @@ private String offeringGuidOf(V3ServicePlan plan) { .data() == null) { return null; } + return plan.relationships() .serviceOffering() .data() .guid(); } - // Mirrors the OSS findPlanForService: list the offerings for the service's label (optionally scoped by broker), then find the plan by - // name; a missing plan throws CloudOperationException(NOT_FOUND). private UUID findPlanGuidForService(CloudServiceInstance service, String planName) { List offeringGuids = findServiceOfferingGuids(service.getLabel(), service.getBroker()); + for (String offeringGuid : offeringGuids) { - String uri = "/v3/service_plans?per_page=" + PER_PAGE + "&service_offering_guids=" + offeringGuid + "&names=" + planName; + String uri = CloudControllerV3Endpoints.SERVICE_PLANS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE + CloudControllerV3Endpoints.AMPERSAND_SERVICE_OFFERING_GUIDS + offeringGuid + + CloudControllerV3Endpoints.AMPERSAND_NAMES + planName; + V3ServicePlan plan = cc.list(uri, new ParameterizedTypeReference>() { - }) + }) .stream() .findFirst() .orElse(null); + if (plan != null) { return UUID.fromString(plan.guid()); } } - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Service plan " + planName + " not found."); + + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.SERVICE_PLAN_0_NOT_FOUND, planName)); } private List findServiceOfferingGuids(String label, String broker) { - StringBuilder uri = new StringBuilder("/v3/service_offerings?per_page=").append(PER_PAGE) - .append("&space_guids=") - .append(getTargetSpaceGuid()) - .append("&names=") - .append(label); + StringBuilder uri = new StringBuilder( + CloudControllerV3Endpoints.SERVICE_OFFERINGS + CloudControllerV3Endpoints.QUERY_PER_PAGE).append( + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE) + .append( + CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS) + .append( + getTargetSpaceGuid()) + .append( + CloudControllerV3Endpoints.AMPERSAND_NAMES) + .append( + label); + if (hasText(broker)) { - uri.append("&service_broker_names=") + uri.append(CloudControllerV3Endpoints.AMPERSAND_SERVICE_BROKER_NAMES) .append(broker); } + return cc.list(uri.toString(), new ParameterizedTypeReference>() { - }) + }) .stream() .map(V3ServiceOffering::guid) .collect(Collectors.toList()); } private void doDeleteServiceInstance(UUID serviceInstanceGuid) { - // OSS parity: the delete is issued and the response consumed, but the async job is NOT followed. cc.getRestClient() .delete() - .uri("/v3/service_instances/{guid}", serviceInstanceGuid.toString()) + .uri(CloudControllerV3Endpoints.SERVICE_INSTANCE_BY_GUID, serviceInstanceGuid.toString()) .retrieve() .toBodilessEntity(); } private void patchServiceInstance(UUID guid, Map body) { - // OSS parity: the update is issued and the response consumed, but the async job is NOT followed. cc.getRestClient() .patch() - .uri("/v3/service_instances/{guid}", guid.toString()) + .uri(CloudControllerV3Endpoints.SERVICE_INSTANCE_BY_GUID, guid.toString()) .body(body) .retrieve() .toBodilessEntity(); @@ -420,10 +432,12 @@ private Map toMetadataMap(Metadata metadata) { if (metadata == null) { return null; } - Map map = new HashMap<>(); - map.put("labels", metadata.getLabels() == null ? Map.of() : metadata.getLabels()); - map.put("annotations", metadata.getAnnotations() == null ? Map.of() : metadata.getAnnotations()); - return map; + + Map metadataMapResult = new HashMap<>(); + metadataMapResult.put("labels", metadata.getLabels() == null ? Map.of() : metadata.getLabels()); + metadataMapResult.put("annotations", metadata.getAnnotations() == null ? Map.of() : metadata.getAnnotations()); + + return metadataMapResult; } private static void putIfNotNull(Map body, String key, Object value) { @@ -436,27 +450,31 @@ private static boolean hasText(String value) { return value != null && !value.isBlank(); } - // Mirrors the OSS toBatches: split a large collection into batches bounded by the total character length of the elements. private List> toBatches(Collection largeList, int maxCharLength) { if (largeList.isEmpty()) { return List.of(); } + List> batches = new ArrayList<>(); int currentBatchLength = 0; int currentBatchIndex = 0; batches.add(new ArrayList<>()); + for (T element : largeList) { int elementLength = element.toString() .length(); + if (elementLength + currentBatchLength >= maxCharLength) { batches.add(new ArrayList<>()); currentBatchIndex++; currentBatchLength = 0; } + batches.get(currentBatchIndex) .add(element); currentBatchLength += elementLength; } + return batches; } @@ -468,6 +486,7 @@ private UUID getGuid(CloudEntity entity) { if (entity == null || entity.getMetadata() == null) { return null; } + return entity.getMetadata() .getGuid(); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceKeysV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceKeysV3Operations.java index e6e422acfe..b85c8b7aef 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceKeysV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceKeysV3Operations.java @@ -1,7 +1,6 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; import java.net.URI; -import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -9,12 +8,12 @@ import java.util.Optional; import java.util.UUID; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType; +import org.cloudfoundry.multiapps.controller.Constants; import org.cloudfoundry.multiapps.controller.client.facade.Messages; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceBinding; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServiceKeyDetails; @@ -22,72 +21,51 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.ResponseEntity; -/** - * CF v3 service key operations for the cf-java-client replacement. A service key is a service credential binding of - * {@code type=key}, so all endpoints target {@code /v3/service_credential_bindings}. Reproduces the HTTP shape and domain-mapping of the - * OSS {@code CloudControllerRestClientImpl} service-key methods: - *
    - *
  • create → {@code POST /v3/service_credential_bindings} (type=key) — returns a job (202);
  • - *
  • list → {@code GET /v3/service_credential_bindings?type=key&service_instance_guids={guid}[&names={name}]} (paginated);
  • - *
  • credentials → {@code GET /v3/service_credential_bindings/{guid}/details}.
  • - *
- * - *

- * The interface methods that accept a {@code serviceInstanceName} require the owning {@link CloudServiceInstance} (for its GUID and to - * embed in the resulting {@link CloudServiceKey}). Mirroring the OSS impl — which first calls {@code getServiceInstance(name)} - * — these operations take the already-resolved {@link CloudServiceInstance}; the integrator resolves it in - * {@code CloudControllerRestClientV3Impl} before delegating. - *

- */ public class ServiceKeysV3Operations { - // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). - private static final int DEFAULT_PAGE_SIZE = 5000; - // Mirrors the OSS BINDING_OPERATIONS_TIMEOUT used when synchronously creating (and fetching) a service key. - private static final Duration BINDING_OPERATIONS_TIMEOUT = Duration.ofMinutes(5); - private final CloudControllerV3Client cc; - private final CloudSpace target; - public ServiceKeysV3Operations(CloudControllerV3Client cc, CloudSpace target) { + public ServiceKeysV3Operations(CloudControllerV3Client cc) { this.cc = cc; - this.target = target; } - // --- create ----------------------------------------------------------------------------------------------------------------- - public CloudServiceKey createAndFetchServiceKey(CloudServiceKey keyModel, CloudServiceInstance serviceInstance) { - ResponseEntity accepted = postServiceKey(keyModel.getName(), keyModel.getCredentials(), keyModel.getV3Metadata(), - serviceInstance); - cc.followAsyncJob(accepted, BINDING_OPERATIONS_TIMEOUT); - V3ServiceBinding created = getServiceKeyResourceByNameAndServiceInstanceGuid(keyModel.getName(), serviceInstance.getGuid()); - if (created == null) { + ResponseEntity acceptedResponse = postServiceKey(keyModel.getName(), keyModel.getCredentials(), keyModel.getV3Metadata(), + serviceInstance); + + cc.followAsyncJob(acceptedResponse, Constants.BINDING_OPERATIONS_TIMEOUT); + + V3ServiceBinding createdServiceKey = getServiceKeyResourceByNameAndServiceInstanceGuid(keyModel.getName(), + serviceInstance.getGuid()); + if (createdServiceKey == null) { return null; } - Map credentials = getServiceKeyCredentials(created.guid()); - return V3ServiceKeyMapper.toCloudServiceKey(created, serviceInstance, credentials); + + Map credentials = getServiceKeyCredentials(createdServiceKey.guid()); + return V3ServiceKeyMapper.toCloudServiceKey(createdServiceKey, serviceInstance, credentials); } public Optional createServiceKey(CloudServiceKey keyModel, CloudServiceInstance serviceInstance) { - ResponseEntity accepted = postServiceKey(keyModel.getName(), keyModel.getCredentials(), keyModel.getV3Metadata(), - serviceInstance); - return extractJobGuid(accepted); + ResponseEntity acceptedResponse = postServiceKey(keyModel.getName(), keyModel.getCredentials(), keyModel.getV3Metadata(), + serviceInstance); + + return extractJobGuid(acceptedResponse); } public Optional createServiceKey(CloudServiceInstance serviceInstance, String serviceKeyName, Map parameters) { - ResponseEntity accepted = postServiceKey(serviceKeyName, parameters, null, serviceInstance); - return extractJobGuid(accepted); - } + ResponseEntity acceptedResponse = postServiceKey(serviceKeyName, parameters, null, serviceInstance); - // --- read ------------------------------------------------------------------------------------------------------------------- + return extractJobGuid(acceptedResponse); + } public CloudServiceKey getServiceKey(CloudServiceInstance serviceInstance, String serviceKeyName) { - V3ServiceBinding key = getServiceKeyResourceByNameAndServiceInstanceGuid(serviceKeyName, serviceInstance.getGuid()); - if (key == null) { + V3ServiceBinding serviceKey = getServiceKeyResourceByNameAndServiceInstanceGuid(serviceKeyName, serviceInstance.getGuid()); + if (serviceKey == null) { return null; } - Map credentials = getServiceKeyCredentials(key.guid()); - return V3ServiceKeyMapper.toCloudServiceKey(key, serviceInstance, credentials); + + Map credentials = getServiceKeyCredentials(serviceKey.guid()); + return V3ServiceKeyMapper.toCloudServiceKey(serviceKey, serviceInstance, credentials); } public List getServiceKeys(CloudServiceInstance serviceInstance) { @@ -97,24 +75,21 @@ public List getServiceKeys(CloudServiceInstance serviceInstance } public List getServiceKeysWithCredentials(CloudServiceInstance serviceInstance) { - // Each key needs a separate GET /details for its credentials. The OSS client fetched these concurrently (reactive flatMap); do the - // same here so a service with many keys doesn't serialize N credential round-trips. return ReactiveFanOut.mapConcurrently(listServiceKeyResources(serviceInstance.getGuid()), key -> V3ServiceKeyMapper.toCloudServiceKey(key, serviceInstance, getServiceKeyCredentials(key.guid()))); } - // --- helpers ---------------------------------------------------------------------------------------------------------------- - private ResponseEntity postServiceKey(String name, Map parameters, Metadata metadata, CloudServiceInstance serviceInstance) { if (serviceInstance.getType() != ServiceInstanceType.MANAGED) { throw new IllegalArgumentException( String.format(Messages.CANT_CREATE_SERVICE_KEY_FOR_USER_PROVIDED_SERVICE, serviceInstance.getName())); } + return cc.getRestClient() .post() - .uri("/v3/service_credential_bindings") + .uri(CloudControllerV3Endpoints.SERVICE_CREDENTIAL_BINDINGS) .body(buildCreateServiceKeyBody(name, parameters, metadata, serviceInstance.getGuid())) .retrieve() .toBodilessEntity(); @@ -122,53 +97,66 @@ private ResponseEntity postServiceKey(String name, Map par private Map buildCreateServiceKeyBody(String name, Map parameters, Metadata metadata, UUID serviceInstanceGuid) { - Map body = new HashMap<>(); - body.put("type", "key"); - body.put("name", name); - body.put("relationships", Map.of("service_instance", Map.of("data", Map.of("guid", serviceInstanceGuid.toString())))); + Map resultBody = new HashMap<>(); + resultBody.put("type", "key"); + resultBody.put("name", name); + resultBody.put("relationships", Map.of("service_instance", Map.of("data", Map.of("guid", serviceInstanceGuid.toString())))); + if (parameters != null && !parameters.isEmpty()) { - body.put("parameters", parameters); + resultBody.put("parameters", parameters); } + Map metadataBody = buildMetadataBody(metadata); if (metadataBody != null) { - body.put("metadata", metadataBody); + resultBody.put("metadata", metadataBody); } - return body; + + return resultBody; } private Map buildMetadataBody(Metadata metadata) { if (metadata == null) { return null; } + Map metadataBody = new HashMap<>(); if (metadata.getLabels() != null) { metadataBody.put("labels", metadata.getLabels()); } + if (metadata.getAnnotations() != null) { metadataBody.put("annotations", metadata.getAnnotations()); } + return metadataBody.isEmpty() ? null : metadataBody; } private V3ServiceBinding getServiceKeyResourceByNameAndServiceInstanceGuid(String name, UUID serviceInstanceGuid) { - String query = "/v3/service_credential_bindings?per_page=" + DEFAULT_PAGE_SIZE + "&type=key&service_instance_guids=" - + serviceInstanceGuid + "&names=" + name; + String query = CloudControllerV3Endpoints.SERVICE_CREDENTIAL_BINDINGS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE + CloudControllerV3Endpoints.AMPERSAND_TYPE + "key" + + CloudControllerV3Endpoints.AMPERSAND_SERVICE_INSTANCE_GUIDS + serviceInstanceGuid + CloudControllerV3Endpoints.AMPERSAND_NAMES + + name; + List keys = cc.list(query, new ParameterizedTypeReference>() { }); - return keys.isEmpty() ? null : keys.get(0); + + return keys.isEmpty() ? null : keys.getFirst(); } private List listServiceKeyResources(UUID serviceInstanceGuid) { - String query = "/v3/service_credential_bindings?per_page=" + DEFAULT_PAGE_SIZE + "&type=key&service_instance_guids=" - + serviceInstanceGuid; + String query = CloudControllerV3Endpoints.SERVICE_CREDENTIAL_BINDINGS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE + CloudControllerV3Endpoints.AMPERSAND_TYPE + "key" + + CloudControllerV3Endpoints.AMPERSAND_SERVICE_INSTANCE_GUIDS + serviceInstanceGuid; + return cc.list(query, new ParameterizedTypeReference>() { }); } - // CF v3 returns 404 when fetching the details of a service key whose creation failed; treat that as empty credentials, matching the OSS impl. private Map getServiceKeyCredentials(String keyGuid) { - Optional details = cc.getOptional("/v3/service_credential_bindings/" + keyGuid + "/details", + Optional details = cc.getOptional(CloudControllerV3Endpoints.SERVICE_CREDENTIAL_BINDINGS + "/" + keyGuid + + "/details", V3ServiceKeyDetails.class); + return details.map(V3ServiceKeyDetails::credentials) .orElse(Collections.emptyMap()); } @@ -176,12 +164,15 @@ private Map getServiceKeyCredentials(String keyGuid) { private Optional extractJobGuid(ResponseEntity accepted) { URI location = accepted.getHeaders() .getLocation(); + if (location == null) { return Optional.empty(); } + String href = location.toString(); - int idx = href.lastIndexOf("/v3/jobs/"); - String jobGuid = idx < 0 ? href.substring(href.lastIndexOf('/') + 1) : href.substring(idx + "/v3/jobs/".length()); + int index = href.lastIndexOf("/v3/jobs/"); + String jobGuid = index < 0 ? href.substring(href.lastIndexOf('/') + 1) : href.substring(index + "/v3/jobs/".length()); + return Optional.of(jobGuid); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceOfferingsV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceOfferingsV3Operations.java index 9f5672110d..8d6cfe3cab 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceOfferingsV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServiceOfferingsV3Operations.java @@ -12,29 +12,8 @@ import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ServicePlanMapper; import org.springframework.core.ParameterizedTypeReference; -/** - * CF v3 service offerings operations of the in-house {@code CloudControllerRestClient}. Reproduces the HTTP shape, filtering and - * per-offering plan fan-out of the OSS {@code CloudControllerRestClientImpl#getServiceOfferings} on top of the shared - * {@link CloudControllerV3Client} machinery. - * - *

- * Endpoint map (see {@code docs/cf-java-client-migration/02-endpoint-inventory.md}): - *

- *
    - *
  • list offerings → {@code GET /v3/service_offerings?space_guids=} (paginated);
  • - *
  • resolve each offering's plans → {@code GET /v3/service_plans?service_offering_guids=} (paginated).
  • - *
- * - *

- * This mirrors the OSS {@code getServiceResources()} + {@code zipWithAuxiliaryServiceOfferingContent(...)} flow: the offerings are scoped - * to the target space, and each offering is enriched with the plans belonging to it before being mapped to the domain object. - *

- */ public class ServiceOfferingsV3Operations { - // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). - private static final int PER_PAGE = 5000; - private static final ParameterizedTypeReference> OFFERING_LIST_TYPE = new ParameterizedTypeReference<>() { }; private static final ParameterizedTypeReference> PLAN_LIST_TYPE = new ParameterizedTypeReference<>() { @@ -54,25 +33,29 @@ public List getServiceOfferings() { .toList(); } - // Mirrors the OSS getServiceResources(): list the target space's offerings (space_guids filter), walking all pages. private List getServiceOfferingResources() { - StringBuilder query = new StringBuilder("/v3/service_offerings?per_page=" + PER_PAGE); + StringBuilder query = new StringBuilder(CloudControllerV3Endpoints.SERVICE_OFFERINGS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE); + if (target != null && target.getGuid() != null) { - query.append("&space_guids=") + query.append(CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS) .append(target.getGuid()); } + return cc.list(query.toString(), OFFERING_LIST_TYPE); } - // Mirrors the OSS zipWithAuxiliaryServiceOfferingContent(...): fetch the offering's plans, then map to the domain object. private CloudServiceOffering toCloudServiceOfferingWithPlans(V3ServiceOffering serviceOffering) { List servicePlans = getServicePlans(serviceOffering.guid()); + return V3ServiceOfferingMapper.toCloudServiceOffering(serviceOffering, servicePlans); } - // Mirrors the OSS getServicePlanResourcesByServiceOfferingGuid(...): list the plans belonging to the given offering, walking all pages. private List getServicePlans(String serviceOfferingGuid) { - String uri = "/v3/service_plans?service_offering_guids=" + serviceOfferingGuid + "&per_page=" + PER_PAGE; + String uri = + CloudControllerV3Endpoints.SERVICE_PLANS + CloudControllerV3Endpoints.QUERY_SERVICE_OFFERING_GUIDS + serviceOfferingGuid + + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE; + return cc.list(uri, PLAN_LIST_TYPE) .stream() .map(V3ServicePlanMapper::toCloudServicePlan) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServicePlansV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServicePlansV3Operations.java index d3e2fd116d..7a9c033585 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServicePlansV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ServicePlansV3Operations.java @@ -1,9 +1,13 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; +import java.text.MessageFormat; import java.util.List; import java.util.Map; import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.domain.ServicePlanVisibility; @@ -12,31 +16,8 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; -/** - * CF v3 service plans operations of the in-house {@code CloudControllerRestClient}. Reproduces the HTTP shape, filtering and - * broker-scoped fan-out of the OSS {@code CloudControllerRestClientImpl#updateServicePlanVisibilityForBroker} on top of the shared - * {@link CloudControllerV3Client} machinery. - * - *

- * Endpoint map (see {@code docs/cf-java-client-migration/02-endpoint-inventory.md}): - *

- *
    - *
  • resolve broker by name → {@code GET /v3/service_brokers?names=} (404 when absent, as the broker is required);
  • - *
  • resolve the broker's offerings → {@code GET /v3/service_offerings?service_broker_guids=&space_guids=};
  • - *
  • resolve each offering's plans → {@code GET /v3/service_plans?service_offering_guids=};
  • - *
  • update visibility → {@code PATCH /v3/service_plans/{guid}/visibility} with body {@code { "type": "" }}.
  • - *
- * - *

- * The {@code visibility} body value is the lowercase CF visibility token ({@code public|admin|organization}); this matches the OSS impl, - * which passes {@code Visibility.from(visibility.toString())} where {@link ServicePlanVisibility#toString()} lower-cases the enum name. - *

- */ public class ServicePlansV3Operations { - // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). - private static final int PER_PAGE = 5000; - private static final ParameterizedTypeReference> BROKER_LIST_TYPE = new ParameterizedTypeReference<>() { }; private static final ParameterizedTypeReference> OFFERING_LIST_TYPE = new ParameterizedTypeReference<>() { @@ -60,28 +41,32 @@ public void updateServicePlanVisibilityForBroker(String name, ServicePlanVisibil } } - // Mirrors the OSS getServiceBroker(name, true): find the broker by name in the (global) broker list and 404 if it is absent. private UUID getRequiredServiceBrokerGuid(String name) { - String uri = "/v3/service_brokers?names=" + name + "&per_page=" + PER_PAGE; + String uri = CloudControllerV3Endpoints.SERVICE_BROKERS + CloudControllerV3Endpoints.QUERY_NAMES + name + + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE; + return cc.list(uri, BROKER_LIST_TYPE) .stream() .findFirst() .map(broker -> UUID.fromString(broker.guid())) - .orElseThrow(() -> new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", - "Service broker " + name + " not found.")); + .orElseThrow(() -> new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.SERVICE_BROKER_0_NOT_FOUND, name))); } - // Mirrors the OSS findServicePlansByBrokerGuid: list the broker's offerings (scoped to the target space), then each offering's plans. private List findServicePlanGuidsByBrokerGuid(UUID brokerGuid) { - List offeringGuids = findServiceOfferingGuidsByBrokerGuid(brokerGuid); - if (offeringGuids.isEmpty()) { + List offeredServicesGuids = findServiceOfferingGuidsByBrokerGuid(brokerGuid); + if (offeredServicesGuids.isEmpty()) { return List.of(); } - String offeringGuidsFilter = offeringGuids.stream() - .map(UUID::toString) - .reduce((a, b) -> a + "," + b) - .orElse(""); - String uri = "/v3/service_plans?service_offering_guids=" + offeringGuidsFilter + "&per_page=" + PER_PAGE; + + String offeringGuidsFilter = offeredServicesGuids.stream() + .map(UUID::toString) + .reduce((a, b) -> a + "," + b) + .orElse(""); + + String uri = CloudControllerV3Endpoints.SERVICE_PLANS + CloudControllerV3Endpoints.QUERY_SERVICE_OFFERING_GUIDS + + offeringGuidsFilter + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE; + return cc.list(uri, PLAN_LIST_TYPE) .stream() .map(plan -> UUID.fromString(plan.guid())) @@ -89,34 +74,37 @@ private List findServicePlanGuidsByBrokerGuid(UUID brokerGuid) { } private List findServiceOfferingGuidsByBrokerGuid(UUID brokerGuid) { - StringBuilder query = new StringBuilder("/v3/service_offerings?service_broker_guids=" + brokerGuid + "&per_page=" + PER_PAGE); + StringBuilder query = new StringBuilder(CloudControllerV3Endpoints.SERVICE_OFFERINGS + + CloudControllerV3Endpoints.QUERY_SERVICE_BROKER_GUIDS + brokerGuid + + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE); + if (target != null && target.getGuid() != null) { - query.append("&space_guids=") + query.append(CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS) .append(target.getGuid()); } + return cc.list(query.toString(), OFFERING_LIST_TYPE) .stream() .map(offering -> UUID.fromString(offering.guid())) .toList(); } - // Mirrors the OSS updateServicePlanVisibility(UUID, ServicePlanVisibility): PATCH the plan's visibility with the lowercase CF token. private void updateServicePlanVisibility(UUID servicePlanGuid, ServicePlanVisibility visibility) { cc.getRestClient() .patch() - .uri("/v3/service_plans/{guid}/visibility", servicePlanGuid) + .uri(CloudControllerV3Endpoints.SERVICE_PLAN_VISIBILITY, servicePlanGuid) .body(Map.of("type", visibility.toString())) .retrieve() .toBodilessEntity(); } - // Minimal wire models to resolve GUIDs by name/relationship without depending on the (separately-owned) broker/offering groups. - @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) - private record V3ServiceBrokerRef(@com.fasterxml.jackson.annotation.JsonProperty("guid") String guid) { + @JsonIgnoreProperties(ignoreUnknown = true) + private record V3ServiceBrokerRef(@JsonProperty("guid") String guid) { } - @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) - private record V3ServiceOfferingRef(@com.fasterxml.jackson.annotation.JsonProperty("guid") String guid) { + @JsonIgnoreProperties(ignoreUnknown = true) + private record V3ServiceOfferingRef(@JsonProperty("guid") String guid) { } } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/StacksV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/StacksV3Operations.java index 18e8537756..d8cc570f5a 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/StacksV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/StacksV3Operations.java @@ -1,9 +1,10 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; +import java.text.MessageFormat; import java.util.List; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; -import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudStack; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3ListResponse; import org.cloudfoundry.multiapps.controller.client.facade.rest.resources.V3Stack; @@ -11,26 +12,15 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; -/** - * CF v3 stacks operations of the cf-java-client replacement. Reproduces the HTTP shape and domain mapping of the OSS - * {@code CloudControllerRestClientImpl} stack methods: - *
    - *
  • {@code getStacks()} → {@code GET /v3/stacks} (paginated);
  • - *
  • {@code getStack(name[, required])} → {@code GET /v3/stacks?names=}, taking the first match.
  • - *
- * Stacks are a global resource, so — like the OSS impl — the listing is not filtered by the target space. - */ public class StacksV3Operations { private static final ParameterizedTypeReference> STACK_PAGE = new ParameterizedTypeReference<>() { }; private final CloudControllerV3Client cc; - private final CloudSpace target; - public StacksV3Operations(CloudControllerV3Client cc, CloudSpace target) { + public StacksV3Operations(CloudControllerV3Client cc) { this.cc = cc; - this.target = target; } public CloudStack getStack(String name) { @@ -38,22 +28,27 @@ public CloudStack getStack(String name) { } public CloudStack getStack(String name, boolean required) { - CloudStack stack = findStackByName(name); - if (stack == null && required) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Stack " + name + " not found."); + CloudStack resultStack = findStackByName(name); + + if (resultStack == null && required) { + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.STACK_0_NOT_FOUND, name)); } - return stack; + + return resultStack; } public List getStacks() { - return cc.list("/v3/stacks?per_page=5000", STACK_PAGE) + return cc.list(CloudControllerV3Endpoints.STACKS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE, STACK_PAGE) .stream() .map(V3StackMapper::toCloudStack) .toList(); } private CloudStack findStackByName(String name) { - return cc.list("/v3/stacks?names=" + name + "&per_page=5000", STACK_PAGE) + return cc.list(CloudControllerV3Endpoints.STACKS + CloudControllerV3Endpoints.QUERY_NAMES + name + + CloudControllerV3Endpoints.AMPERSAND_PER_PAGE + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE, STACK_PAGE) .stream() .map(V3StackMapper::toCloudStack) .findFirst() diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/TasksV3Operations.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/TasksV3Operations.java index 0fd5155e8d..486480dd58 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/TasksV3Operations.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/TasksV3Operations.java @@ -1,10 +1,12 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest; +import java.text.MessageFormat; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import org.cloudfoundry.multiapps.controller.Messages; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudTask; @@ -15,23 +17,8 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; -/** - * CF v3 task operations for the cf-java-client replacement. Reproduces the HTTP shape and domain-mapping of the OSS - * {@code CloudControllerRestClientImpl} task methods: - *
    - *
  • {@code getTask} → {@code GET /v3/tasks/{guid}}
  • - *
  • {@code getTasks} → {@code GET /v3/tasks?app_guids={appGuid}} (paginated)
  • - *
  • {@code runTask} → {@code POST /v3/apps/{appGuid}/tasks}
  • - *
  • {@code cancelTask} → {@code POST /v3/tasks/{guid}/actions/cancel}
  • - *
- * Application-name resolution mirrors the OSS impl's {@code getRequiredApplicationGuid}: it queries the target space and throws - * {@link CloudOperationException} with {@link HttpStatus#NOT_FOUND} when the application does not exist. - */ public class TasksV3Operations { - // CF v3 caps per_page at 5000; use a large page to minimise round-trips (the pagination walker still handles multiple pages). - private static final int DEFAULT_PAGE_SIZE = 5000; - private final CloudControllerV3Client cc; private final CloudSpace target; @@ -41,15 +28,20 @@ public TasksV3Operations(CloudControllerV3Client cc, CloudSpace target) { } public CloudTask getTask(UUID taskGuid) { - V3Task task = cc.get("/v3/tasks/" + taskGuid, V3Task.class); + V3Task task = cc.get(CloudControllerV3Endpoints.TASKS + "/" + taskGuid, V3Task.class); + return task == null ? null : V3TaskMapper.toCloudTask(task); } public List getTasks(String applicationName) { UUID applicationGuid = getRequiredApplicationGuid(applicationName); - String query = "/v3/tasks?per_page=" + DEFAULT_PAGE_SIZE + "&app_guids=" + applicationGuid; + + String query = CloudControllerV3Endpoints.TASKS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE + CloudControllerV3Endpoints.AMPERSAND_APP_GUIDS + applicationGuid; + List tasks = cc.list(query, new ParameterizedTypeReference>() { }); + return tasks.stream() .map(V3TaskMapper::toCloudTask) .toList(); @@ -57,21 +49,24 @@ public List getTasks(String applicationName) { public CloudTask runTask(String applicationName, CloudTask task) { UUID applicationGuid = getRequiredApplicationGuid(applicationName); + V3Task created = cc.getRestClient() .post() - .uri("/v3/apps/{guid}/tasks", applicationGuid) + .uri(CloudControllerV3Endpoints.APP_TASKS, applicationGuid) .body(buildCreateTaskBody(task)) .retrieve() .body(V3Task.class); + return created == null ? null : V3TaskMapper.toCloudTask(created); } public CloudTask cancelTask(UUID taskGuid) { V3Task cancelled = cc.getRestClient() .post() - .uri("/v3/tasks/{guid}/actions/cancel", taskGuid) + .uri(CloudControllerV3Endpoints.TASK_CANCEL, taskGuid) .retrieve() .body(V3Task.class); + return cancelled == null ? null : V3TaskMapper.toCloudTask(cancelled); } @@ -79,33 +74,41 @@ private Map buildCreateTaskBody(CloudTask task) { Map body = new HashMap<>(); body.put("command", task.getCommand()); body.put("name", task.getName()); + CloudTask.Limits limits = task.getLimits(); if (limits != null) { if (limits.getMemory() != null) { body.put("memory_in_mb", limits.getMemory()); } + if (limits.getDisk() != null) { body.put("disk_in_mb", limits.getDisk()); } } + return body; } - // Mirrors the OSS impl's getRequiredApplicationGuid: look up the app in the target space and 404 if it is absent. private UUID getRequiredApplicationGuid(String applicationName) { - StringBuilder query = new StringBuilder("/v3/apps?per_page=" + DEFAULT_PAGE_SIZE); + StringBuilder query = new StringBuilder(CloudControllerV3Endpoints.APPS + CloudControllerV3Endpoints.QUERY_PER_PAGE + + CloudControllerV3Endpoints.DEFAULT_PAGE_SIZE); + if (target != null && target.getGuid() != null) { - query.append("&space_guids=") + query.append(CloudControllerV3Endpoints.AMPERSAND_SPACE_GUIDS) .append(target.getGuid()); } - query.append("&names=") + + query.append(CloudControllerV3Endpoints.AMPERSAND_NAMES) .append(applicationName); List apps = cc.list(query.toString(), new ParameterizedTypeReference>() { }); + if (apps.isEmpty()) { - throw new CloudOperationException(HttpStatus.NOT_FOUND, "Not Found", "Application " + applicationName + " not found."); + throw new CloudOperationException(HttpStatus.NOT_FOUND, Messages.NOT_FOUND, + MessageFormat.format(Messages.APPLICATION_0_NOT_FOUND, applicationName)); } - return UUID.fromString(apps.get(0) + + return UUID.fromString(apps.getFirst() .guid()); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/LogCacheReadResponse.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/LogCacheReadResponse.java index 3965703cdb..010e348dea 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/LogCacheReadResponse.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/LogCacheReadResponse.java @@ -7,18 +7,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -/** - * Thin Jackson wire-model of a Log-Cache {@code read} response ({@code GET /api/v1/read/{source_id}}), which is a plain JSON/REST API: - * - *
- * { "envelopes": { "batch": [
- *     { "source_id": "...", "timestamp": "1699999999000000000", "tags": { "source_type": "APP/PROC/WEB" },
- *       "log": { "payload": "<base64>", "type": "OUT" | "ERR" } } ] } }
- * 
- * - * Replaces the OSS {@code org.cloudfoundry.logcache.v1.*} types so the log-cache client no longer depends on cf-java-client. Only the - * fields the mapper needs are declared. - */ @JsonIgnoreProperties(ignoreUnknown = true) public record LogCacheReadResponse(@JsonProperty("envelopes") EnvelopeBatch envelopes) { @@ -26,6 +14,7 @@ public List batch() { if (envelopes == null || envelopes.batch() == null) { return Collections.emptyList(); } + return envelopes.batch(); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Application.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Application.java index 2eab011a70..9394d5c894 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Application.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Application.java @@ -42,10 +42,6 @@ public record V3ToOneRelationship(@JsonProperty("data") V3RelationshipData data) public record V3RelationshipData(@JsonProperty("guid") String guid) { } - /** - * The CF v3 app environment variables response ({@code GET /v3/apps/{guid}/environment_variables}): - * {@code { "var": { "KEY": "VALUE", ... } }}. - */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3EnvironmentVariables(@JsonProperty("var") Map var) { } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ApplicationMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ApplicationMapper.java index ae4de63eb3..cfab780cd7 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ApplicationMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ApplicationMapper.java @@ -10,10 +10,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.Lifecycle; import org.cloudfoundry.multiapps.controller.client.facade.domain.LifecycleType; -/** - * Maps the {@link V3Application} wire model to the project's {@link CloudApplication} domain object. Mirrors the OSS - * {@code RawCloudApplication} adapter field-for-field, so both client implementations yield identical domain objects. - */ public final class V3ApplicationMapper { private static final String BUILDPACKS = "buildpacks"; @@ -41,6 +37,7 @@ private static Lifecycle parseLifecycle(V3Application.V3Lifecycle lifecycle) { if (lifecycle == null || lifecycle.type() == null) { return null; } + return ImmutableLifecycle.builder() .type(LifecycleType.valueOf(lifecycle.type() .toUpperCase())) @@ -52,6 +49,7 @@ private static Map extractLifecycleData(V3Application.V3Lifecycl if (data == null) { return Collections.emptyMap(); } + return Map.of(BUILDPACKS, data.buildpacks() == null ? Collections.emptyList() : data.buildpacks(), STACK, data.stack() == null ? "" : data.stack()); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEvent.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEvent.java index de24e0babc..95907cb759 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEvent.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEvent.java @@ -12,10 +12,6 @@ * "target": { "guid": "...", "type": "app", "name": "..." }, * "data": { ... }, "space": {...}, "organization": {...} } * - * - * Only the fields the domain {@code CloudEvent} needs are mapped; everything else is ignored so CF can evolve the payload without breaking - * us. Mirrors the CF v3 JSON consumed by the OSS {@code AuditEventResource} adapter (whose {@code actor}/{@code target} carry - * {@code guid}/{@code name}/{@code type}). */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3AuditEvent(@JsonProperty("guid") String guid, @JsonProperty("created_at") String createdAt, diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEventMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEventMapper.java index ede9fc9837..e06b1b1518 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEventMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3AuditEventMapper.java @@ -5,12 +5,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudEvent; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudEvent.ImmutableParticipant; -/** - * Maps the {@link V3AuditEvent} wire model to the project's {@link CloudEvent} domain object. Mirrors the OSS {@code RawCloudEvent} adapter - * field-for-field, so both client implementations yield identical domain objects. - *

- * As in the OSS adapter, a {@code null} actor/target still yields an empty (non-{@code null}) {@link Participant}. - */ public final class V3AuditEventMapper { private V3AuditEventMapper() { @@ -30,6 +24,7 @@ private static Participant parseParticipant(V3AuditEvent.V3Participant participa return ImmutableParticipant.builder() .build(); } + return ImmutableParticipant.builder() .guid(V3ResourceMappers.parseNullableGuid(participant.guid())) .name(participant.name()) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3BuildMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3BuildMapper.java index c40417b701..fffddea5e7 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3BuildMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3BuildMapper.java @@ -7,10 +7,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudBuild.ImmutablePackageInfo; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableDropletInfo; -/** - * Maps the {@link V3Build} wire model to the project's {@link CloudBuild} domain object. Mirrors the OSS {@code RawCloudBuild} adapter - * field-for-field, so both client implementations yield identical domain objects. - */ public final class V3BuildMapper { private V3BuildMapper() { @@ -31,6 +27,7 @@ private static CloudBuild.CreatedBy parseCreatedBy(V3Build.V3CreatedBy createdBy if (createdBy == null) { return null; } + return ImmutableCreatedBy.builder() .guid(V3ResourceMappers.parseNullableGuid(createdBy.guid())) .name(createdBy.name()) @@ -41,6 +38,7 @@ private static CloudBuild.PackageInfo parsePackageInfo(V3Build.V3PackageReferenc if (inputPackage == null) { return null; } + return ImmutablePackageInfo.of(V3ResourceMappers.parseNullableGuid(inputPackage.guid())); } @@ -48,6 +46,7 @@ private static DropletInfo parseDropletInfo(V3Build.V3DropletReference droplet) if (droplet == null || droplet.guid() == null) { return null; } + return ImmutableDropletInfo.builder() .guid(V3ResourceMappers.parseNullableGuid(droplet.guid())) .build(); diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Domain.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Domain.java index 8bc9403d99..2234f4c698 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Domain.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Domain.java @@ -12,9 +12,6 @@ * "metadata": { "labels": {...}, "annotations": {...} }, * "relationships": { "organization": { "data": { "guid": "..." } | null } } } * - * - * The {@code relationships.organization.data} discriminates shared domains (data == null) from private domains (data != null); this - * mirrors the filtering the OSS impl applied on {@code DomainResource.getRelationships().getOrganization().getData()}. */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3Domain(@JsonProperty("guid") String guid, @JsonProperty("name") String name, @@ -33,10 +30,6 @@ public record V3ToOneRelationship(@JsonProperty("data") V3RelationshipData data) public record V3RelationshipData(@JsonProperty("guid") String guid) { } - /** - * True when this domain is scoped to an organization (a private domain), i.e. its {@code relationships.organization.data} is present. - * A shared domain has {@code data == null}. Mirrors the OSS {@code getPrivateDomainResources}/{@code getSharedDomainResources} filters. - */ public boolean isPrivate() { return relationships != null && relationships.organization() != null && relationships.organization() .data() != null; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3DomainMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3DomainMapper.java index 98e1c96bb8..b0f9af0eed 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3DomainMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3DomainMapper.java @@ -3,10 +3,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudDomain; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudDomain; -/** - * Maps the {@link V3Domain} wire model to the project's {@link CloudDomain} domain object. Mirrors the OSS {@code RawCloudDomain} adapter - * field-for-field (metadata from the resource envelope + {@code name}), so both client implementations yield identical domain objects. - */ public final class V3DomainMapper { private V3DomainMapper() { diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Droplet.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Droplet.java index 5281407ddd..f69486e983 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Droplet.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Droplet.java @@ -11,8 +11,6 @@ *

  * { "guid": "...", "links": { "package": { "href": "https://.../v3/packages/{guid}" }, ... } }
  * 
- * - * The client only needs the droplet {@code guid} and the {@code package} link, from which the package GUID is derived. */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3Droplet(@JsonProperty("guid") String guid, @JsonProperty("links") Map links) { diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3InstancesInfoMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3InstancesInfoMapper.java index f111e8acc6..ed1109da12 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3InstancesInfoMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3InstancesInfoMapper.java @@ -10,14 +10,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.InstanceState; import org.cloudfoundry.multiapps.controller.client.facade.domain.InstancesInfo; -/** - * Maps the {@link V3Process.V3ProcessStats} wire model to the project's {@link InstancesInfo} domain object. Mirrors the OSS - * {@code RawInstancesInfo} adapter field-for-field, so both client implementations yield identical domain objects. - *

- * The instance state string comes from CF ({@code GET /v3/apps/{guid}/processes/web/stats}) and is resolved via - * {@link InstanceState#valueOfWithDefault(String)} (which falls back to {@link InstanceState#UNKNOWN} for unrecognised values), so no OSS - * leaf type is involved. - */ public final class V3InstancesInfoMapper { private V3InstancesInfoMapper() { @@ -33,6 +25,7 @@ private static List parseProcessStatistics(List - * - * The async job poller in {@link org.cloudfoundry.multiapps.controller.client.facade.rest.CloudControllerV3Client} polls this until - * {@code state} reaches a terminal value (COMPLETE / FAILED), mirroring the OSS {@code JobV3Util} behaviour. */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3Job(@JsonProperty("guid") String guid, @JsonProperty("created_at") String createdAt, diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3JobMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3JobMapper.java index 89a153a5e4..b643ed0130 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3JobMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3JobMapper.java @@ -7,10 +7,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudAsyncJob; import org.cloudfoundry.multiapps.controller.client.facade.domain.JobState; -/** - * Maps the {@link V3Job} wire model to the project's {@link CloudAsyncJob} domain object. Mirrors the OSS {@code RawCloudAsyncJob} adapter - * field-for-field, so both client implementations yield identical domain objects. - */ public final class V3JobMapper { private V3JobMapper() { @@ -35,6 +31,7 @@ private static String getWarnings(V3Job job) { if (warnings == null) { return ""; } + return warnings.stream() .map(V3Job.V3Warning::detail) .collect(Collectors.joining(",")); @@ -45,6 +42,7 @@ private static String getErrors(V3Job job) { if (errors == null) { return ""; } + return errors.stream() .map(V3JobMapper::joinErrorDetails) .collect(Collectors.joining(",")); diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ListResponse.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ListResponse.java index 43e91ce3a5..5476df61e6 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ListResponse.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ListResponse.java @@ -15,12 +15,6 @@ * "resources": [ ... ] * } * - * - * Only the fields the client actually needs are mapped; everything else is ignored so CF can evolve the payload without breaking us. - * The {@link org.cloudfoundry.multiapps.controller.client.facade.rest.CloudControllerV3Client pagination walker} follows - * {@code pagination.next.href} until it is {@code null}, accumulating {@link #resources()} from every page. - * - * @param the wire type of a single resource in this list */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3ListResponse(@JsonProperty("pagination") Pagination pagination, @JsonProperty("resources") List resources) { @@ -33,6 +27,7 @@ public String nextPageHref() { if (pagination == null || pagination.next() == null) { return null; } + return pagination.next() .href(); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3PackageMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3PackageMapper.java index 4f62f4b347..d0ba2e0616 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3PackageMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3PackageMapper.java @@ -1,15 +1,12 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; +import org.cloudfoundry.multiapps.controller.client.facade.domain.BitsData; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudPackage; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableBitsData; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudPackage; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableDockerData; import org.cloudfoundry.multiapps.controller.client.facade.domain.Status; -/** - * Maps the {@link V3Package} wire model to the project's {@link CloudPackage} domain object. Mirrors the OSS {@code RawCloudPackage} - * adapter field-for-field, so both client implementations yield identical domain objects. - */ public final class V3PackageMapper { private V3PackageMapper() { @@ -37,6 +34,7 @@ private static CloudPackage.PackageData parseData(V3Package resource) { if (resource.type() != null && CloudPackage.Type.from(resource.type()) == CloudPackage.Type.BITS) { return parseBitsData(resource.data()); } + return parseDockerData(resource.data()); } @@ -45,17 +43,19 @@ private static CloudPackage.PackageData parseBitsData(V3Package.V3PackageData da return ImmutableBitsData.builder() .build(); } + return ImmutableBitsData.builder() .checksum(parseBitsChecksum(data.checksum())) .error(data.error()) .build(); } - private static org.cloudfoundry.multiapps.controller.client.facade.domain.BitsData.Checksum - parseBitsChecksum(V3Package.V3Checksum checksum) { + private static BitsData.Checksum + parseBitsChecksum(V3Package.V3Checksum checksum) { if (checksum == null) { return null; } + return ImmutableBitsData.ImmutableChecksum.builder() .algorithm(checksum.type()) .value(checksum.value()) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Process.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Process.java index 263a4d29dc..d010a950ee 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Process.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Process.java @@ -7,12 +7,6 @@ /** * Thin Jackson wire-models for the CF v3 process family of endpoints consumed by the client: - *

    - *
  • {@link V3Process} — {@code GET /v3/apps/{guid}/processes/web} (also {@code GET /v3/processes/{guid}});
  • - *
  • {@link V3ProcessStats} — {@code GET /v3/apps/{guid}/processes/web/stats};
  • - *
  • {@link V3AppFeatures} / {@link V3AppFeature} — {@code GET /v3/apps/{guid}/features};
  • - *
  • {@link V3SshEnabled} — {@code GET /v3/apps/{guid}/ssh_enabled}.
  • - *
* *
  * // process
@@ -20,8 +14,6 @@
  *   "health_check": { "type": "port|http|process", "data": { "timeout": N, "invocation_timeout": N, "endpoint": "...", "interval": N } },
  *   "readiness_health_check": { "type": "process|http|port", "data": { "invocation_timeout": N, "endpoint": "...", "interval": N } } }
  * 
- * - * Every record ignores unknown fields so CF can evolve the payload without breaking us. */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3Process(@JsonProperty("guid") String guid, @JsonProperty("command") String command, @@ -39,10 +31,6 @@ public record V3HealthCheckData(@JsonProperty("timeout") Integer timeout, @JsonProperty("endpoint") String endpoint, @JsonProperty("interval") Integer interval) { } - /** - * The CF v3 process statistics response ({@code GET /v3/apps/{guid}/processes/web/stats}): - * {@code { "resources": [ { "index": 0, "state": "RUNNING", "routable": true }, ... ] }}. - */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3ProcessStats(@JsonProperty("resources") List resources) { } @@ -52,18 +40,10 @@ public record V3ProcessStatsResource(@JsonProperty("index") Integer index, @Json @JsonProperty("routable") String routable) { } - /** - * The CF v3 app features response ({@code GET /v3/apps/{guid}/features}): - * {@code { "resources": [ { "name": "ssh", "enabled": true }, ... ] }}. - */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3AppFeature(@JsonProperty("name") String name, @JsonProperty("enabled") Boolean enabled) { } - /** - * The CF v3 app SSH-enabled response ({@code GET /v3/apps/{guid}/ssh_enabled}): - * {@code { "enabled": true, "reason": "..." }}. - */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3SshEnabled(@JsonProperty("enabled") Boolean enabled, @JsonProperty("reason") String reason) { } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ProcessMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ProcessMapper.java index 94de49472a..49718c5fd4 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ProcessMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ProcessMapper.java @@ -4,10 +4,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.HealthCheckType; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudProcess; -/** - * Maps the {@link V3Process} wire model to the project's {@link CloudProcess} domain object. Mirrors the OSS - * {@code RawCloudProcess} adapter field-for-field, so both client implementations yield identical domain objects. - */ public final class V3ProcessMapper { private V3ProcessMapper() { @@ -21,6 +17,7 @@ public static CloudProcess toCloudProcess(V3Process process) { String healthCheckHttpEndpoint = null; Integer healthCheckInvocationTimeout = null; Integer healthCheckInterval = null; + if (healthCheck != null && healthCheck.data() != null) { V3Process.V3HealthCheckData healthCheckData = healthCheck.data(); healthCheckTimeout = healthCheckData.timeout(); @@ -32,6 +29,7 @@ public static CloudProcess toCloudProcess(V3Process process) { Integer readinessHealthCheckInvocationTimeout = null; String readinessHealthCheckHttpEndpoint = null; Integer readinessHealthCheckInterval = null; + if (readinessHealthCheck != null && readinessHealthCheck.data() != null) { V3Process.V3HealthCheckData readinessHealthCheckData = readinessHealthCheck.data(); readinessHealthCheckInvocationTimeout = readinessHealthCheckData.invocationTimeout(); diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ResourceMappers.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ResourceMappers.java index e5fc872a2b..e8500919ee 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ResourceMappers.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ResourceMappers.java @@ -1,20 +1,16 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.time.format.DateTimeParseException; import java.util.UUID; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudMetadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Helpers shared by all CF v3 wire-model mappers: parse the resource envelope fields ({@code guid}, {@code created_at}, - * {@code updated_at}) into the project's {@link CloudMetadata}, and parse GUIDs/dates leniently — mirroring the behaviour of the OSS - * {@code RawCloudEntity} so the two client implementations produce identical domain objects. - */ public final class V3ResourceMappers { private static final Logger LOGGER = LoggerFactory.getLogger(V3ResourceMappers.class); @@ -30,14 +26,11 @@ public static CloudMetadata parseMetadata(String guid, String createdAt, String .build(); } - /** - * Build the project's v3 {@link Metadata} (labels/annotations) from the wire representation. Kept as the OSS leaf type for now; it - * is re-homed to a project-owned type in the leaf-type migration step. - */ public static Metadata toV3Metadata(V3Metadata metadata) { if (metadata == null) { return null; } + return Metadata.builder() .labels(metadata.labels()) .annotations(metadata.annotations()) @@ -48,6 +41,7 @@ public static UUID parseNullableGuid(String guid) { if (guid == null) { return null; } + try { return UUID.fromString(guid); } catch (IllegalArgumentException e) { @@ -60,10 +54,11 @@ public static LocalDateTime parseNullableDate(String date) { if (date == null) { return null; } + try { // CF v3 timestamps are ISO-8601 with offset, e.g. 2026-08-04T10:15:30Z - return java.time.OffsetDateTime.parse(date) - .toLocalDateTime(); + return OffsetDateTime.parse(date) + .toLocalDateTime(); } catch (DateTimeParseException e) { LOGGER.warn("Could not parse date string: \"{}\"", date, e); return null; diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Role.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Role.java index f280bc0267..f192169500 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Role.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Role.java @@ -12,9 +12,6 @@ * "relationships": { "user": { "data": { "guid": "..." } }, "space": { "data": { "guid": "..." } | null }, * "organization": { "data": { "guid": "..." } | null } } } * - * - * Only {@code type} is consumed by the mapper (mirroring the OSS {@code RawUserRole}); the remaining fields are documented for - * completeness and ignored so CF can evolve the payload without breaking us. */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3Role(@JsonProperty("guid") String guid, @JsonProperty("created_at") String createdAt, diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RoleMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RoleMapper.java index 5b75520137..7e5d4372a1 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RoleMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RoleMapper.java @@ -2,10 +2,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.UserRole; -/** - * Maps the {@link V3Role} wire model to the project's {@link UserRole} domain enum. Passes the raw CF v3 role {@code type} wire value - * (e.g. {@code space_developer}) straight to {@link UserRole#fromRoleType(String)}, so this mapper depends on no OSS types. - */ public final class V3RoleMapper { private V3RoleMapper() { diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Route.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Route.java index 6cc6e7f991..b29aa562d7 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Route.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Route.java @@ -16,9 +16,6 @@ * "metadata": { "labels": {...}, "annotations": {...} }, * "relationships": { "space": { "data": { "guid": "..." } }, "domain": { "data": { "guid": "..." } } } } * - * - * Mirrors exactly the fields the OSS {@code RawCloudRoute} adapter consumes from {@code org.cloudfoundry.client.v3.routes.Route}, so - * both client implementations yield identical {@code CloudRoute} domain objects. */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3Route(@JsonProperty("guid") String guid, @JsonProperty("host") String host, @JsonProperty("path") String path, diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RouteMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RouteMapper.java index b33b856d80..6c8691d6e4 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RouteMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3RouteMapper.java @@ -12,35 +12,22 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableRouteDestination; import org.cloudfoundry.multiapps.controller.client.facade.domain.RouteDestination; -/** - * Maps the {@link V3Route} wire model to the project's {@link CloudRoute} domain object. Mirrors the OSS {@code RawCloudRoute} adapter - * field-for-field (including the domain-name derivation from the route URL and the requested-protocol lookup by application GUID), so - * both client implementations yield identical domain objects. - */ public final class V3RouteMapper { private V3RouteMapper() { } - /** - * Maps a route without an application context (equivalent to OSS {@code ImmutableRawCloudRoute.of(route)}): the requested protocol is - * always {@code null}. - */ public static CloudRoute toCloudRoute(V3Route route) { return toCloudRoute(route, null); } - /** - * Maps a route in the context of a specific application (equivalent to OSS - * {@code ImmutableRawCloudRoute.builder().route(route).applicationGuid(applicationGuid).build()}): the requested protocol is taken - * from the destination that points at {@code applicationGuid}. - */ public static CloudRoute toCloudRoute(V3Route route, UUID applicationGuid) { List destinations = mapDestinations(route); String domainGuid = route.relationships() .domain() .data() .guid(); + return ImmutableCloudRoute.builder() .metadata(V3ResourceMappers.parseMetadata(route.guid(), route.createdAt(), route.updatedAt())) .appsUsingRoute(route.destinations() @@ -62,15 +49,19 @@ private static String computeDomain(V3Route route) { String domain = route.url(); String host = route.host() == null ? "" : route.host(); String path = route.path() == null ? "" : route.path(); + if (!host.isEmpty()) { domain = domain.substring(host.length() + 1); } + if (!path.isEmpty()) { domain = domain.substring(0, domain.indexOf('/')); } + if (route.port() != null) { domain = domain.substring(0, domain.indexOf(':')); } + return domain; } @@ -94,6 +85,7 @@ private static String computeRequestedProtocol(List destinatio if (applicationGuid == null) { return null; } + return destinations.stream() .filter(destination -> Objects.equals(destination.getApplicationGuid(), applicationGuid)) .findFirst() diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBindingMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBindingMapper.java index 8a06576d65..96a3bf82c6 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBindingMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBindingMapper.java @@ -9,15 +9,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableServiceCredentialBindingOperation; import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceCredentialBindingOperation; -/** - * Maps the {@link V3ServiceBinding} wire model to the project's {@link CloudServiceBinding} domain object. Mirrors the OSS - * {@code RawCloudServiceBinding} adapter field-for-field, so both client implementations yield identical domain objects: - *
    - *
  • {@code applicationGuid} — nullable; from {@code relationships.app.data.guid} (absent for {@code type=key} bindings);
  • - *
  • {@code serviceInstanceGuid} — required; from {@code relationships.service_instance.data.guid};
  • - *
  • {@code serviceBindingOperation} — from {@code last_operation}, mirroring {@link ServiceCredentialBindingOperation#from}.
  • - *
- */ public final class V3ServiceBindingMapper { private V3ServiceBindingMapper() { @@ -37,9 +28,11 @@ private static UUID parseApplicationGuid(V3ServiceBinding binding) { V3ServiceBinding.V3ToOneRelationship application = binding.relationships() == null ? null : binding.relationships() .application(); + if (application == null || application.data() == null) { return null; } + return V3ResourceMappers.parseNullableGuid(application.data() .guid()); } @@ -55,6 +48,7 @@ private static ServiceCredentialBindingOperation parseServiceBindingOperation(V3 if (lastOperation == null) { return null; } + return ImmutableServiceCredentialBindingOperation.builder() .type(ServiceCredentialBindingOperation.Type.fromString(lastOperation.type())) .state(ServiceCredentialBindingOperation.State.fromString(lastOperation.state())) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBroker.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBroker.java index 4be2e36a9b..2fe6d5d6d8 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBroker.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBroker.java @@ -11,8 +11,6 @@ * "metadata": { "labels": {...}, "annotations": {...} }, * "relationships": { "space": { "data": { "guid": "..." } | null } } } * - * - * Only the fields the client maps into {@code CloudServiceBroker} are declared; everything else is ignored. */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3ServiceBroker(@JsonProperty("guid") String guid, @JsonProperty("name") String name, @JsonProperty("url") String url, diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBrokerMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBrokerMapper.java index 34d1bab18d..8b2b1cff2b 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBrokerMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceBrokerMapper.java @@ -5,15 +5,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBroker; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceBroker; -/** - * Maps the {@link V3ServiceBroker} wire model to the project's {@link CloudServiceBroker} domain object. Mirrors the OSS - * {@code RawCloudServiceBroker} adapter field-for-field, so both client implementations yield identical domain objects. - * - *

- * As in the OSS adapter, {@code username}/{@code password} are write-only credentials that CF never returns, so they are not mapped; - * only {@code name}, {@code url} and the (optional) space GUID from {@code relationships.space.data.guid} are populated. - *

- */ public final class V3ServiceBrokerMapper { private V3ServiceBrokerMapper() { diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstance.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstance.java index a220b724b3..1f01ad0c34 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstance.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstance.java @@ -16,14 +16,13 @@ * "metadata": { "labels": {...}, "annotations": {...} }, * "relationships": { "space": { "data": { "guid": "..." } }, "service_plan": { "data": { "guid": "..." } } } } * - * - * Only the fields the client needs are mapped; everything else is ignored so CF can evolve the payload without breaking us. */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3ServiceInstance(@JsonProperty("guid") String guid, @JsonProperty("name") String name, @JsonProperty("type") String type, @JsonProperty("created_at") String createdAt, @JsonProperty("updated_at") String updatedAt, @JsonProperty("tags") List tags, @JsonProperty("syslog_drain_url") String syslogDrainUrl, - @JsonProperty("last_operation") V3LastOperation lastOperation, @JsonProperty("metadata") V3Metadata metadata, + @JsonProperty("last_operation") V3LastOperation lastOperation, + @JsonProperty("metadata") V3Metadata metadata, @JsonProperty("relationships") V3Relationships relationships) { @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstanceMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstanceMapper.java index 45b6e73b51..2746c0cb29 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstanceMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceInstanceMapper.java @@ -1,29 +1,15 @@ package org.cloudfoundry.multiapps.controller.client.facade.rest.resources; -import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceInstance; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceInstanceType; import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceOperation; -/** - * Maps the {@link V3ServiceInstance} wire model to the project's {@link CloudServiceInstance} domain object. Mirrors the OSS - * {@code RawCloudServiceInstance} and {@code RawV3CloudServiceInstance} adapters field-for-field, so both client implementations yield - * identical domain objects. - * - *

- * The OSS {@code RawCloudServiceInstance} derives {@code plan} and {@code label} from auxiliary {@code ServicePlan} / - * {@code ServiceOffering} resources it is zipped with; those names are resolved by the operations helper and passed in here. - * {@code RawV3CloudServiceInstance} (used for the {@code byNames} batch lookups) maps only the base fields with no auxiliary content. - *

- */ public final class V3ServiceInstanceMapper { private V3ServiceInstanceMapper() { } - /** - * Full mapping mirroring the OSS {@code RawCloudServiceInstance.derive()}: base fields plus the resolved plan/offering names. - */ public static CloudServiceInstance toCloudServiceInstance(V3ServiceInstance serviceInstance, String planName, String labelName) { return ImmutableCloudServiceInstance.builder() .metadata(V3ResourceMappers.parseMetadata(serviceInstance.guid(), @@ -35,16 +21,12 @@ public static CloudServiceInstance toCloudServiceInstance(V3ServiceInstance serv .label(labelName) .type(parseType(serviceInstance.type())) .tags(serviceInstance.tags() == null ? java.util.Collections.emptyList() - : serviceInstance.tags()) + : serviceInstance.tags()) .lastOperation(parseLastOperation(serviceInstance.lastOperation())) .syslogDrainUrl(serviceInstance.syslogDrainUrl()) .build(); } - /** - * Base-only mapping mirroring the OSS {@code RawV3CloudServiceInstance.derive()}: metadata, v3Metadata, name and tags (used by the - * {@code byNames} batch lookups, which never resolve plan/offering and leave {@code type} unset). - */ public static CloudServiceInstance toCloudServiceInstanceWithoutAuxiliaryContent(V3ServiceInstance serviceInstance) { return ImmutableCloudServiceInstance.builder() .metadata(V3ResourceMappers.parseMetadata(serviceInstance.guid(), @@ -53,35 +35,33 @@ public static CloudServiceInstance toCloudServiceInstanceWithoutAuxiliaryContent .v3Metadata(V3ResourceMappers.toV3Metadata(serviceInstance.metadata())) .name(serviceInstance.name()) .tags(serviceInstance.tags() == null ? java.util.Collections.emptyList() - : serviceInstance.tags()) + : serviceInstance.tags()) .build(); } - /** - * Map the CF v3 {@code type} JSON value ({@code managed} / {@code user-provided}) to the project's {@link ServiceInstanceType}. - */ private static ServiceInstanceType parseType(String type) { if (type == null) { return null; } + for (ServiceInstanceType serviceInstanceType : ServiceInstanceType.values()) { if (serviceInstanceType.getValue() .equals(type)) { return serviceInstanceType; } } + return null; } - /** - * Mirrors {@code ServiceOperation.fromLastOperation(LastOperation)}: a {@code null} operation, type or state yields {@code null}. - */ private static ServiceOperation parseLastOperation(V3ServiceInstance.V3LastOperation lastOperation) { if (lastOperation == null || lastOperation.type() == null || lastOperation.state() == null) { return null; } + ServiceOperation.Type type = ServiceOperation.Type.fromString(lastOperation.type()); ServiceOperation.State state = ServiceOperation.State.fromString(lastOperation.state()); + return new ServiceOperation(type, lastOperation.description(), state); } diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyDetails.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyDetails.java index d844dfaa51..2d1df8b410 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyDetails.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyDetails.java @@ -13,9 +13,6 @@ *
  * { "credentials": { ... }, "syslog_drain_url": "...", "volume_mounts": [ ... ] }
  * 
- * - * Mirrors the OSS impl's {@code serviceBindingsV3().getDetails(...)} call. The CF v3 API returns a 404 when fetching the details of a - * service key whose creation failed; callers translate that into empty credentials (see {@code ServiceKeysV3Operations}). */ @JsonIgnoreProperties(ignoreUnknown = true) public record V3ServiceKeyDetails(@JsonProperty("credentials") Map credentials, diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyMapper.java index bc617456c8..23a6f16816 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceKeyMapper.java @@ -8,20 +8,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableServiceCredentialBindingOperation; import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceCredentialBindingOperation; -/** - * Maps a CF v3 service credential binding of {@code type=key} ({@link V3ServiceBinding}) to the project's {@link CloudServiceKey} domain - * object. Mirrors the OSS {@code RawCloudServiceKey#derive()} adapter field-for-field, so both client implementations yield identical - * domain objects: - *
    - *
  • {@code metadata} — from the binding resource envelope ({@code guid}, {@code created_at}, {@code updated_at});
  • - *
  • {@code v3Metadata} — labels/annotations from {@code metadata};
  • - *
  • {@code name} — the service key name;
  • - *
  • {@code credentials} — nullable; fetched separately from the binding details endpoint (absent when listing without - * credentials);
  • - *
  • {@code serviceInstance} — the owning (already-derived) {@link CloudServiceInstance};
  • - *
  • {@code serviceKeyOperation} — from {@code last_operation}, mirroring {@link ServiceCredentialBindingOperation#from}.
  • - *
- */ public final class V3ServiceKeyMapper { private V3ServiceKeyMapper() { @@ -47,6 +33,7 @@ private static ServiceCredentialBindingOperation parseServiceKeyOperation(V3Serv if (lastOperation == null) { return null; } + return ImmutableServiceCredentialBindingOperation.builder() .type(ServiceCredentialBindingOperation.Type.fromString(lastOperation.type())) .state(ServiceCredentialBindingOperation.State.fromString(lastOperation.state())) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceOfferingMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceOfferingMapper.java index 7393d9a97e..d98d5f2b3f 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceOfferingMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServiceOfferingMapper.java @@ -7,11 +7,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServicePlan; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceOffering; -/** - * Maps the {@link V3ServiceOffering} wire model (plus the service plans fetched separately for it) to the project's - * {@link CloudServiceOffering} domain object. Mirrors the OSS {@code RawCloudServiceOffering} adapter field-for-field, so both client - * implementations yield identical domain objects. - */ public final class V3ServiceOfferingMapper { private V3ServiceOfferingMapper() { @@ -39,6 +34,7 @@ private static Boolean extractBindable(V3ServiceOffering.V3BrokerCatalog brokerC if (brokerCatalog == null || brokerCatalog.features() == null) { return null; } + return brokerCatalog.features() .bindable(); } @@ -56,6 +52,7 @@ private static String extractBrokerId(V3ServiceOffering.V3Relationships relation .data() == null) { return null; } + return relationships.serviceBroker() .data() .guid(); diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServicePlanMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServicePlanMapper.java index 4a7c197e67..9b6711fa94 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServicePlanMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3ServicePlanMapper.java @@ -3,13 +3,8 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServicePlan; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServicePlan; -/** - * Maps the {@link V3ServicePlan} wire model to the project's {@link CloudServicePlan} domain object. Mirrors the OSS - * {@code RawCloudServicePlan} adapter field-for-field, so both client implementations yield identical domain objects. - */ public final class V3ServicePlanMapper { - - // CF v3 visibility_type value that maps to the domain's isPublic flag (see RawCloudServicePlan#derive). + private static final String PUBLIC_VISIBILITY = "public"; private V3ServicePlanMapper() { @@ -40,9 +35,10 @@ private static String extractBrokerCatalogId(V3ServicePlan.V3BrokerCatalog broke private static String extractServiceOfferingId(V3ServicePlan.V3Relationships relationships) { if (relationships == null || relationships.serviceOffering() == null || relationships.serviceOffering() - .data() == null) { + .data() == null) { return null; } + return relationships.serviceOffering() .data() .guid(); diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Space.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Space.java index 4e7c0d28c8..70e8c3f4bd 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Space.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3Space.java @@ -20,6 +20,7 @@ public String organizationGuid() { .data() == null) { return null; } + return relationships.organization() .data() .guid(); diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3StackMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3StackMapper.java index 9d4c7c84d9..c821ec9104 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3StackMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3StackMapper.java @@ -3,10 +3,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudStack; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudStack; -/** - * Maps the {@link V3Stack} wire model to the project's {@link CloudStack} domain object. Mirrors the OSS - * {@code RawCloudStack} adapter field-for-field, so both client implementations yield identical domain objects. - */ public final class V3StackMapper { private V3StackMapper() { diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3TaskMapper.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3TaskMapper.java index 1341ea722e..bde50c807e 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3TaskMapper.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/resources/V3TaskMapper.java @@ -3,10 +3,6 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudTask; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudTask; -/** - * Maps the {@link V3Task} wire model to the project's {@link CloudTask} domain object. Mirrors the OSS {@code RawCloudTask} adapter - * field-for-field, so both client implementations yield identical domain objects. - */ public final class V3TaskMapper { private V3TaskMapper() { @@ -27,6 +23,7 @@ private static CloudTask.Result parseResult(V3Task task) { if (task.result() == null) { return null; } + return ImmutableCloudTask.ImmutableResult.of(task.result() .failureReason()); } diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientIntegrationTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientIntegrationTest.java index 3b0e958345..9c7ea635fe 100644 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientIntegrationTest.java +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientIntegrationTest.java @@ -12,6 +12,8 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.Lifecycle; import org.cloudfoundry.multiapps.controller.client.facade.domain.LifecycleType; import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; +import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; +import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudControllerRestClientFactory; import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudSpaceClient; import org.cloudfoundry.multiapps.controller.client.facade.rest.ImmutableCloudControllerRestClientFactory; import org.cloudfoundry.multiapps.controller.client.facade.util.RestUtil; @@ -37,10 +39,10 @@ static void login() throws MalformedURLException { CloudCredentials credentials = getCloudCredentials(); URL apiUrl = URI.create(ITVariable.CF_API.getValue()) .toURL(); - var clientFactory = ImmutableCloudControllerRestClientFactory.builder() - .shouldTrustSelfSignedCertificates(true) - .build(); - var oauthClient = new RestUtil().createOAuthClientByControllerUrl(apiUrl, true); + CloudControllerRestClientFactory clientFactory = ImmutableCloudControllerRestClientFactory.builder() + .shouldTrustSelfSignedCertificates(true) + .build(); + OAuthClient oauthClient = new RestUtil().createOAuthClientByControllerUrl(apiUrl, true); oauthClient.init(credentials); CloudSpaceClient spaceClient = clientFactory.createSpaceClient(apiUrl, oauthClient, Collections.emptyMap()); target = spaceClient.getSpace(ITVariable.ORG.getValue(), ITVariable.SPACE.getValue()); @@ -66,7 +68,7 @@ protected Lifecycle createLifecycle(Staging staging) { .build(); } - var data = new HashMap(); + HashMap data = new HashMap(); data.put("buildpacks", staging.getBuildpacks()); data.put("stack", DEFAULT_STACK); diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3ClientPageUriTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3ClientPageUriTest.java index c5e4eb67a8..a9a385f183 100644 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3ClientPageUriTest.java +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerV3ClientPageUriTest.java @@ -6,7 +6,7 @@ class CloudControllerV3ClientPageUriTest { @Test - void testAddsPageWhenAbsentAndKeepsExistingQuery() { + void testPageUriAddsPageWhenAbsentAndKeepsExistingQuery() { String uri = CloudControllerV3Client.pageUri("/v3/apps?per_page=5000&space_guids=abc", 3); Assertions.assertTrue(uri.contains("page=3"), uri); @@ -15,17 +15,16 @@ void testAddsPageWhenAbsentAndKeepsExistingQuery() { } @Test - void testReplacesExistingPageParam() { + void testPageUriReplacesExistingPageParam() { String uri = CloudControllerV3Client.pageUri("/v3/apps?per_page=5000&page=1", 4); Assertions.assertTrue(uri.contains("page=4"), uri); Assertions.assertFalse(uri.contains("page=1"), uri); - // per_page must survive the replacement. Assertions.assertTrue(uri.contains("per_page=5000"), uri); } @Test - void testPreservesMultipleFilters() { + void testPageUriPreservesMultipleFilters() { String first = "/v3/service_credential_bindings?per_page=5000&type=key&service_instance_guids=g1&names=n1"; String uri = CloudControllerV3Client.pageUri(first, 2); diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOutTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOutTest.java index 7cdc626d5c..752a9e8c3f 100644 --- a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOutTest.java +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/rest/ReactiveFanOutTest.java @@ -14,24 +14,23 @@ class ReactiveFanOutTest { @Test - void testEmptyReturnsEmpty() { + void testMapConcurrentlyReturnsEmptyWhenEmpty() { Assertions.assertEquals(List.of(), ReactiveFanOut.mapConcurrently(List.of(), x -> x)); } @Test - void testSingleElementShortCircuits() { + void testMapConcurrentlyShortCircuitsWhenSingleElement() { List result = ReactiveFanOut.mapConcurrently(List.of("a"), s -> s + "!"); Assertions.assertEquals(List.of("a!"), result); } @Test - void testResultsPreserveInputOrderEvenWhenTasksFinishOutOfOrder() { + void testMapConcurrentlyResultPreserveInputOrderEvenWhenTasksFinishOutOfOrder() { List input = IntStream.rangeClosed(1, 20) .boxed() .toList(); - // Later items finish sooner (inverse sleep) — the result must still be in input order. List result = ReactiveFanOut.mapConcurrently(input, i -> { sleep((20 - i)); return i * 10; @@ -45,7 +44,7 @@ void testResultsPreserveInputOrderEvenWhenTasksFinishOutOfOrder() { @Test @Timeout(10) - void testWorkRunsConcurrently() throws Exception { + void testMapConcurrentlyWorkRunsConcurrently() throws Exception { int n = 8; // If the mapper ran sequentially, the barrier (which needs all n parties) would never trip and the test would time out. CyclicBarrier barrier = new CyclicBarrier(n); diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/jobs/OrphanedDataCleaner.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/jobs/OrphanedDataCleaner.java index 73cd64bc99..9b8fbabb61 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/jobs/OrphanedDataCleaner.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/jobs/OrphanedDataCleaner.java @@ -8,8 +8,10 @@ import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; -import org.cloudfoundry.multiapps.controller.client.facade.rest.ImmutableCloudControllerRestClientFactory; +import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; +import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudControllerRestClientFactory; import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudSpaceClient; +import org.cloudfoundry.multiapps.controller.client.facade.rest.ImmutableCloudControllerRestClientFactory; import org.cloudfoundry.multiapps.controller.core.auditlogging.MtaConfigurationPurgerAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.OAuthClientFactory; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; @@ -97,13 +99,13 @@ protected void initSpaceClient() { CloudCredentials cloudCredentials = new CloudCredentials(configuration.getGlobalAuditorUser(), configuration.getGlobalAuditorPassword(), SecurityUtil.CLIENT_ID, SecurityUtil.CLIENT_SECRET, configuration.getGlobalAuditorOrigin()); - var clientFactory = ImmutableCloudControllerRestClientFactory.builder() - .connectTimeout(Duration.ofMinutes(5)) - .responseTimeout(Duration.ofMinutes(5)) - .connectionPoolSize(1) - .threadPoolSize(1) - .build(); - var oauthClient = oAuthClientFactory.createOAuthClient(); + CloudControllerRestClientFactory clientFactory = ImmutableCloudControllerRestClientFactory.builder() + .connectTimeout(Duration.ofMinutes(5)) + .responseTimeout(Duration.ofMinutes(5)) + .connectionPoolSize(1) + .threadPoolSize(1) + .build(); + OAuthClient oauthClient = oAuthClientFactory.createOAuthClient(); oauthClient.init(cloudCredentials); spaceClient = clientFactory.createSpaceClient(configuration.getControllerUrl(), oauthClient, Collections.emptyMap()); } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceCreateUpdateServiceActionsStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceCreateUpdateServiceActionsStep.java index 818e6de5fc..bcb90f5197 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceCreateUpdateServiceActionsStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DetermineServiceCreateUpdateServiceActionsStep.java @@ -16,13 +16,13 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; -import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.common.util.JsonUtil; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Metadata; import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceOperation; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudServiceInstanceExtended;