diff --git a/multiapps-controller-client/pom.xml b/multiapps-controller-client/pom.xml
index 46b5c774a1..d118963a47 100644
--- a/multiapps-controller-client/pom.xml
+++ b/multiapps-controller-client/pom.xml
@@ -1,5 +1,5 @@
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
multiapps-controller-client
@@ -22,17 +22,13 @@
log4j-core
- org.apache.logging.log4j
- log4j-api
+ org.apache.logging.log4j
+ log4j-api
org.slf4j
slf4j-api
-
- com.sap.cloud.lm.sl
- cloudfoundry-client-facade
-
org.cloudfoundry.multiapps
multiapps-common
@@ -44,18 +40,129 @@
org.springframework.security
spring-security-config
- ${spring-security.version}
org.springframework.security
spring-security-web
- ${spring-security.version}
+
+
+
+ org.springframework
+ spring-webflux
+
+
+
+ org.springframework
+ spring-jcl
+
+
org.springframework.security
spring-security-oauth2-client
- ${spring-security.version}
+
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+ commons-io
+ commons-io
+
+
+
+ org.immutables
+ value
+ provided
+
+
+
+ org.cloudfoundry
+ cloudfoundry-client-reactor
+
+
+
+
+ io.projectreactor.netty
+ reactor-netty
+
+
+
+
+ io.micrometer
+ micrometer-core
+ ${micrometer.version}
+
+
+
+ integration
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.3
+
+
+ **/*
+
+
+
+
+ maven-failsafe-plugin
+ 3.2.5
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ **/*IntegrationTest
+
+
+
+
+
+
+
+ org.cloudfoundry
+ cloudfoundry-client-reactor
+ ${cloudfoundry-client.version}
+
+
+
+ org.slf4j
+ jcl-over-slf4j
+
+
+
+
+
+ commons-logging
+ commons-logging
+ ${commons-logging.version}
+
+
+
+
\ No newline at end of file
diff --git a/multiapps-controller-client/src/main/java/module-info.java b/multiapps-controller-client/src/main/java/module-info.java
index f4beda24b9..4201bc0a0b 100644
--- a/multiapps-controller-client/src/main/java/module-info.java
+++ b/multiapps-controller-client/src/main/java/module-info.java
@@ -4,18 +4,32 @@
exports org.cloudfoundry.multiapps.controller.client.lib.domain;
exports org.cloudfoundry.multiapps.controller.client.uaa;
exports org.cloudfoundry.multiapps.controller.client.util;
+ exports org.cloudfoundry.multiapps.controller.client.facade;
+ exports org.cloudfoundry.multiapps.controller.client.facade.rest;
+ exports org.cloudfoundry.multiapps.controller.client.facade.oauth2;
+ exports org.cloudfoundry.multiapps.controller.client.facade.domain;
+ exports org.cloudfoundry.multiapps.controller.client.facade.adapters;
+ exports org.cloudfoundry.multiapps.controller.client.facade.util;
+ exports org.cloudfoundry.multiapps.controller.client.facade.dto;
requires transitive org.cloudfoundry.client;
- requires transitive com.sap.cloudfoundry.client.facade;
requires spring.security.oauth2.core;
requires transitive spring.web;
+ requires com.fasterxml.jackson.databind;
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;
requires spring.webflux;
requires reactor.core;
+ requires reactor.netty.core;
+ requires reactor.netty.http;
requires org.reactivestreams;
requires io.netty.handler;
requires io.netty.transport;
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/CloudFoundryTokenProvider.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/CloudFoundryTokenProvider.java
index e126350e74..2f50664905 100644
--- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/CloudFoundryTokenProvider.java
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/CloudFoundryTokenProvider.java
@@ -1,7 +1,7 @@
package org.cloudfoundry.multiapps.controller.client;
-import com.sap.cloudfoundry.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo;
-import com.sap.cloudfoundry.client.facade.oauth2.OAuthClient;
+import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo;
+import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient;
public class CloudFoundryTokenProvider implements TokenProvider {
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 1c92c5522e..51023de044 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
@@ -11,40 +11,39 @@
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;
+import org.cloudfoundry.multiapps.controller.client.facade.ServiceBindingOperationCallback;
+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.rest.CloudControllerRestClient;
import org.cloudfoundry.multiapps.controller.client.util.ResilientCloudOperationExecutor;
import org.springframework.http.HttpStatus;
-import com.sap.cloudfoundry.client.facade.ApplicationServicesUpdateCallback;
-import com.sap.cloudfoundry.client.facade.CloudControllerClient;
-import com.sap.cloudfoundry.client.facade.CloudControllerClientImpl;
-import com.sap.cloudfoundry.client.facade.ServiceBindingOperationCallback;
-import com.sap.cloudfoundry.client.facade.UploadStatusCallback;
-import com.sap.cloudfoundry.client.facade.domain.CloudApplication;
-import com.sap.cloudfoundry.client.facade.domain.CloudAsyncJob;
-import com.sap.cloudfoundry.client.facade.domain.CloudBuild;
-import com.sap.cloudfoundry.client.facade.domain.CloudDomain;
-import com.sap.cloudfoundry.client.facade.domain.CloudEvent;
-import com.sap.cloudfoundry.client.facade.domain.CloudPackage;
-import com.sap.cloudfoundry.client.facade.domain.CloudProcess;
-import com.sap.cloudfoundry.client.facade.domain.CloudRoute;
-import com.sap.cloudfoundry.client.facade.domain.CloudServiceBinding;
-import com.sap.cloudfoundry.client.facade.domain.CloudServiceBroker;
-import com.sap.cloudfoundry.client.facade.domain.CloudServiceInstance;
-import com.sap.cloudfoundry.client.facade.domain.CloudServiceKey;
-import com.sap.cloudfoundry.client.facade.domain.CloudServiceOffering;
-import com.sap.cloudfoundry.client.facade.domain.CloudSpace;
-import com.sap.cloudfoundry.client.facade.domain.CloudStack;
-import com.sap.cloudfoundry.client.facade.domain.CloudTask;
-import com.sap.cloudfoundry.client.facade.domain.DockerInfo;
-import com.sap.cloudfoundry.client.facade.domain.DropletInfo;
-import com.sap.cloudfoundry.client.facade.domain.InstancesInfo;
-import com.sap.cloudfoundry.client.facade.domain.ServicePlanVisibility;
-import com.sap.cloudfoundry.client.facade.domain.Staging;
-import com.sap.cloudfoundry.client.facade.domain.Upload;
-import com.sap.cloudfoundry.client.facade.domain.UserRole;
-import com.sap.cloudfoundry.client.facade.dto.ApplicationToCreateDto;
-import com.sap.cloudfoundry.client.facade.rest.CloudControllerRestClient;
-
public class ResilientCloudControllerClient implements CloudControllerClient {
private final CloudControllerClientImpl delegate;
@@ -400,11 +399,11 @@ public void updateServiceSyslogDrainUrl(String serviceName, String syslogDrainUr
public CloudPackage asyncUploadApplicationWithExponentialBackoff(String applicationName, Path file, UploadStatusCallback callback,
Duration overrideTimeout) {
if (overrideTimeout != null) {
- return executeWithRetry(() -> delegate.asyncUploadApplicationWithExponentialBackoff(applicationName, file, callback,
- overrideTimeout));
+ return executeWithRetry(
+ () -> delegate.asyncUploadApplicationWithExponentialBackoff(applicationName, file, callback, overrideTimeout));
}
- return executeWithExponentialBackoff(timeout -> delegate.asyncUploadApplicationWithExponentialBackoff(applicationName, file,
- callback, timeout));
+ return executeWithExponentialBackoff(
+ timeout -> delegate.asyncUploadApplicationWithExponentialBackoff(applicationName, file, callback, timeout));
}
@Override
@@ -550,8 +549,8 @@ public List getBuildsForApplication(UUID applicationGuid) {
@Override
public Optional unbindServiceInstance(String applicationName, String serviceInstanceName,
ApplicationServicesUpdateCallback applicationServicesUpdateCallback) {
- return executeWithRetry(() -> delegate.unbindServiceInstance(applicationName, serviceInstanceName,
- applicationServicesUpdateCallback));
+ return executeWithRetry(
+ () -> delegate.unbindServiceInstance(applicationName, serviceInstanceName, applicationServicesUpdateCallback));
}
@Override
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/TokenProvider.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/TokenProvider.java
index 4c62cef357..41f79f3aee 100644
--- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/TokenProvider.java
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/TokenProvider.java
@@ -1,6 +1,6 @@
package org.cloudfoundry.multiapps.controller.client;
-import com.sap.cloudfoundry.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo;
+import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo;
public interface TokenProvider {
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/ApplicationLogListener.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/ApplicationLogListener.java
new file mode 100644
index 0000000000..2fa158f172
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/ApplicationLogListener.java
@@ -0,0 +1,13 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+import org.cloudfoundry.multiapps.controller.client.facade.domain.ApplicationLog;
+
+public interface ApplicationLogListener {
+
+ void onComplete();
+
+ void onError(Throwable exception);
+
+ void onMessage(ApplicationLog log);
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/ApplicationServicesUpdateCallback.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/ApplicationServicesUpdateCallback.java
new file mode 100644
index 0000000000..f1e1bb6569
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/ApplicationServicesUpdateCallback.java
@@ -0,0 +1,6 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+public interface ApplicationServicesUpdateCallback {
+
+ void onError(CloudOperationException e, String applicationName, String serviceInstanceName);
+}
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
new file mode 100644
index 0000000000..38248b40a0
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClient.java
@@ -0,0 +1,734 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+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.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;
+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;
+
+/**
+ * The interface defining operations making up the Cloud Foundry Java client's API.
+ *
+ */
+public interface CloudControllerClient {
+
+ CloudSpace getTarget();
+
+ /**
+ * Add a private domain in the current organization.
+ *
+ * @param domainName the domain to add
+ */
+ void addDomain(String domainName);
+
+ /**
+ * Register a new route to the a domain.
+ *
+ * @param host the host of the route to register
+ * @param domainName the domain of the route to register
+ */
+ void addRoute(String host, String domainName, String path);
+
+ /**
+ * Associate (provision) a service with an application.
+ *
+ * @param bindingName the binding name
+ * @param applicationName the application name
+ * @param serviceInstanceName the service instance name
+ * @return job id for async polling if present
+ */
+ Optional bindServiceInstance(String bindingName, String applicationName, String serviceInstanceName);
+
+ /**
+ * Associate (provision) a service with an application.
+ *
+ * @param bindingName the binding name
+ * @param applicationName the application name
+ * @param serviceInstanceName the service instance name
+ * @param parameters the binding parameters
+ * @param updateServicesCallback callback used for error handling
+ * @return job id for async polling if present
+ */
+ Optional bindServiceInstance(String bindingName, String applicationName, String serviceInstanceName,
+ Map parameters, ApplicationServicesUpdateCallback updateServicesCallback);
+
+ /**
+ * Create application
+ *
+ * @param applicationToCreateDto the application's parameters used for its creation
+ */
+ void createApplication(ApplicationToCreateDto applicationToCreateDto);
+
+ /**
+ * Create a service instance.
+ *
+ * @param serviceInstance cloud service instance info
+ */
+ void createServiceInstance(CloudServiceInstance serviceInstance);
+
+ /**
+ * Create a service broker.
+ *
+ * @param serviceBroker cloud service broker info
+ * @return job id for async poll
+ */
+ String createServiceBroker(CloudServiceBroker serviceBroker);
+
+ /**
+ *
+ * @param keyModel service-key cloud object
+ * @param serviceInstanceName name of related service instance
+ * @return the service-key object populated with new guid
+ */
+ CloudServiceKey createAndFetchServiceKey(CloudServiceKey keyModel, String serviceInstanceName);
+
+ /**
+ *
+ * @param keyModel service-key cloud object
+ * @param serviceInstanceName name of related service instance
+ * @return job id for async polling if present
+ */
+ Optional createServiceKey(CloudServiceKey keyModel, String serviceInstanceName);
+
+ /**
+ * Create a service key.
+ *
+ * @param serviceInstanceName name of service instance
+ * @param serviceKeyName name of service-key
+ * @param parameters parameters of service-key
+ * @return job id for async polling if present
+ */
+ Optional createServiceKey(String serviceInstanceName, String serviceKeyName, Map parameters);
+
+ /**
+ * Create a user-provided service instance.
+ *
+ * @param serviceInstance cloud service instance info
+ */
+ void createUserProvidedServiceInstance(CloudServiceInstance serviceInstance);
+
+ /**
+ * Delete application.
+ *
+ * @param applicationName name of application
+ */
+ void deleteApplication(String applicationName);
+
+ /**
+ * Delete a private domain in the current organization.
+ *
+ * @param domainName the domain to delete
+ */
+ void deleteDomain(String domainName);
+
+ /**
+ * Delete routes that do not have any application which is assigned to them.
+ */
+ void deleteOrphanedRoutes();
+
+ /**
+ * Delete a registered route from the space of the current session.
+ *
+ * @param host the host of the route to delete
+ * @param domainName the domain of the route to delete
+ */
+ void deleteRoute(String host, String domainName, String path);
+
+ /**
+ * Delete cloud service instance.
+ *
+ * @param serviceInstance name of service instance
+ */
+ void deleteServiceInstance(String serviceInstance);
+
+ /**
+ *
+ * @param serviceInstance {@link CloudServiceInstance}
+ */
+ void deleteServiceInstance(CloudServiceInstance serviceInstance);
+
+ /**
+ * Delete a service broker.
+ *
+ * @param name the service broker name
+ * @return async job id
+ */
+ String deleteServiceBroker(String name);
+
+ /**
+ * Get a service binding
+ *
+ * @param serviceBindingId the id of the service binding
+ * @return the service binding
+ */
+ CloudServiceBinding getServiceBinding(UUID serviceBindingId);
+
+ /**
+ * Delete a service binding.
+ *
+ * @param serviceInstanceName name of service instance
+ * @param serviceKeyName name of service key
+ * @return job id for async polling if present
+ */
+ Optional deleteServiceBinding(String serviceInstanceName, String serviceKeyName);
+
+ /**
+ * Delete a service binding.
+ *
+ * @param bindingGuid The GUID of the binding
+ * @param serviceBindingOperationCallback callback used for error handling
+ * @return job id for async polling if present
+ */
+ Optional deleteServiceBinding(UUID bindingGuid, ServiceBindingOperationCallback serviceBindingOperationCallback);
+
+ /**
+ * Delete a service binding.
+ *
+ * @param bindingGuid The GUID of the binding
+ * @return job id for async polling if present
+ */
+ Optional deleteServiceBinding(UUID bindingGuid);
+
+ /**
+ * Get cloud application with the specified name.
+ *
+ * @param applicationName name of the app
+ * @return the cloud application
+ */
+ CloudApplication getApplication(String applicationName);
+
+ /**
+ * Get cloud application with the specified name.
+ *
+ * @param applicationName name of the app
+ * @param required if true, and organization is not found, throw an exception
+ * @return the cloud application
+ */
+ CloudApplication getApplication(String applicationName, boolean required);
+
+ /**
+ * Get the GUID of the cloud application with the specified name.
+ *
+ * @param applicationName name of the app
+ * @return the cloud application's guid
+ */
+ UUID getApplicationGuid(String applicationName);
+
+ String getApplicationName(UUID applicationGuid);
+
+ /**
+ * Get application environment variables for the app with the specified name.
+ *
+ * @param applicationName name of the app
+ * @return the cloud application environment variables
+ */
+ Map getApplicationEnvironment(String applicationName);
+
+ /**
+ * Get application environment variables for the app with the specified GUID.
+ *
+ * @param applicationGuid GUID of the app
+ * @return the cloud application environment variables
+ */
+ Map getApplicationEnvironment(UUID applicationGuid);
+
+ /**
+ * Get application events.
+ *
+ * @param applicationName name of application
+ * @return application events
+ */
+ List getApplicationEvents(String applicationName);
+
+ List getEventsByActee(UUID uuid);
+
+ /**
+ * Get application instances info for application.
+ *
+ * @param app the application.
+ * @return instances info
+ */
+ InstancesInfo getApplicationInstances(CloudApplication app);
+
+ InstancesInfo getApplicationInstances(UUID applicationGuid);
+
+ CloudProcess getApplicationProcess(UUID applicationGuid);
+
+ List getApplicationRoutes(UUID applicationGuid);
+
+ boolean getApplicationSshEnabled(UUID applicationGuid);
+
+ /**
+ * Get all applications in the currently targeted space. This method has EXTREMELY poor performance for spaces with a lot of
+ * applications.
+ *
+ * @return list of applications
+ */
+ List getApplications();
+
+ /**
+ * Gets the default domain for the current org, which is the first shared domain.
+ *
+ * @return the default domain
+ */
+ CloudDomain getDefaultDomain();
+
+ /**
+ * Get list of all domain shared and private domains.
+ *
+ * @return list of domains
+ */
+ List getDomains();
+
+ /**
+ * Get list of all domain registered for the current organization.
+ *
+ * @return list of domains
+ */
+ List getDomainsForOrganization();
+
+ /**
+ * Get system events.
+ *
+ * @return all system events
+ */
+ List getEvents();
+
+ /**
+ * Get list of all private domains.
+ *
+ * @return list of private domains
+ */
+ List getPrivateDomains();
+
+ /**
+ * Get the info for all routes for a domain.
+ *
+ * @param domainName the domain the routes belong to
+ * @return list of routes
+ */
+ List getRoutes(String domainName);
+
+ /**
+ * Get a service broker.
+ *
+ * @param name the service broker name
+ * @return the service broker
+ */
+ CloudServiceBroker getServiceBroker(String name);
+
+ /**
+ * Get a service broker.
+ *
+ * @param name the service broker name
+ * @param required if true, and organization is not found, throw an exception
+ * @return the service broker
+ */
+ CloudServiceBroker getServiceBroker(String name, boolean required);
+
+ /**
+ * Get all service brokers.
+ *
+ * @return the service brokers
+ */
+ List getServiceBrokers();
+
+ /**
+ * Get the GUID of a service instance.
+ *
+ * @param serviceInstanceName the name of the service instance
+ * @return the service instance GUID
+ */
+ UUID getRequiredServiceInstanceGuid(String serviceInstanceName);
+
+ /**
+ * Get a service instance.
+ *
+ * @param serviceInstanceName name of the service instance
+ * @return the service instance info
+ */
+ CloudServiceInstance getServiceInstance(String serviceInstanceName);
+
+ /**
+ * Get a service instance.
+ *
+ * @param serviceInstanceName name of the service instance
+ * @param required if true, and service instance is not found, throw an exception
+ * @return the service instance info
+ */
+ CloudServiceInstance getServiceInstance(String serviceInstanceName, boolean required);
+
+ /**
+ * Get a service instance name.
+ *
+ * @param serviceInstanceGuid GUID of the service instance
+ * @return the service instance name
+ */
+ String getServiceInstanceName(UUID serviceInstanceGuid);
+
+ /**
+ * Get a service instance.
+ *
+ * @param serviceInstanceName name of the service instance
+ * @return the service instance info
+ */
+ CloudServiceInstance getServiceInstanceWithoutAuxiliaryContent(String serviceInstanceName);
+
+ /**
+ * Get a service instance.
+ *
+ * @param serviceInstanceName name of the service instance
+ * @param required if true, and service instance is not found, throw an exception
+ * @return the service instance info
+ */
+ CloudServiceInstance getServiceInstanceWithoutAuxiliaryContent(String serviceInstanceName, boolean required);
+
+ /**
+ * Get the bindings for a particular service instance.
+ *
+ * @param serviceInstanceGuid the GUID of the service instance
+ * @return the bindings
+ */
+ List getServiceAppBindings(UUID serviceInstanceGuid);
+
+ /**
+ * Get the bindings for a particular application.
+ *
+ * @param applicationGuid the GUID of the application
+ * @return the bindings
+ */
+ List getAppBindings(UUID applicationGuid);
+
+ /**
+ * Get the binding between an application and a service instance.
+ *
+ * @param applicationId the GUID of the application
+ * @param serviceInstanceGuid the GUID of the service instance
+ * @return the binding
+ */
+ CloudServiceBinding getServiceBindingForApplication(UUID applicationId, UUID serviceInstanceGuid);
+
+ /**
+ * Get all service instance parameters.
+ *
+ * @param guid The service instance guid
+ * @return service instance parameters in key-value pairs
+ */
+ Map getServiceInstanceParameters(UUID guid);
+
+ /**
+ * Get all user-provided service instance parameters
+ *
+ * @param guid The service instance guid
+ * @return user-provided service instance parameters in key-value pairs
+ */
+ Map getUserProvidedServiceInstanceParameters(UUID guid);
+
+ /**
+ * Get all service binding parameters.
+ *
+ * @param guid The service binding guid
+ * @return service binding parameters in key-value pairs
+ */
+ Map getServiceBindingParameters(UUID guid);
+
+ /**
+ * Get a service key.
+ *
+ * @param serviceInstanceName The service instance name
+ * @param serviceKeyName The service key name
+ * @return the service key info
+ */
+ CloudServiceKey getServiceKey(String serviceInstanceName, String serviceKeyName);
+
+ /**
+ * Get service keys for a service instance.
+ *
+ * @param serviceInstanceName name containing service keys
+ * @return the service keys info
+ */
+ List getServiceKeys(String serviceInstanceName);
+
+ List getServiceKeysWithCredentials(String serviceInstanceName);
+
+ /**
+ * Get service keys for a service instance.
+ *
+ * @param serviceInstance instance containing service keys
+ * @return the service keys info
+ */
+ List getServiceKeys(CloudServiceInstance serviceInstance);
+
+ List getServiceKeysWithCredentials(CloudServiceInstance serviceInstance);
+
+ /**
+ * Get all service offerings.
+ *
+ * @return list of service offerings
+ */
+ List getServiceOfferings();
+
+ /**
+ * Get list of all shared domains.
+ *
+ * @return list of shared domains
+ */
+ List getSharedDomains();
+
+ /**
+ * Get a stack by name.
+ *
+ * @param name the name of the stack to get
+ * @return the stack
+ */
+ CloudStack getStack(String name);
+
+ /**
+ * Get a stack by name.
+ *
+ * @param name the name of the stack to get
+ * @param required if true, and organization is not found, throw an exception
+ * @return the stack, or null if not found
+ */
+ CloudStack getStack(String name, boolean required);
+
+ /**
+ * Get the list of stacks available for staging applications.
+ *
+ * @return the list of available stacks
+ */
+ List getStacks();
+
+ /**
+ * Rename an application.
+ *
+ * @param applicationName the current name
+ * @param newName the new name
+ */
+ void rename(String applicationName, String newName);
+
+ /**
+ * Restart application.
+ *
+ * @param applicationName name of application
+ */
+ void restartApplication(String applicationName);
+
+ /**
+ * Start application. May return starting info if the response obtained after the start request contains headers . If the response does
+ * not contain headers, null is returned instead.
+ *
+ * @param applicationName name of application
+ */
+ void startApplication(String applicationName);
+
+ /**
+ * Stop application.
+ *
+ * @param applicationName name of application
+ */
+ void stopApplication(String applicationName);
+
+ /**
+ * Un-associate (unprovision) a service from an application.
+ *
+ * @param applicationName the application name
+ * @param serviceInstanceName the service instance name
+ * @param applicationServicesUpdateCallback callback used for error handling
+ * @return job id for async polling if present
+ */
+ Optional unbindServiceInstance(String applicationName, String serviceInstanceName,
+ ApplicationServicesUpdateCallback applicationServicesUpdateCallback);
+
+ /**
+ * Un-associate (unprovision) a service from an application.
+ *
+ * @param applicationName the application name
+ * @param serviceInstanceName the service instance name
+ * @return job id for async polling if present
+ */
+ Optional unbindServiceInstance(String applicationName, String serviceInstanceName);
+
+ /**
+ * Un-associate (unprovision) a service from an application.
+ *
+ * @param applicationGuid the application guid
+ * @param serviceInstanceGuid the service instance guid
+ * @return job id for async polling if present
+ */
+ Optional unbindServiceInstance(UUID applicationGuid, UUID serviceInstanceGuid);
+
+ /**
+ * Update application disk quota.
+ *
+ * @param applicationName name of application
+ * @param disk new disk setting in MB
+ */
+ void updateApplicationDiskQuota(String applicationName, int disk);
+
+ /**
+ * Update application env using a map where the key specifies the name of the environment variable and the value the value of the
+ * environment variable..
+ *
+ * @param applicationName name of application
+ * @param env map of environment settings
+ */
+ void updateApplicationEnv(String applicationName, Map env);
+
+ /**
+ * Update application instances.
+ *
+ * @param applicationName name of application
+ * @param instances number of instances to use
+ */
+ void updateApplicationInstances(String applicationName, int instances);
+
+ /**
+ * Update application memory.
+ *
+ * @param applicationName name of application
+ * @param memory new memory setting in MB
+ */
+ void updateApplicationMemory(String applicationName, int memory);
+
+ /**
+ * Update application staging information.
+ *
+ * @param applicationName name of appplication
+ * @param staging staging information for the app
+ */
+ void updateApplicationStaging(String applicationName, Staging staging);
+
+ /**
+ * Update application Routes.
+ *
+ * @param applicationName name of application
+ * @param routes list of route summary info for the routes the app should use
+ */
+ void updateApplicationRoutes(String applicationName, Set routes);
+
+ /**
+ * Update a service broker (unchanged forces catalog refresh).
+ *
+ * @param serviceBroker cloud service broker info
+ * @return async job id
+ */
+ String updateServiceBroker(CloudServiceBroker serviceBroker);
+
+ /**
+ * Service plans are private by default when a service broker's catalog is fetched/updated. This method will update the visibility of
+ * all plans for a broker to either public or private.
+ *
+ * @param name the service broker name
+ * @param visibility true for public, false for private
+ */
+ void updateServicePlanVisibilityForBroker(String name, ServicePlanVisibility visibility);
+
+ void updateServicePlan(String serviceName, String planName);
+
+ void updateServiceParameters(String serviceName, Map parameters);
+
+ void updateServiceTags(String serviceName, List tags);
+
+ void updateServiceSyslogDrainUrl(String serviceName, String syslogDrainUrl);
+
+ CloudPackage asyncUploadApplicationWithExponentialBackoff(String applicationName, Path file, UploadStatusCallback callback,
+ Duration overrideTimeout);
+
+ Upload getUploadStatus(UUID packageGuid);
+
+ CloudTask getTask(UUID taskGuid);
+
+ /**
+ * Get the list of one-off tasks currently known for the given application.
+ *
+ * @param applicationName the application to look for tasks
+ * @return the list of known tasks
+ * @throws UnsupportedOperationException if the targeted controller does not support tasks
+ */
+ List getTasks(String applicationName);
+
+ /**
+ * Run a one-off task on an application.
+ *
+ * @param applicationName the application to run the task on
+ * @param task the task to run
+ * @return the ran task
+ * @throws UnsupportedOperationException if the targeted controller does not support tasks
+ */
+ CloudTask runTask(String applicationName, CloudTask task);
+
+ /**
+ * Cancel the given task.
+ *
+ * @param taskGuid the GUID of the task to cancel
+ * @return the cancelled task
+ */
+ CloudTask cancelTask(UUID taskGuid);
+
+ CloudBuild createBuild(UUID packageGuid);
+
+ CloudBuild getBuild(UUID buildGuid);
+
+ void bindDropletToApp(UUID dropletGuid, UUID applicationGuid);
+
+ List getBuildsForApplication(UUID applicationGuid);
+
+ List getBuildsForPackage(UUID packageGuid);
+
+ List getApplicationsByMetadataLabelSelector(String labelSelector);
+
+ void updateApplicationMetadata(UUID guid, Metadata metadata);
+
+ void updateServiceInstanceMetadata(UUID guid, Metadata metadata);
+
+ void updateServiceBindingMetadata(UUID guid, Metadata metadata);
+
+ List getServiceInstancesWithoutAuxiliaryContentByNames(List names);
+
+ List getServiceInstancesByMetadataLabelSelector(String labelSelector);
+
+ List getServiceInstancesWithoutAuxiliaryContentByMetadataLabelSelector(String labelSelector);
+
+ DropletInfo getCurrentDropletForApplication(UUID applicationGuid);
+
+ CloudPackage getPackage(UUID packageGuid);
+
+ List getPackagesForApplication(UUID applicationGuid);
+
+ Set getUserRolesBySpaceAndUser(UUID spaceGuid, UUID userGuid);
+
+ CloudPackage createDockerPackage(UUID applicationGuid, DockerInfo dockerInfo);
+
+ CloudAsyncJob getAsyncJob(String jobId);
+
+}
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
new file mode 100644
index 0000000000..648d07210d
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerClientImpl.java
@@ -0,0 +1,669 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+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 java.util.function.Supplier;
+
+import org.cloudfoundry.AbstractCloudFoundryException;
+import org.cloudfoundry.client.v3.Metadata;
+import org.springframework.http.HttpStatus;
+import org.springframework.util.Assert;
+
+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.rest.CloudControllerRestClient;
+import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudControllerRestClientFactory;
+import org.cloudfoundry.multiapps.controller.client.facade.rest.ImmutableCloudControllerRestClientFactory;
+
+/**
+ * A Java client to exercise the Cloud Foundry API.
+ *
+ */
+public class CloudControllerClientImpl implements CloudControllerClient {
+
+ private CloudControllerRestClient delegate;
+
+ /**
+ * Construct client without a default organization and space.
+ */
+ public CloudControllerClientImpl(URL controllerUrl, CloudCredentials credentials) {
+ this(controllerUrl, credentials, null, false);
+ }
+
+ public CloudControllerClientImpl(URL controllerUrl, CloudCredentials credentials, CloudSpace target, boolean trustSelfSignedCerts) {
+ Assert.notNull(controllerUrl, "URL for cloud controller cannot be null");
+ CloudControllerRestClientFactory restClientFactory = ImmutableCloudControllerRestClientFactory.builder()
+ .shouldTrustSelfSignedCertificates(trustSelfSignedCerts)
+ .build();
+ this.delegate = restClientFactory.createClient(controllerUrl, credentials, target);
+ }
+
+ /**
+ * Construct a client with a pre-configured CloudControllerClient
+ */
+ public CloudControllerClientImpl(CloudControllerRestClient delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public CloudSpace getTarget() {
+ return delegate.getTarget();
+ }
+
+ @Override
+ public void addDomain(String domainName) {
+ handleExceptions(() -> delegate.addDomain(domainName));
+ }
+
+ @Override
+ public void addRoute(String host, String domainName, String path) {
+ handleExceptions(() -> delegate.addRoute(host, domainName, path));
+ }
+
+ @Override
+ public Optional bindServiceInstance(String bindingName, String applicationName, String serviceInstanceName) {
+ return handleExceptions(() -> delegate.bindServiceInstance(bindingName, applicationName, serviceInstanceName));
+ }
+
+ @Override
+ public Optional bindServiceInstance(String bindingName, String applicationName, String serviceInstanceName,
+ Map parameters, ApplicationServicesUpdateCallback updateServicesCallback) {
+ try {
+ return handleExceptions(() -> delegate.bindServiceInstance(bindingName, applicationName, serviceInstanceName, parameters));
+ } catch (CloudOperationException e) {
+ updateServicesCallback.onError(e, applicationName, serviceInstanceName);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public void createApplication(ApplicationToCreateDto applicationToCreateDto) {
+ handleExceptions(() -> delegate.createApplication(applicationToCreateDto));
+ }
+
+ @Override
+ public void createServiceInstance(CloudServiceInstance serviceInstance) {
+ handleExceptions(() -> delegate.createServiceInstance(serviceInstance));
+ }
+
+ @Override
+ public String createServiceBroker(CloudServiceBroker serviceBroker) {
+ return handleExceptions(() -> delegate.createServiceBroker(serviceBroker));
+ }
+
+ @Override
+ public CloudServiceKey createAndFetchServiceKey(CloudServiceKey keyModel, String serviceInstanceName) {
+ return handleExceptions(() -> delegate.createAndFetchServiceKey(keyModel, serviceInstanceName));
+ }
+
+ @Override
+ public Optional createServiceKey(CloudServiceKey keyModel, String serviceInstanceName) {
+ return handleExceptions(() -> delegate.createServiceKey(keyModel, serviceInstanceName));
+ }
+
+ @Override
+ public Optional createServiceKey(String serviceInstanceName, String serviceKeyName, Map parameters) {
+ return handleExceptions(() -> delegate.createServiceKey(serviceInstanceName, serviceKeyName, parameters));
+ }
+
+ @Override
+ public void createUserProvidedServiceInstance(CloudServiceInstance serviceInstance) {
+ handleExceptions(() -> delegate.createUserProvidedServiceInstance(serviceInstance));
+ }
+
+ @Override
+ public void deleteApplication(String applicationName) {
+ handleExceptions(() -> delegate.deleteApplication(applicationName));
+ }
+
+ @Override
+ public void deleteDomain(String domainName) {
+ handleExceptions(() -> delegate.deleteDomain(domainName));
+ }
+
+ @Override
+ public void deleteOrphanedRoutes() {
+ handleExceptions(() -> delegate.deleteOrphanedRoutes());
+ }
+
+ @Override
+ public void deleteRoute(String host, String domainName, String path) {
+ handleExceptions(() -> delegate.deleteRoute(host, domainName, path));
+ }
+
+ @Override
+ public void deleteServiceInstance(String serviceInstanceName) {
+ handleExceptions(() -> delegate.deleteServiceInstance(serviceInstanceName));
+ }
+
+ @Override
+ public void deleteServiceInstance(CloudServiceInstance serviceInstance) {
+ handleExceptions(() -> delegate.deleteServiceInstance(serviceInstance));
+ }
+
+ @Override
+ public String deleteServiceBroker(String name) {
+ return handleExceptions(() -> delegate.deleteServiceBroker(name));
+ }
+
+ @Override
+ public CloudServiceBinding getServiceBinding(UUID serviceBindingId) {
+ return handleExceptions(() -> delegate.getServiceBinding(serviceBindingId));
+ }
+
+ @Override
+ public Optional deleteServiceBinding(String serviceInstanceName, String serviceKeyName) {
+ return handleExceptions(() -> delegate.deleteServiceBinding(serviceInstanceName, serviceKeyName));
+ }
+
+ @Override
+ public Optional deleteServiceBinding(UUID bindingGuid, ServiceBindingOperationCallback serviceBindingOperationCallback) {
+ try {
+ return handleExceptions(() -> delegate.deleteServiceBinding(bindingGuid));
+ } catch (CloudOperationException e) {
+ serviceBindingOperationCallback.onError(e, bindingGuid);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional deleteServiceBinding(UUID bindingGuid) {
+ return handleExceptions(() -> delegate.deleteServiceBinding(bindingGuid));
+ }
+
+ @Override
+ public CloudApplication getApplication(String applicationName) {
+ return handleExceptions(() -> delegate.getApplication(applicationName));
+ }
+
+ @Override
+ public CloudApplication getApplication(String applicationName, boolean required) {
+ return handleExceptions(() -> delegate.getApplication(applicationName, required));
+ }
+
+ @Override
+ public UUID getApplicationGuid(String applicationName) {
+ return handleExceptions(() -> delegate.getApplicationGuid(applicationName));
+ }
+
+ @Override
+ public String getApplicationName(UUID applicationGuid) {
+ return handleExceptions(() -> delegate.getApplicationName(applicationGuid));
+ }
+
+ @Override
+ public Map getApplicationEnvironment(UUID applicationGuid) {
+ return handleExceptions(() -> delegate.getApplicationEnvironment(applicationGuid));
+ }
+
+ @Override
+ public Map getApplicationEnvironment(String applicationName) {
+ return handleExceptions(() -> delegate.getApplicationEnvironment(applicationName));
+ }
+
+ @Override
+ public List getApplicationEvents(String applicationName) {
+ return handleExceptions(() -> delegate.getApplicationEvents(applicationName));
+ }
+
+ @Override
+ public List getEventsByActee(UUID uuid) {
+ return handleExceptions(() -> delegate.getEventsByTarget(uuid));
+ }
+
+ @Override
+ public InstancesInfo getApplicationInstances(CloudApplication app) {
+ return handleExceptions(() -> delegate.getApplicationInstances(app));
+ }
+
+ @Override
+ public InstancesInfo getApplicationInstances(UUID applicationGuid) {
+ return handleExceptions(() -> delegate.getApplicationInstances(applicationGuid));
+ }
+
+ @Override
+ public CloudProcess getApplicationProcess(UUID applicationGuid) {
+ return handleExceptions(() -> delegate.getApplicationProcess(applicationGuid));
+ }
+
+ @Override
+ public List getApplicationRoutes(UUID applicationGuid) {
+ return handleExceptions(() -> delegate.getApplicationRoutes(applicationGuid));
+ }
+
+ @Override
+ public boolean getApplicationSshEnabled(UUID applicationGuid) {
+ return handleExceptions(() -> delegate.getApplicationSshEnabled(applicationGuid));
+ }
+
+ @Override
+ public List getApplications() {
+ return handleExceptions(() -> delegate.getApplications());
+ }
+
+ @Override
+ public List getApplicationsByMetadataLabelSelector(String labelSelector) {
+ return handleExceptions(() -> delegate.getApplicationsByMetadataLabelSelector(labelSelector));
+ }
+
+ @Override
+ public CloudDomain getDefaultDomain() {
+ return handleExceptions(() -> delegate.getDefaultDomain());
+ }
+
+ @Override
+ public List getDomains() {
+ return handleExceptions(() -> delegate.getDomains());
+ }
+
+ @Override
+ public List getDomainsForOrganization() {
+ return handleExceptions(() -> delegate.getDomainsForOrganization());
+ }
+
+ @Override
+ public List getEvents() {
+ return handleExceptions(() -> delegate.getEvents());
+ }
+
+ @Override
+ public List getPrivateDomains() {
+ return handleExceptions(() -> delegate.getPrivateDomains());
+ }
+
+ @Override
+ public List getRoutes(String domainName) {
+ return handleExceptions(() -> delegate.getRoutes(domainName));
+ }
+
+ @Override
+ public CloudServiceBroker getServiceBroker(String name) {
+ return handleExceptions(() -> delegate.getServiceBroker(name));
+ }
+
+ @Override
+ public CloudServiceBroker getServiceBroker(String name, boolean required) {
+ return handleExceptions(() -> delegate.getServiceBroker(name, required));
+ }
+
+ @Override
+ public List getServiceBrokers() {
+ return handleExceptions(() -> delegate.getServiceBrokers());
+ }
+
+ @Override
+ public UUID getRequiredServiceInstanceGuid(String name) {
+ return handleExceptions(() -> delegate.getRequiredServiceInstanceGuid(name));
+ }
+
+ @Override
+ public CloudServiceInstance getServiceInstance(String serviceInstanceName) {
+ return handleExceptions(() -> delegate.getServiceInstance(serviceInstanceName));
+ }
+
+ @Override
+ public CloudServiceInstance getServiceInstance(String serviceInstanceName, boolean required) {
+ return handleExceptions(() -> delegate.getServiceInstance(serviceInstanceName, required));
+ }
+
+ @Override
+ public String getServiceInstanceName(UUID serviceInstanceGuid) {
+ return handleExceptions(() -> delegate.getServiceInstanceName(serviceInstanceGuid));
+ }
+
+ @Override
+ public CloudServiceInstance getServiceInstanceWithoutAuxiliaryContent(String serviceInstanceName) {
+ return handleExceptions(() -> delegate.getServiceInstanceWithoutAuxiliaryContent(serviceInstanceName));
+ }
+
+ @Override
+ public CloudServiceInstance getServiceInstanceWithoutAuxiliaryContent(String serviceInstanceName, boolean required) {
+ return handleExceptions(() -> delegate.getServiceInstanceWithoutAuxiliaryContent(serviceInstanceName, required));
+ }
+
+ @Override
+ public List getServiceAppBindings(UUID serviceInstanceGuid) {
+ return handleExceptions(() -> delegate.getServiceAppBindings(serviceInstanceGuid));
+ }
+
+ @Override
+ public List getAppBindings(UUID applicationGuid) {
+ return handleExceptions(() -> delegate.getAppBindings(applicationGuid));
+ }
+
+ @Override
+ public CloudServiceBinding getServiceBindingForApplication(UUID applicationId, UUID serviceInstanceGuid) {
+ return handleExceptions(() -> delegate.getServiceBindingForApplication(applicationId, serviceInstanceGuid));
+ }
+
+ @Override
+ public Map getServiceInstanceParameters(UUID guid) {
+ return handleExceptions(() -> delegate.getServiceInstanceParameters(guid));
+ }
+
+ @Override
+ public Map getUserProvidedServiceInstanceParameters(UUID guid) {
+ return handleExceptions(() -> delegate.getUserProvidedServiceInstanceParameters(guid));
+ }
+
+ @Override
+ public Map getServiceBindingParameters(UUID guid) {
+ return handleExceptions(() -> delegate.getServiceBindingParameters(guid));
+ }
+
+ @Override
+ public CloudServiceKey getServiceKey(String serviceInstanceName, String serviceKeyName) {
+ return handleExceptions(() -> delegate.getServiceKey(serviceInstanceName, serviceKeyName));
+ }
+
+ @Override
+ public List getServiceKeys(String serviceInstanceName) {
+ return handleExceptions(() -> delegate.getServiceKeys(serviceInstanceName));
+ }
+
+ @Override
+ public List getServiceKeysWithCredentials(String serviceInstanceName) {
+ return handleExceptions(() -> delegate.getServiceKeysWithCredentials(serviceInstanceName));
+ }
+
+ @Override
+ public List getServiceKeys(CloudServiceInstance serviceInstance) {
+ return handleExceptions(() -> delegate.getServiceKeys(serviceInstance));
+ }
+
+ @Override
+ public List getServiceKeysWithCredentials(CloudServiceInstance serviceInstance) {
+ return handleExceptions(() -> delegate.getServiceKeysWithCredentials(serviceInstance));
+ }
+
+ @Override
+ public List getServiceOfferings() {
+ return handleExceptions(() -> delegate.getServiceOfferings());
+ }
+
+ @Override
+ public List getServiceInstancesByMetadataLabelSelector(String labelSelector) {
+ return handleExceptions(() -> delegate.getServiceInstancesByMetadataLabelSelector(labelSelector));
+ }
+
+ @Override
+ public List getServiceInstancesWithoutAuxiliaryContentByMetadataLabelSelector(String labelSelector) {
+ return handleExceptions(() -> delegate.getServiceInstancesWithoutAuxiliaryContentByMetadataLabelSelector(labelSelector));
+ }
+
+ @Override
+ public List getSharedDomains() {
+ return handleExceptions(() -> delegate.getSharedDomains());
+ }
+
+ @Override
+ public CloudStack getStack(String name) {
+ return handleExceptions(() -> delegate.getStack(name));
+ }
+
+ @Override
+ public CloudStack getStack(String name, boolean required) {
+ return handleExceptions(() -> delegate.getStack(name, required));
+ }
+
+ @Override
+ public List getStacks() {
+ return handleExceptions(() -> delegate.getStacks());
+ }
+
+ @Override
+ public void rename(String applicationName, String newName) {
+ handleExceptions(() -> delegate.rename(applicationName, newName));
+ }
+
+ @Override
+ public void restartApplication(String applicationName) {
+ handleExceptions(() -> delegate.restartApplication(applicationName));
+ }
+
+ @Override
+ public void startApplication(String applicationName) {
+ handleExceptions(() -> delegate.startApplication(applicationName));
+ }
+
+ @Override
+ public void stopApplication(String applicationName) {
+ handleExceptions(() -> delegate.stopApplication(applicationName));
+ }
+
+ @Override
+ public Optional unbindServiceInstance(String applicationName, String serviceInstanceName,
+ ApplicationServicesUpdateCallback applicationServicesUpdateCallback) {
+ try {
+ return handleExceptions(() -> delegate.unbindServiceInstance(applicationName, serviceInstanceName));
+ } catch (CloudOperationException e) {
+ applicationServicesUpdateCallback.onError(e, applicationName, serviceInstanceName);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional unbindServiceInstance(String applicationName, String serviceInstanceName) {
+ return handleExceptions(() -> delegate.unbindServiceInstance(applicationName, serviceInstanceName));
+ }
+
+ @Override
+ public Optional unbindServiceInstance(UUID applicationGuid, UUID serviceInstanceGuid) {
+ return handleExceptions(() -> delegate.unbindServiceInstance(applicationGuid, serviceInstanceGuid));
+ }
+
+ @Override
+ public void updateApplicationDiskQuota(String applicationName, int disk) {
+ handleExceptions(() -> delegate.updateApplicationDiskQuota(applicationName, disk));
+ }
+
+ @Override
+ public void updateApplicationEnv(String applicationName, Map env) {
+ handleExceptions(() -> delegate.updateApplicationEnv(applicationName, env));
+ }
+
+ @Override
+ public void updateApplicationInstances(String applicationName, int instances) {
+ handleExceptions(() -> delegate.updateApplicationInstances(applicationName, instances));
+ }
+
+ @Override
+ public List getServiceInstancesWithoutAuxiliaryContentByNames(List names) {
+ return handleExceptions(() -> delegate.getServiceInstancesWithoutAuxiliaryContentByNames(names));
+ }
+
+ @Override
+ public void updateApplicationMemory(String applicationName, int memory) {
+ handleExceptions(() -> delegate.updateApplicationMemory(applicationName, memory));
+ }
+
+ @Override
+ public void updateApplicationStaging(String applicationName, Staging staging) {
+ handleExceptions(() -> delegate.updateApplicationStaging(applicationName, staging));
+ }
+
+ @Override
+ public void updateApplicationRoutes(String applicationName, Set routes) {
+ handleExceptions(() -> delegate.updateApplicationRoutes(applicationName, routes));
+ }
+
+ @Override
+ public void updateApplicationMetadata(UUID guid, Metadata metadata) {
+ handleExceptions(() -> delegate.updateApplicationMetadata(guid, metadata));
+ }
+
+ @Override
+ public void updateServiceInstanceMetadata(UUID guid, Metadata metadata) {
+ handleExceptions(() -> delegate.updateServiceInstanceMetadata(guid, metadata));
+ }
+
+ @Override
+ public void updateServiceBindingMetadata(UUID guid, Metadata metadata) {
+ handleExceptions(() -> delegate.updateServiceBindingMetadata(guid, metadata));
+ }
+
+ @Override
+ public String updateServiceBroker(CloudServiceBroker serviceBroker) {
+ return handleExceptions(() -> delegate.updateServiceBroker(serviceBroker));
+ }
+
+ @Override
+ public void updateServicePlanVisibilityForBroker(String name, ServicePlanVisibility visibility) {
+ handleExceptions(() -> delegate.updateServicePlanVisibilityForBroker(name, visibility));
+ }
+
+ @Override
+ public void updateServicePlan(String serviceName, String planName) {
+ handleExceptions(() -> delegate.updateServicePlan(serviceName, planName));
+ }
+
+ @Override
+ public void updateServiceParameters(String serviceName, Map parameters) {
+ handleExceptions(() -> delegate.updateServiceParameters(serviceName, parameters));
+ }
+
+ @Override
+ public void updateServiceTags(String serviceName, List tags) {
+ handleExceptions(() -> delegate.updateServiceTags(serviceName, tags));
+ }
+
+ @Override
+ public void updateServiceSyslogDrainUrl(String serviceName, String syslogDrainUrl) {
+ handleExceptions(() -> delegate.updateServiceSyslogDrainUrl(serviceName, syslogDrainUrl));
+ }
+
+ @Override
+ public CloudPackage asyncUploadApplicationWithExponentialBackoff(String applicationName, Path file, UploadStatusCallback callback,
+ Duration overrideTimeout) {
+ return handleExceptions(() -> delegate.asyncUploadApplication(applicationName, file, callback, overrideTimeout));
+ }
+
+ @Override
+ public Upload getUploadStatus(UUID packageGuid) {
+ return handleExceptions(() -> delegate.getUploadStatus(packageGuid));
+ }
+
+ @Override
+ public CloudBuild createBuild(UUID packageGuid) {
+ return handleExceptions(() -> delegate.createBuild(packageGuid));
+ }
+
+ @Override
+ public CloudBuild getBuild(UUID buildGuid) {
+ return handleExceptions(() -> delegate.getBuild(buildGuid));
+ }
+
+ @Override
+ public CloudTask getTask(UUID taskGuid) {
+ return handleExceptions(() -> delegate.getTask(taskGuid));
+ }
+
+ @Override
+ public List getTasks(String applicationName) {
+ return handleExceptions(() -> delegate.getTasks(applicationName));
+ }
+
+ @Override
+ public CloudTask runTask(String applicationName, CloudTask task) {
+ return handleExceptions(() -> delegate.runTask(applicationName, task));
+ }
+
+ @Override
+ public CloudTask cancelTask(UUID taskGuid) {
+ return handleExceptions(() -> delegate.cancelTask(taskGuid));
+ }
+
+ @Override
+ public void bindDropletToApp(UUID dropletGuid, UUID applicationGuid) {
+ handleExceptions(() -> delegate.bindDropletToApp(dropletGuid, applicationGuid));
+ }
+
+ @Override
+ public List getBuildsForApplication(UUID applicationGuid) {
+ return handleExceptions(() -> delegate.getBuildsForApplication(applicationGuid));
+ }
+
+ @Override
+ public List getBuildsForPackage(UUID packageGuid) {
+ return handleExceptions(() -> delegate.getBuildsForPackage(packageGuid));
+ }
+
+ @Override
+ public DropletInfo getCurrentDropletForApplication(UUID applicationGuid) {
+ return handleExceptions(() -> delegate.getCurrentDropletForApplication(applicationGuid));
+ }
+
+ @Override
+ public CloudPackage getPackage(UUID packageGuid) {
+ return handleExceptions(() -> delegate.getPackage(packageGuid));
+ }
+
+ @Override
+ public List getPackagesForApplication(UUID applicationGuid) {
+ return handleExceptions(() -> delegate.getPackagesForApplication(applicationGuid));
+ }
+
+ @Override
+ public Set getUserRolesBySpaceAndUser(UUID spaceGuid, UUID userGuid) {
+ return handleExceptions(() -> delegate.getUserRolesBySpaceAndUser(spaceGuid, userGuid));
+ }
+
+ @Override
+ public CloudPackage createDockerPackage(UUID applicationGuid, DockerInfo dockerInfo) {
+ return handleExceptions(() -> delegate.createDockerPackage(applicationGuid, dockerInfo));
+ }
+
+ @Override
+ public CloudAsyncJob getAsyncJob(String jobId) {
+ return handleExceptions(() -> delegate.getAsyncJob(jobId));
+ }
+
+ private void handleExceptions(Runnable runnable) {
+ handleExceptions(() -> {
+ runnable.run();
+ return null;
+ });
+ }
+
+ 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);
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerException.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerException.java
new file mode 100644
index 0000000000..2021ffeab8
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerException.java
@@ -0,0 +1,40 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+import java.text.MessageFormat;
+
+import org.springframework.http.HttpStatus;
+
+public class CloudControllerException extends CloudOperationException {
+
+ private static final long serialVersionUID = 1L;
+ private static final String DEFAULT_CLOUD_CONTROLLER_ERROR_MESSAGE = "Controller operation failed: {0}";
+
+ public CloudControllerException(CloudOperationException cloudOperationException) {
+ super(cloudOperationException.getStatusCode(),
+ cloudOperationException.getStatusText(),
+ cloudOperationException.getDescription(),
+ cloudOperationException);
+ }
+
+ public CloudControllerException(HttpStatus statusCode) {
+ super(statusCode);
+ }
+
+ public CloudControllerException(HttpStatus statusCode, String statusText) {
+ super(statusCode, statusText);
+ }
+
+ public CloudControllerException(HttpStatus statusCode, String statusText, String description) {
+ super(statusCode, statusText, description);
+ }
+
+ @Override
+ public String getMessage() {
+ return decorateExceptionMessage(super.getMessage());
+ }
+
+ private String decorateExceptionMessage(String exceptionMessage) {
+ return MessageFormat.format(DEFAULT_CLOUD_CONTROLLER_ERROR_MESSAGE, exceptionMessage);
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudCredentials.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudCredentials.java
new file mode 100644
index 0000000000..bfd843020c
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudCredentials.java
@@ -0,0 +1,225 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo;
+
+/**
+ * Class that encapsulates credentials used for authentication
+ *
+ */
+public class CloudCredentials {
+
+ private String clientId = "cf";
+ private String clientSecret = "";
+ private String email;
+ private String password;
+ private String proxyUser;
+ private String origin;
+ private boolean refreshable = true;
+ private OAuth2AccessTokenWithAdditionalInfo token;
+
+ /**
+ * Create credentials using email and password.
+ *
+ * @param email email to authenticate with
+ * @param password the password
+ */
+ public CloudCredentials(String email, String password) {
+ this.email = email;
+ this.password = password;
+ }
+
+ /**
+ * Create credentials using email, password, and client ID.
+ *
+ * @param email email to authenticate with
+ * @param password the password
+ * @param clientId the client ID to use for authorization
+ */
+ public CloudCredentials(String email, String password, String clientId) {
+ this.email = email;
+ this.password = password;
+ this.clientId = clientId;
+ }
+
+ /**
+ * Create credentials using email, password and client ID.
+ *
+ * @param email email to authenticate with
+ * @param password the password
+ * @param clientId the client ID to use for authorization
+ * @param clientSecret the secret for the given client
+ */
+ public CloudCredentials(String email, String password, String clientId, String clientSecret) {
+ this.email = email;
+ this.password = password;
+ this.clientId = clientId;
+ this.clientSecret = clientSecret;
+ }
+
+ /**
+ * Create credentials using email, password, client ID and login origin.
+ *
+ * @param email email to authenticate with
+ * @param password the password
+ * @param clientId the client ID to use for authorization
+ * @param clientSecret the secret for the given client
+ * @param origin the origin name
+ */
+ public CloudCredentials(String email, String password, String clientId, String clientSecret, String origin) {
+ this.email = email;
+ this.password = password;
+ this.clientId = clientId;
+ this.clientSecret = clientSecret;
+ this.origin = origin;
+ }
+
+ /**
+ * Create credentials using a token.
+ *
+ * @param token token to use for authorization
+ */
+ public CloudCredentials(OAuth2AccessTokenWithAdditionalInfo token) {
+ this.token = token;
+ }
+
+ /**
+ * Create credentials using a token and indicates if the token is refreshable or not.
+ *
+ * @param token token to use for authorization
+ * @param refreshable indicates if the token can be refreshed or not
+ */
+ public CloudCredentials(OAuth2AccessTokenWithAdditionalInfo token, boolean refreshable) {
+ this.token = token;
+ this.refreshable = refreshable;
+ }
+
+ /**
+ * Create credentials using a token.
+ *
+ * @param token token to use for authorization
+ * @param clientId the client ID to use for authorization
+ */
+ public CloudCredentials(OAuth2AccessTokenWithAdditionalInfo token, String clientId) {
+ this.token = token;
+ this.clientId = clientId;
+ }
+
+ /**
+ * Create credentials using a token.
+ *
+ * @param token token to use for authorization
+ * @param clientId the client ID to use for authorization
+ * @param clientSecret the password for the specified client
+ */
+ public CloudCredentials(OAuth2AccessTokenWithAdditionalInfo token, String clientId, String clientSecret) {
+ this.token = token;
+ this.clientId = clientId;
+ this.clientSecret = clientSecret;
+ }
+
+ /**
+ * Create proxy credentials.
+ *
+ * @param cloudCredentials credentials to use
+ * @param proxyForUser user to be proxied
+ */
+ public CloudCredentials(CloudCredentials cloudCredentials, String proxyForUser) {
+ this.email = cloudCredentials.getEmail();
+ this.password = cloudCredentials.getPassword();
+ this.clientId = cloudCredentials.getClientId();
+ this.token = cloudCredentials.getToken();
+ this.proxyUser = proxyForUser;
+ }
+
+ /**
+ * Get the client ID.
+ *
+ * @return the client ID
+ */
+ public String getClientId() {
+ return clientId;
+ }
+
+ /**
+ * Get the client secret
+ *
+ * @return the client secret
+ */
+ public String getClientSecret() {
+ return clientSecret;
+ }
+
+ /**
+ * Get the origin
+ *
+ * @return the origin
+ */
+ public String getOrigin() {
+ return origin;
+ }
+
+ /**
+ * Get the email.
+ *
+ * @return the email
+ */
+ public String getEmail() {
+ return email;
+ }
+
+ /**
+ * Get the password.
+ *
+ * @return the password
+ */
+ public String getPassword() {
+ return password;
+ }
+
+ /**
+ * Get the proxy user.
+ *
+ * @return the proxy user
+ */
+ public String getProxyUser() {
+ return proxyUser;
+ }
+
+ /**
+ * Get the token.
+ *
+ * @return the token
+ */
+ public OAuth2AccessTokenWithAdditionalInfo getToken() {
+ return token;
+ }
+
+ /**
+ * Is this a proxied set of credentials?
+ *
+ * @return whether a proxy user is set
+ */
+ public boolean isProxyUserSet() {
+ return proxyUser != null;
+ }
+
+ /**
+ * Indicates weather the token stored in the cloud credentials can be refreshed or not. This is useful when the token stored in this
+ * object was obtained via implicit OAuth2 authentication and therefore can not be refreshed.
+ *
+ * @return weather the token can be refreshed
+ */
+ public boolean isRefreshable() {
+ return refreshable;
+ }
+
+ /**
+ * Run commands as a different user. The authenticated user must be privileged to run as this user.
+ *
+ * @param user the user to proxy for
+ * @return credentials for the proxied user
+ */
+ public CloudCredentials proxyForUser(String user) {
+ return new CloudCredentials(this, user);
+ }
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudException.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudException.java
new file mode 100644
index 0000000000..ca6b64926f
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudException.java
@@ -0,0 +1,18 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+@SuppressWarnings("serial")
+public class CloudException extends RuntimeException {
+
+ public CloudException(Throwable cause) {
+ super(cause);
+ }
+
+ public CloudException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public CloudException(String message) {
+ super(message);
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudOperationException.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudOperationException.java
new file mode 100644
index 0000000000..2f75d0ce85
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudOperationException.java
@@ -0,0 +1,50 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+import org.springframework.http.HttpStatus;
+
+@SuppressWarnings("serial")
+public class CloudOperationException extends CloudException {
+
+ private final HttpStatus statusCode;
+ private final String statusText;
+ private final String description;
+
+ public CloudOperationException(HttpStatus statusCode) {
+ this(statusCode, statusCode.getReasonPhrase());
+ }
+
+ public CloudOperationException(HttpStatus statusCode, String statusText) {
+ this(statusCode, statusText, null);
+ }
+
+ public CloudOperationException(HttpStatus statusCode, String statusText, String description) {
+ this(statusCode, statusText, description, null);
+ }
+
+ public CloudOperationException(HttpStatus statusCode, String statusText, String description, Throwable cause) {
+ super(getExceptionMessage(statusCode, statusText, description), cause);
+ this.statusCode = statusCode;
+ this.statusText = statusText;
+ this.description = description;
+ }
+
+ private static String getExceptionMessage(HttpStatus statusCode, String statusText, String description) {
+ if (description != null) {
+ return String.format("%d %s: %s", statusCode.value(), statusText, description);
+ }
+ return String.format("%d %s", statusCode.value(), statusText);
+ }
+
+ public HttpStatus getStatusCode() {
+ return statusCode;
+ }
+
+ public String getStatusText() {
+ return statusText;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudServiceBrokerException.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudServiceBrokerException.java
new file mode 100644
index 0000000000..92a0b2cec2
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/CloudServiceBrokerException.java
@@ -0,0 +1,39 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+import java.text.MessageFormat;
+
+import org.springframework.http.HttpStatus;
+
+public class CloudServiceBrokerException extends CloudOperationException {
+
+ private static final long serialVersionUID = 1L;
+ private static final String DEFAULT_SERVICE_BROKER_ERROR_MESSAGE = "Service broker operation failed: {0}";
+
+ public CloudServiceBrokerException(CloudOperationException cloudOperationException) {
+ super(cloudOperationException.getStatusCode(),
+ cloudOperationException.getStatusText(),
+ cloudOperationException.getDescription(),
+ cloudOperationException);
+ }
+
+ public CloudServiceBrokerException(HttpStatus statusCode) {
+ super(statusCode);
+ }
+
+ public CloudServiceBrokerException(HttpStatus statusCode, String statusText) {
+ super(statusCode, statusText);
+ }
+
+ public CloudServiceBrokerException(HttpStatus statusCode, String statusText, String description) {
+ super(statusCode, statusText, description);
+ }
+
+ @Override
+ public String getMessage() {
+ return decorateExceptionMessage(super.getMessage());
+ }
+
+ private String decorateExceptionMessage(String exceptionMessage) {
+ return MessageFormat.format(DEFAULT_SERVICE_BROKER_ERROR_MESSAGE, exceptionMessage);
+ }
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/Constants.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/Constants.java
new file mode 100644
index 0000000000..3bc7be98c2
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/Constants.java
@@ -0,0 +1,13 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+public class Constants {
+
+ private Constants() {
+
+ }
+
+ public static final String PACKAGE = "package";
+
+ public static final String ORIGIN_KEY = "origin";
+
+}
\ No newline at end of file
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/Messages.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/Messages.java
new file mode 100644
index 0000000000..50bf9ae64b
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/Messages.java
@@ -0,0 +1,30 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+public final class Messages {
+
+ private Messages() {
+
+ }
+
+ // INFO messages
+ public static final String CALLING_CF_ROOT_0_TO_ACCESS_LOG_CACHE_URL = "Calling CF root: {0} to access log-cache URL";
+ public static final String CF_ROOT_REQUEST_FINISHED = "CF root request finished";
+ public static final String TRYING_TO_GET_APP_LOGS = "Trying to get app logs";
+ public static final String APP_LOGS_WERE_FETCHED_SUCCESSFULLY = "App logs were fetched successfully";
+
+ // WARN messages
+ public static final String RETRYING_OPERATION = "Retrying operation that failed with: {0}";
+ public static final String CALL_TO_0_FAILED_WITH_1 = "Calling {0} failed with: {1}";
+
+ // ERROR messages
+ public static final String UNKNOWN_PACKAGE_TYPE = "Unknown package type: %s";
+ public static final String CANT_CREATE_SERVICE_KEY_FOR_USER_PROVIDED_SERVICE = "Service keys can't be created for user-provided service instance \"%s\"";
+ 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_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 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 FAILED_TO_FETCH_APP_LOGS_FOR_APP = "Failed to fetch app logs for app: %s";
+
+ public static final String BUILDPACKS_ARE_REQUIRED_FOR_CNB_LIFECYCLE_TYPE = "Buildpacks are required for CNB lifecycle type.";
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/Nullable.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/Nullable.java
new file mode 100644
index 0000000000..315978dea0
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/Nullable.java
@@ -0,0 +1,5 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+public @interface Nullable {
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/ServiceBindingOperationCallback.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/ServiceBindingOperationCallback.java
new file mode 100644
index 0000000000..c79e7ecd17
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/ServiceBindingOperationCallback.java
@@ -0,0 +1,8 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+import java.util.UUID;
+
+public interface ServiceBindingOperationCallback {
+
+ void onError(CloudOperationException e, UUID serviceBindingGuid);
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/SkipNulls.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/SkipNulls.java
new file mode 100644
index 0000000000..62e1ea2a86
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/SkipNulls.java
@@ -0,0 +1,5 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+public @interface SkipNulls {
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/UploadStatusCallback.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/UploadStatusCallback.java
new file mode 100644
index 0000000000..7a22102a85
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/UploadStatusCallback.java
@@ -0,0 +1,68 @@
+package org.cloudfoundry.multiapps.controller.client.facade;
+
+import java.util.Set;
+
+/**
+ * Reports status information when uploading an application.
+ */
+public interface UploadStatusCallback {
+
+ /**
+ * Empty implementation
+ */
+ UploadStatusCallback NONE = new UploadStatusCallback() {
+ @Override
+ public void onCheckResources() {
+ }
+
+ @Override
+ public void onMatchedFileNames(Set matchedFileNames) {
+ }
+
+ @Override
+ public void onProcessMatchedResources(int length) {
+ }
+
+ @Override
+ public boolean onProgress(String status) {
+ return false;
+ }
+
+ @Override
+ public void onError(String description) {
+ }
+ };
+
+ /**
+ * Called after the /resources call is made.
+ */
+ void onCheckResources();
+
+ /**
+ * Called after the files to be uploaded have been identified.
+ *
+ * @param matchedFileNames the files to be uploaded
+ */
+ void onMatchedFileNames(Set matchedFileNames);
+
+ /**
+ * Called after the data to be uploaded has been processed
+ *
+ * @param length the size of the upload data (before compression)
+ */
+ void onProcessMatchedResources(int length);
+
+ /**
+ * Called during asynchronous upload process.
+ *
+ * Implementation can return true to unsubscribe from progress update reports. This is useful if the caller want to unblock the thread
+ * that initiated the upload. Note, however, that the upload job that has been asynchronously started will continue to execute on the
+ * server.
+ *
+ * @param status string such as "queued", "finished"
+ * @return true to unsubscribe from update report
+ */
+ boolean onProgress(String status);
+
+ void onError(String description);
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/ApplicationLogEntity.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/ApplicationLogEntity.java
new file mode 100644
index 0000000000..69301aeda1
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/ApplicationLogEntity.java
@@ -0,0 +1,43 @@
+package org.cloudfoundry.multiapps.controller.client.facade.adapters;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.immutables.value.Value;
+
+import java.util.Map;
+
+@Value.Immutable
+@Value.Enclosing
+@JsonDeserialize(as = ImmutableApplicationLogEntity.class)
+public abstract class ApplicationLogEntity implements Comparable {
+
+ @JsonProperty("timestamp")
+ public abstract Long getTimestampInNanoseconds();
+
+ @JsonProperty("source_id")
+ public abstract String getSourceId();
+
+ @JsonProperty("instance_id")
+ public abstract String getInstanceId();
+
+ public abstract Map getTags();
+
+ @JsonProperty("log")
+ public abstract LogBody getLogBody();
+
+ @Value.Immutable
+ @JsonDeserialize(as = ImmutableApplicationLogEntity.ImmutableLogBody.class)
+ public interface LogBody {
+
+ @JsonProperty("payload")
+ String getMessage();
+
+ @JsonProperty("type")
+ String getMessageType();
+ }
+
+ @Override
+ public int compareTo(ApplicationLogEntity otherLog) {
+ return getTimestampInNanoseconds().compareTo(otherLog.getTimestampInNanoseconds());
+ }
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/ApplicationLogsResponse.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/ApplicationLogsResponse.java
new file mode 100644
index 0000000000..0617ded5f3
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/ApplicationLogsResponse.java
@@ -0,0 +1,17 @@
+package org.cloudfoundry.multiapps.controller.client.facade.adapters;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonRootName;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.immutables.value.Value;
+
+import java.util.List;
+
+@Value.Immutable
+@JsonRootName("envelopes")
+@JsonDeserialize(as = ImmutableApplicationLogsResponse.class)
+public interface ApplicationLogsResponse {
+
+ @JsonProperty("batch")
+ List getLogs();
+}
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
new file mode 100644
index 0000000000..5461f765a8
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/CloudFoundryClientFactory.java
@@ -0,0 +1,155 @@
+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
new file mode 100644
index 0000000000..ea8d519b3f
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/LogCacheClient.java
@@ -0,0 +1,117 @@
+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;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.Base64;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+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;
+ }
+
+ 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);
+
+ 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 {
+ 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();
+ }
+
+ private ReadResponse readApplicationLogs(org.cloudfoundry.logcache.v1.LogCacheClient logCacheClient, 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();
+ }
+
+ private ApplicationLog mapToAppLog(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()
+ .get(SOURCE_TYPE_KEY_NAME))
+ .build();
+ }
+
+ private String decodeLogPayload(String base64Encoded) {
+ var result = Base64.getDecoder()
+ .decode(base64Encoded.getBytes(StandardCharsets.UTF_8));
+ return new String(result, StandardCharsets.UTF_8);
+ }
+
+ private LocalDateTime fromLogTimestamp(long timestampNanos) {
+ Duration duration = Duration.ofNanos(timestampNanos);
+ Instant instant = Instant.ofEpochSecond(duration.getSeconds(), duration.getNano());
+ return LocalDateTime.ofInstant(instant, ZoneId.of("UTC"));
+ }
+
+ private ApplicationLog.MessageType fromLogMessageType(String messageType) {
+ return "OUT".equals(messageType) ? ApplicationLog.MessageType.STDOUT : ApplicationLog.MessageType.STDERR;
+ }
+}
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
new file mode 100644
index 0000000000..ff5def4c97
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/OAuthTokenProvider.java
@@ -0,0 +1,27 @@
+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
new file mode 100644
index 0000000000..898cea1a8a
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudApplication.java
@@ -0,0 +1,72 @@
+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(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
new file mode 100644
index 0000000000..1e8bad359c
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudAsyncJob.java
@@ -0,0 +1,50 @@
+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(job.getState())
+ .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
new file mode 100644
index 0000000000..2cdbc39eaa
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudBuild.java
@@ -0,0 +1,67 @@
+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
new file mode 100644
index 0000000000..f03981dbaa
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudDomain.java
@@ -0,0 +1,24 @@
+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
new file mode 100644
index 0000000000..537b1260e2
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEntity.java
@@ -0,0 +1,79 @@
+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();
+ }
+
+ 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
new file mode 100644
index 0000000000..7a0937f970
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudEvent.java
@@ -0,0 +1,56 @@
+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
new file mode 100644
index 0000000000..73933bf781
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudPackage.java
@@ -0,0 +1,74 @@
+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
new file mode 100644
index 0000000000..8fb386031a
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcess.java
@@ -0,0 +1,44 @@
+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.immutables.value.Value;
+
+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;
+
+@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();
+ Integer healthCheckTimeout = null;
+ String healthCheckHttpEndpoint = null;
+ Integer healthCheckInvocationTimeout = null;
+ if (healthCheck.getData() != null) {
+ Data healthCheckData = healthCheck.getData();
+ healthCheckTimeout = healthCheckData.getTimeout();
+ healthCheckInvocationTimeout = healthCheckData.getInvocationTimeout();
+ healthCheckHttpEndpoint = healthCheckData.getEndpoint();
+ }
+ 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)
+ .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
new file mode 100644
index 0000000000..e2ae758814
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudRoute.java
@@ -0,0 +1,99 @@
+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
new file mode 100644
index 0000000000..ab9ba4cad8
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBinding.java
@@ -0,0 +1,37 @@
+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(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
new file mode 100644
index 0000000000..d76fcc5cdd
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceBroker.java
@@ -0,0 +1,40 @@
+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
new file mode 100644
index 0000000000..af4d58b936
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceInstance.java
@@ -0,0 +1,55 @@
+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(resource.getMetadata())
+ .name(resource.getName())
+ .plan(getServicePlanName())
+ .label(getLabelName())
+ .type(resource.getType())
+ .tags(resource.getTags())
+ .lastOperation(ServiceOperation.fromLastOperation(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
new file mode 100644
index 0000000000..97f6e8eb31
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceKey.java
@@ -0,0 +1,41 @@
+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(serviceBindingResource.getMetadata())
+ .name(serviceBindingResource.getName())
+ .credentials(getCredentials())
+ .serviceInstance(serviceInstance.derive())
+ .serviceKeyOperation(ServiceCredentialBindingOperation.from(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
new file mode 100644
index 0000000000..c252aa50bd
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServiceOffering.java
@@ -0,0 +1,45 @@
+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
new file mode 100644
index 0000000000..a79527037e
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudServicePlan.java
@@ -0,0 +1,37 @@
+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
new file mode 100644
index 0000000000..6c2a8ce152
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudStack.java
@@ -0,0 +1,25 @@
+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
new file mode 100644
index 0000000000..b963d4457a
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudTask.java
@@ -0,0 +1,50 @@
+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
new file mode 100644
index 0000000000..0c55d9f1e5
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawInstancesInfo.java
@@ -0,0 +1,47 @@
+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.immutables.value.Value;
+
+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;
+
+@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()))
+ .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
new file mode 100644
index 0000000000..8eacc03f55
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawUserRole.java
@@ -0,0 +1,21 @@
+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());
+ }
+
+}
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
new file mode 100644
index 0000000000..8155a4ad41
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawV3CloudServiceInstance.java
@@ -0,0 +1,25 @@
+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(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/ApplicationLog.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ApplicationLog.java
new file mode 100644
index 0000000000..4765ea0bac
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ApplicationLog.java
@@ -0,0 +1,34 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.time.LocalDateTime;
+
+import org.immutables.value.Value;
+
+@Value.Immutable
+public abstract class ApplicationLog implements Comparable {
+
+ public enum MessageType {
+ STDOUT, STDERR
+ }
+
+ public abstract String getApplicationGuid();
+
+ public abstract String getMessage();
+
+ public abstract LocalDateTime getTimestamp();
+
+ public abstract MessageType getMessageType();
+
+ public abstract String getSourceName();
+
+ @Override
+ public int compareTo(ApplicationLog other) {
+ return getTimestamp().compareTo(other.getTimestamp());
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s [%s] %s (%s, %s)", getApplicationGuid(), getTimestamp(), getMessage(),
+ getMessageType(), getSourceName());
+ }
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ApplicationLogs.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ApplicationLogs.java
new file mode 100644
index 0000000000..2601598245
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ApplicationLogs.java
@@ -0,0 +1,7 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.ArrayList;
+
+public class ApplicationLogs extends ArrayList {
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/BitsData.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/BitsData.java
new file mode 100644
index 0000000000..e8b2a5d548
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/BitsData.java
@@ -0,0 +1,33 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+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
+@Value.Enclosing
+@JsonSerialize(as = ImmutableBitsData.class)
+@JsonDeserialize(as = ImmutableBitsData.class)
+public interface BitsData extends CloudPackage.PackageData {
+
+ @Nullable
+ Checksum getChecksum();
+
+ @Nullable
+ String getError();
+
+ @Value.Immutable
+ @JsonSerialize(as = ImmutableBitsData.ImmutableChecksum.class)
+ @JsonDeserialize(as = ImmutableBitsData.ImmutableChecksum.class)
+ interface Checksum {
+
+ @Nullable
+ String getAlgorithm();
+
+ @Nullable
+ String getValue();
+
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudApplication.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudApplication.java
new file mode 100644
index 0000000000..f3ed3eb59b
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudApplication.java
@@ -0,0 +1,32 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudApplication.class)
+@JsonDeserialize(as = ImmutableCloudApplication.class)
+public abstract class CloudApplication extends CloudEntity implements Derivable {
+
+ public enum State {
+ STARTED, STOPPED
+ }
+
+ @Nullable
+ public abstract State getState();
+
+ @Nullable
+ public abstract Lifecycle getLifecycle();
+
+ @Nullable
+ public abstract CloudSpace getSpace();
+
+ @Override
+ public CloudApplication derive() {
+ return this;
+ }
+
+}
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
new file mode 100644
index 0000000000..394a40c3b7
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudAsyncJob.java
@@ -0,0 +1,31 @@
+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;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.cloudfoundry.multiapps.controller.client.facade.Nullable;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudAsyncJob.class)
+@JsonDeserialize(as = ImmutableCloudAsyncJob.class)
+public abstract class CloudAsyncJob extends CloudEntity implements Derivable {
+
+ public abstract JobState getState();
+
+ @Nullable
+ public abstract String getOperation();
+
+ @Nullable
+ public abstract String getWarnings();
+
+ @Nullable
+ public abstract String getErrors();
+
+ @Override
+ public CloudAsyncJob derive() {
+ return this;
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudBuild.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudBuild.java
new file mode 100644
index 0000000000..6992277f62
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudBuild.java
@@ -0,0 +1,67 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.UUID;
+
+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.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudBuild.ImmutableCreatedBy;
+import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudBuild.ImmutablePackageInfo;
+
+@Value.Enclosing
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudBuild.class)
+@JsonDeserialize(as = ImmutableCloudBuild.class)
+public abstract class CloudBuild extends CloudEntity implements Derivable {
+
+ @Nullable
+ public abstract State getState();
+
+ @Nullable
+ public abstract CreatedBy getCreatedBy();
+
+ @Nullable
+ public abstract DropletInfo getDropletInfo();
+
+ @Nullable
+ public abstract PackageInfo getPackageInfo();
+
+ @Nullable
+ public abstract String getError();
+
+ @Override
+ public CloudBuild derive() {
+ return this;
+ }
+
+ public enum State {
+ FAILED, STAGED, STAGING
+ }
+
+ @Value.Immutable
+ @JsonSerialize(as = ImmutablePackageInfo.class)
+ @JsonDeserialize(as = ImmutablePackageInfo.class)
+ public interface PackageInfo {
+
+ @Nullable
+ @Value.Parameter
+ UUID getGuid();
+
+ }
+
+ @Value.Immutable
+ @JsonSerialize(as = ImmutableCreatedBy.class)
+ @JsonDeserialize(as = ImmutableCreatedBy.class)
+ public interface CreatedBy {
+
+ @Nullable
+ UUID getGuid();
+
+ @Nullable
+ String getName();
+
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudDomain.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudDomain.java
new file mode 100644
index 0000000000..93ecbab450
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudDomain.java
@@ -0,0 +1,18 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import org.immutables.value.Value;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudDomain.class)
+@JsonDeserialize(as = ImmutableCloudDomain.class)
+public abstract class CloudDomain extends CloudEntity implements Derivable {
+
+ @Override
+ public CloudDomain derive() {
+ return this;
+ }
+
+}
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
new file mode 100644
index 0000000000..16bf0aef9b
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudEntity.java
@@ -0,0 +1,29 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.UUID;
+
+import org.cloudfoundry.client.v3.Metadata;
+
+import org.cloudfoundry.multiapps.controller.client.facade.Nullable;
+
+/**
+ * Do not extend {@code Derivable} in this interface. It is tempting, because all of its children have the same implementation, but
+ * implementing the {@code derive()} method here leads to this bug: https://github.com/immutables/immutables/issues/1045
+ *
+ */
+public abstract class CloudEntity {
+
+ @Nullable
+ public abstract String getName();
+
+ @Nullable
+ public abstract CloudMetadata getMetadata();
+
+ @Nullable
+ public abstract Metadata getV3Metadata();
+
+ public UUID getGuid() {
+ return getMetadata().getGuid();
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudEvent.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudEvent.java
new file mode 100644
index 0000000000..3afabb6db0
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudEvent.java
@@ -0,0 +1,54 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+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.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudEvent.ImmutableParticipant;
+
+@Value.Enclosing
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudEvent.class)
+@JsonDeserialize(as = ImmutableCloudEvent.class)
+public abstract class CloudEvent extends CloudEntity implements Derivable {
+
+ @Nullable
+ public abstract String getType();
+
+ @Nullable
+ public abstract Participant getActor();
+
+ @Nullable
+ public abstract Participant getTarget();
+
+ @Nullable
+ public LocalDateTime getTimestamp() {
+ return getMetadata().getCreatedAt();
+ }
+
+ @Override
+ public CloudEvent derive() {
+ return this;
+ }
+
+ @Value.Immutable
+ @JsonSerialize(as = ImmutableParticipant.class)
+ @JsonDeserialize(as = ImmutableParticipant.class)
+ public interface Participant {
+
+ @Nullable
+ UUID getGuid();
+
+ @Nullable
+ String getName();
+
+ @Nullable
+ String getType();
+
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudJob.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudJob.java
new file mode 100644
index 0000000000..431c2350ee
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudJob.java
@@ -0,0 +1,51 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudJob.class)
+@JsonDeserialize(as = ImmutableCloudJob.class)
+public abstract class CloudJob extends CloudEntity implements Derivable {
+
+ @Nullable
+ public abstract Status getStatus();
+
+ @Nullable
+ public abstract ErrorDetails getErrorDetails();
+
+ @Override
+ public CloudJob derive() {
+ return this;
+ }
+
+ public enum Status {
+
+ FAILED("failed"), FINISHED("finished"), QUEUED("queued"), RUNNING("running");
+
+ private final String value;
+
+ Status(String value) {
+ this.value = value;
+ }
+
+ public static Status fromString(String value) {
+ for (Status status : Status.values()) {
+ if (status.value.equals(value)) {
+ return status;
+ }
+ }
+ throw new IllegalArgumentException("Invalid job status: " + value);
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudMetadata.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudMetadata.java
new file mode 100644
index 0000000000..8565216bbf
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudMetadata.java
@@ -0,0 +1,35 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudMetadata.class)
+@JsonDeserialize(as = ImmutableCloudMetadata.class)
+public interface CloudMetadata {
+
+ @Nullable
+ @Value.Parameter
+ UUID getGuid();
+
+ @Nullable
+ LocalDateTime getCreatedAt();
+
+ @Nullable
+ LocalDateTime getUpdatedAt();
+
+ @Nullable
+ String getUrl();
+
+ static CloudMetadata defaultMetadata() {
+ return ImmutableCloudMetadata.builder()
+ .build();
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudOrganization.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudOrganization.java
new file mode 100644
index 0000000000..2e5231e7e2
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudOrganization.java
@@ -0,0 +1,18 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import org.immutables.value.Value;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudOrganization.class)
+@JsonDeserialize(as = ImmutableCloudOrganization.class)
+public abstract class CloudOrganization extends CloudEntity implements Derivable {
+
+ @Override
+ public CloudOrganization derive() {
+ return this;
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudPackage.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudPackage.java
new file mode 100644
index 0000000000..fe9ddd124d
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudPackage.java
@@ -0,0 +1,62 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+import org.immutables.value.Value;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.cloudfoundry.multiapps.controller.client.facade.Messages;
+import org.cloudfoundry.multiapps.controller.client.facade.Nullable;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudPackage.class)
+@JsonDeserialize(as = ImmutableCloudPackage.class)
+public abstract class CloudPackage extends CloudEntity implements Derivable {
+
+ @Nullable
+ public abstract Type getType();
+
+ @Nullable
+ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type")
+ @JsonSubTypes({ @JsonSubTypes.Type(name = "bits", value = BitsData.class),
+ @JsonSubTypes.Type(name = "docker", value = DockerData.class) })
+ public abstract PackageData getData();
+
+ @Nullable
+ public abstract Status getStatus();
+
+ @Override
+ public CloudPackage derive() {
+ return this;
+ }
+
+ public enum Type {
+ BITS, DOCKER;
+
+ @JsonCreator
+ public static Type from(String s) {
+ Objects.requireNonNull(s);
+ return Arrays.stream(Type.values())
+ .filter(type -> s.toLowerCase()
+ .equals(type.toString()))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException(String.format(Messages.UNKNOWN_PACKAGE_TYPE, s)));
+ }
+
+ @JsonValue
+ @Override
+ public String toString() {
+ return name().toLowerCase();
+ }
+ }
+
+ public interface PackageData {
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudProcess.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudProcess.java
new file mode 100644
index 0000000000..8176d4cf66
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudProcess.java
@@ -0,0 +1,37 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudProcess.class)
+@JsonDeserialize(as = ImmutableCloudProcess.class)
+public abstract class CloudProcess extends CloudEntity implements Derivable {
+
+ public abstract String getCommand();
+
+ public abstract Integer getDiskInMb();
+
+ public abstract Integer getInstances();
+
+ public abstract Integer getMemoryInMb();
+
+ public abstract HealthCheckType getHealthCheckType();
+
+ @Nullable
+ public abstract String getHealthCheckHttpEndpoint();
+
+ @Nullable
+ public abstract Integer getHealthCheckInvocationTimeout();
+
+ @Nullable
+ public abstract Integer getHealthCheckTimeout();
+
+ @Override
+ public CloudProcess derive() {
+ return this;
+ }
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudRoute.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudRoute.java
new file mode 100644
index 0000000000..c769706512
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudRoute.java
@@ -0,0 +1,83 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.List;
+import java.util.Objects;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudRoute.class)
+@JsonDeserialize(as = ImmutableCloudRoute.class)
+public abstract class CloudRoute extends CloudEntity implements Derivable {
+
+ @Value.Default
+ public int getAppsUsingRoute() {
+ return 0;
+ }
+
+ public abstract CloudDomain getDomain();
+
+ @Nullable
+ public abstract String getHost();
+
+ @Nullable
+ public abstract String getPath();
+
+ @Nullable
+ public abstract Integer getPort();
+
+ @Nullable
+ public abstract String getRequestedProtocol();
+
+ @Nullable
+ public abstract List getDestinations();
+
+ public abstract String getUrl();
+
+ @Override
+ public CloudRoute derive() {
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return getUrl();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getDomain().getName(), getHost(), getPath(), getPort());
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object == this) {
+ return true;
+ }
+ if (!(object instanceof CloudRoute)) {
+ return false;
+ }
+ var otherRoute = (CloudRoute) object;
+ var thisDomain = getDomain().getName();
+ var otherDomain = otherRoute.getDomain()
+ .getName();
+ // @formatter:off
+ return thisDomain.equals(otherDomain)
+ && areEmptyOrEqual(getHost(), otherRoute.getHost())
+ && areEmptyOrEqual(getPath(), otherRoute.getPath())
+ && Objects.equals(getPort(), otherRoute.getPort());
+ // @formatter:on
+ }
+
+ private static boolean areEmptyOrEqual(String lhs, String rhs) {
+ if (lhs == null || lhs.isEmpty()) {
+ return rhs == null || rhs.isEmpty();
+ }
+ return lhs.equals(rhs);
+ }
+
+}
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
new file mode 100644
index 0000000000..bceb94223b
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceBinding.java
@@ -0,0 +1,29 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.UUID;
+
+import org.cloudfoundry.Nullable;
+import org.immutables.value.Value;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudServiceBinding.class)
+@JsonDeserialize(as = ImmutableCloudServiceBinding.class)
+public abstract class CloudServiceBinding extends CloudEntity implements Derivable {
+
+ @Nullable
+ public abstract UUID getApplicationGuid();
+
+ public abstract UUID getServiceInstanceGuid();
+
+ @Nullable
+ public abstract ServiceCredentialBindingOperation getServiceBindingOperation();
+
+ @Override
+ public CloudServiceBinding derive() {
+ return this;
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceBroker.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceBroker.java
new file mode 100644
index 0000000000..bfc6170166
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceBroker.java
@@ -0,0 +1,31 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudServiceBroker.class)
+@JsonDeserialize(as = ImmutableCloudServiceBroker.class)
+public abstract class CloudServiceBroker extends CloudEntity implements Derivable {
+
+ @Nullable
+ public abstract String getUsername();
+
+ @Nullable
+ public abstract String getPassword();
+
+ @Nullable
+ public abstract String getUrl();
+
+ @Nullable
+ public abstract String getSpaceGuid();
+
+ @Override
+ public CloudServiceBroker derive() {
+ return this;
+ }
+
+}
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
new file mode 100644
index 0000000000..78db202828
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceInstance.java
@@ -0,0 +1,57 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.List;
+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;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.cloudfoundry.multiapps.controller.client.facade.Nullable;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudServiceInstance.class)
+@JsonDeserialize(as = ImmutableCloudServiceInstance.class)
+public abstract class CloudServiceInstance extends CloudEntity implements Derivable {
+
+ @Nullable
+ public abstract String getLabel();
+
+ @Nullable
+ public abstract String getPlan();
+
+ @Nullable
+ public abstract String getProvider();
+
+ @Nullable
+ public abstract String getBroker();
+
+ @Nullable
+ public abstract String getVersion();
+
+ @AllowNulls
+ public abstract Map getCredentials();
+
+ @Nullable
+ public abstract String getSyslogDrainUrl();
+
+ public abstract List getTags();
+
+ @Nullable
+ public abstract ServiceInstanceType getType();
+
+ @Nullable
+ public abstract ServiceOperation getLastOperation();
+
+ public boolean isUserProvided() {
+ return getType() != null && getType().equals(ServiceInstanceType.USER_PROVIDED);
+ }
+
+ @Override
+ public CloudServiceInstance derive() {
+ return this;
+ }
+
+}
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
new file mode 100644
index 0000000000..a77cad1967
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceKey.java
@@ -0,0 +1,32 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.Map;
+
+import org.cloudfoundry.AllowNulls;
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudServiceKey.class)
+@JsonDeserialize(as = ImmutableCloudServiceKey.class)
+public abstract class CloudServiceKey extends CloudEntity implements Derivable {
+
+ @Nullable
+ @AllowNulls
+ public abstract Map getCredentials();
+
+ @Nullable
+ public abstract CloudServiceInstance getServiceInstance();
+
+ @Nullable
+ public abstract ServiceCredentialBindingOperation getServiceKeyOperation();
+
+ @Override
+ public CloudServiceKey derive() {
+ return this;
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceOffering.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceOffering.java
new file mode 100644
index 0000000000..a7a29cec57
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServiceOffering.java
@@ -0,0 +1,48 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.List;
+import java.util.Map;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudServiceOffering.class)
+@JsonDeserialize(as = ImmutableCloudServiceOffering.class)
+public abstract class CloudServiceOffering extends CloudEntity implements Derivable {
+
+ @Nullable
+ public abstract Boolean isAvailable();
+
+ @Nullable
+ public abstract Boolean isBindable();
+
+ @Nullable
+ public abstract Boolean isShareable();
+
+ public abstract List getServicePlans();
+
+ @Nullable
+ public abstract String getDescription();
+
+ @Nullable
+ public abstract String getDocUrl();
+
+ @Nullable
+ public abstract Map getExtra();
+
+ @Nullable
+ public abstract String getBrokerId();
+
+ @Nullable
+ public abstract String getUniqueId();
+
+ @Override
+ public CloudServiceOffering derive() {
+ return this;
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServicePlan.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServicePlan.java
new file mode 100644
index 0000000000..02dddfb231
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudServicePlan.java
@@ -0,0 +1,39 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.Map;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudServicePlan.class)
+@JsonDeserialize(as = ImmutableCloudServicePlan.class)
+public abstract class CloudServicePlan extends CloudEntity implements Derivable {
+
+ @Nullable
+ public abstract String getDescription();
+
+ @Nullable
+ public abstract Map getExtra();
+
+ @Nullable
+ public abstract String getUniqueId();
+
+ @Nullable
+ public abstract String getServiceOfferingId();
+
+ @Nullable
+ public abstract Boolean isFree();
+
+ @Nullable
+ public abstract Boolean isPublic();
+
+ @Override
+ public CloudServicePlan derive() {
+ return this;
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudSpace.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudSpace.java
new file mode 100644
index 0000000000..7af5890352
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudSpace.java
@@ -0,0 +1,22 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudSpace.class)
+@JsonDeserialize(as = ImmutableCloudSpace.class)
+public abstract class CloudSpace extends CloudEntity implements Derivable {
+
+ @Nullable
+ public abstract CloudOrganization getOrganization();
+
+ @Override
+ public CloudSpace derive() {
+ return this;
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudStack.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudStack.java
new file mode 100644
index 0000000000..269dc5b6df
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudStack.java
@@ -0,0 +1,22 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudStack.class)
+@JsonDeserialize(as = ImmutableCloudStack.class)
+public abstract class CloudStack extends CloudEntity implements Derivable {
+
+ @Nullable
+ public abstract String getDescription();
+
+ @Override
+ public CloudStack derive() {
+ return this;
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudTask.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudTask.java
new file mode 100644
index 0000000000..f37f9d8f99
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudTask.java
@@ -0,0 +1,61 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+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.multiapps.controller.client.facade.domain.ImmutableCloudTask.ImmutableLimits;
+import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudTask.ImmutableResult;
+import org.immutables.value.Value;
+
+@Value.Enclosing
+@Value.Immutable
+@JsonSerialize(as = ImmutableCloudTask.class)
+@JsonDeserialize(as = ImmutableCloudTask.class)
+public abstract class CloudTask extends CloudEntity implements Derivable {
+
+ @Nullable
+ public abstract String getCommand();
+
+ @Nullable
+ public abstract Limits getLimits();
+
+ @Nullable
+ public abstract Result getResult();
+
+ @Nullable
+ public abstract State getState();
+
+ @Override
+ public CloudTask derive() {
+ return this;
+ }
+
+ public enum State {
+ PENDING, RUNNING, SUCCEEDED, CANCELING, FAILED
+ }
+
+ @Value.Immutable
+ @JsonSerialize(as = ImmutableResult.class)
+ @JsonDeserialize(as = ImmutableResult.class)
+ public interface Result {
+
+ @Nullable
+ @Value.Parameter
+ String getFailureReason();
+
+ }
+
+ @Value.Immutable
+ @JsonSerialize(as = ImmutableLimits.class)
+ @JsonDeserialize(as = ImmutableLimits.class)
+ public interface Limits {
+
+ @Nullable
+ Integer getDisk();
+
+ @Nullable
+ Integer getMemory();
+
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Derivable.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Derivable.java
new file mode 100644
index 0000000000..bcf68886e0
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Derivable.java
@@ -0,0 +1,7 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+public interface Derivable {
+
+ T derive();
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/DockerCredentials.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/DockerCredentials.java
new file mode 100644
index 0000000000..00530d05d3
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/DockerCredentials.java
@@ -0,0 +1,17 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import org.immutables.value.Value;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableDockerCredentials.class)
+@JsonDeserialize(as = ImmutableDockerCredentials.class)
+public interface DockerCredentials {
+
+ String getUsername();
+
+ String getPassword();
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/DockerData.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/DockerData.java
new file mode 100644
index 0000000000..d250baa17b
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/DockerData.java
@@ -0,0 +1,21 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+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 = ImmutableDockerData.class)
+@JsonDeserialize(as = ImmutableDockerData.class)
+public interface DockerData extends CloudPackage.PackageData {
+
+ String getImage();
+
+ @Nullable
+ String getUsername();
+
+ @Nullable
+ String getPassword();
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/DockerInfo.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/DockerInfo.java
new file mode 100644
index 0000000000..84491ce5ff
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/DockerInfo.java
@@ -0,0 +1,19 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableDockerInfo.class)
+@JsonDeserialize(as = ImmutableDockerInfo.class)
+public interface DockerInfo {
+
+ String getImage();
+
+ @Nullable
+ DockerCredentials getCredentials();
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/DropletInfo.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/DropletInfo.java
new file mode 100644
index 0000000000..22376f1a69
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/DropletInfo.java
@@ -0,0 +1,23 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.UUID;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableDropletInfo.class)
+@JsonDeserialize(as = ImmutableDropletInfo.class)
+public interface DropletInfo {
+
+ @Nullable
+ @Value.Parameter
+ UUID getGuid();
+
+ @Nullable
+ @Value.Parameter
+ UUID getPackageGuid();
+}
\ No newline at end of file
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ErrorDetails.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ErrorDetails.java
new file mode 100644
index 0000000000..de2672a9e8
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ErrorDetails.java
@@ -0,0 +1,25 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableErrorDetails.class)
+@JsonDeserialize(as = ImmutableErrorDetails.class)
+public interface ErrorDetails {
+
+ @Value.Default
+ default long getCode() {
+ return 0;
+ }
+
+ @Nullable
+ String getDescription();
+
+ @Nullable
+ String getErrorCode();
+
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000..6cd1a86842
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/HealthCheckType.java
@@ -0,0 +1,10 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+public enum HealthCheckType {
+ HTTP, PORT, PROCESS, @Deprecated NONE;
+
+ @Override
+ public String toString() {
+ return this.name().toLowerCase();
+ }
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstanceInfo.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstanceInfo.java
new file mode 100644
index 0000000000..f984108fff
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstanceInfo.java
@@ -0,0 +1,17 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import org.immutables.value.Value;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableInstanceInfo.class)
+@JsonDeserialize(as = ImmutableInstanceInfo.class)
+public interface InstanceInfo {
+
+ int getIndex();
+
+ InstanceState getState();
+
+}
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
new file mode 100644
index 0000000000..89d88d803d
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstanceState.java
@@ -0,0 +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) {
+ if (state == null) {
+ return UNKNOWN;
+ }
+ try {
+ return InstanceState.valueOf(state.getValue());
+ } catch (IllegalArgumentException e) {
+ return InstanceState.UNKNOWN;
+ }
+ }
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstancesInfo.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstancesInfo.java
new file mode 100644
index 0000000000..e569e52821
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/InstancesInfo.java
@@ -0,0 +1,21 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.immutables.value.Value;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableInstancesInfo.class)
+@JsonDeserialize(as = ImmutableInstancesInfo.class)
+public interface InstancesInfo {
+
+ @Value.Default
+ default List getInstances() {
+ return Collections.emptyList();
+ }
+
+}
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
new file mode 100644
index 0000000000..f448f2c668
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Lifecycle.java
@@ -0,0 +1,22 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+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.immutables.value.Value;
+
+import java.util.Map;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableLifecycle.class)
+@JsonDeserialize(as = ImmutableLifecycle.class)
+public interface Lifecycle {
+
+ LifecycleType getType();
+
+ @Nullable
+ @AllowNulls
+ Map getData();
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/LifecycleType.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/LifecycleType.java
new file mode 100644
index 0000000000..0fd6463d42
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/LifecycleType.java
@@ -0,0 +1,12 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+public enum LifecycleType {
+
+ BUILDPACK, DOCKER, KPACK, CNB;
+
+ public String toString() {
+ return this.name()
+ .toLowerCase();
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/PackageState.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/PackageState.java
new file mode 100644
index 0000000000..6079c7135a
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/PackageState.java
@@ -0,0 +1,5 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+public enum PackageState {
+ PENDING, STAGED, FAILED,
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/RouteDestination.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/RouteDestination.java
new file mode 100644
index 0000000000..b8b23d5093
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/RouteDestination.java
@@ -0,0 +1,30 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.UUID;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableRouteDestination.class)
+@JsonDeserialize(as = ImmutableRouteDestination.class)
+public abstract class RouteDestination extends CloudEntity implements Derivable {
+
+ public abstract UUID getApplicationGuid();
+
+ @Nullable
+ public abstract Integer getPort();
+
+ @Nullable
+ public abstract Integer getWeight();
+
+ public abstract String getProtocol();
+
+ @Override
+ public RouteDestination derive() {
+ return this;
+ }
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/SecurityGroupRule.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/SecurityGroupRule.java
new file mode 100644
index 0000000000..38047bfb58
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/SecurityGroupRule.java
@@ -0,0 +1,29 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableSecurityGroupRule.class)
+@JsonDeserialize(as = ImmutableSecurityGroupRule.class)
+public interface SecurityGroupRule {
+
+ String getProtocol();
+
+ String getPorts();
+
+ String getDestination();
+
+ @Nullable
+ Boolean getLog();
+
+ @Nullable
+ Integer getType();
+
+ @Nullable
+ Integer getCode();
+
+}
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
new file mode 100644
index 0000000000..4f202834cc
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceCredentialBindingOperation.java
@@ -0,0 +1,92 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.text.MessageFormat;
+import java.time.LocalDateTime;
+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;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.cloudfoundry.multiapps.controller.client.facade.Nullable;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableServiceCredentialBindingOperation.class)
+@JsonDeserialize(as = ImmutableServiceCredentialBindingOperation.class)
+public abstract class ServiceCredentialBindingOperation {
+
+ public abstract Type getType();
+
+ public abstract State getState();
+
+ @Nullable
+ public abstract String getDescription();
+
+ @Nullable
+ public abstract LocalDateTime getCreatedAt();
+
+ @Nullable
+ 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();
+ return ImmutableServiceCredentialBindingOperation.builder()
+ .type(ServiceCredentialBindingOperation.Type.fromString(lastOperationType))
+ .state(ServiceCredentialBindingOperation.State.fromString(lastOperationState))
+ .description(lastOperationDescription)
+ .createdAt(LocalDateTime.parse(lastOperationCreatedAt,
+ DateTimeFormatter.ISO_DATE_TIME))
+ .updatedAt(LocalDateTime.parse(lastOperationUpdatedAt,
+ DateTimeFormatter.ISO_DATE_TIME))
+ .build();
+ }
+
+ public enum Type {
+ CREATE, DELETE;
+
+ public static Type fromString(String value) {
+ return Arrays.stream(values())
+ .filter(type -> type.toString()
+ .equals(value))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException(MessageFormat.format("Illegal service binding operation type: \"{0}\"",
+ value)));
+ }
+
+ @Override
+ public String toString() {
+ return name().toLowerCase();
+ }
+ }
+
+ public enum State {
+ INITIAL("initial"), IN_PROGRESS("in progress"), SUCCEEDED("succeeded"), FAILED("failed");
+
+ private final String name;
+
+ State(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ public static State fromString(String value) {
+ return Arrays.stream(values())
+ .filter(state -> state.toString()
+ .equals(value))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException(MessageFormat.format("Illegal service binding state: \"{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
new file mode 100644
index 0000000000..151dae6bfa
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServiceOperation.java
@@ -0,0 +1,114 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.text.MessageFormat;
+import java.util.Objects;
+
+import org.cloudfoundry.client.v3.LastOperation;
+
+public class ServiceOperation {
+
+ public enum Type {
+
+ CREATE, UPDATE, DELETE;
+
+ @Override
+ public String toString() {
+ return name().toLowerCase();
+ }
+
+ public static Type fromString(String value) {
+ for (Type type : Type.values()) {
+ if (type.toString()
+ .equals(value)) {
+ return type;
+ }
+ }
+ throw new IllegalArgumentException(MessageFormat.format("Illegal service operation type: {0}", value));
+ }
+
+ }
+
+ public enum State {
+
+ SUCCEEDED("succeeded"), FAILED("failed"), IN_PROGRESS("in progress"), INITIAL("initial");
+
+ private final String name;
+
+ State(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ public static State fromString(String value) {
+ for (State state : State.values()) {
+ if (state.toString()
+ .equals(value)) {
+ return state;
+ }
+ }
+ throw new IllegalArgumentException(MessageFormat.format("Illegal service operation state: {0}", value));
+ }
+
+ }
+
+ private Type type;
+ private String description;
+ private State state;
+
+ ServiceOperation() {
+ // Required by Jackson.
+ }
+
+ public ServiceOperation(Type type, String description, State state) {
+ this.type = type;
+ this.description = description;
+ this.state = state;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public State getState() {
+ return state;
+ }
+
+ public static ServiceOperation fromLastOperation(LastOperation lastOperation) {
+ if (lastOperation == null || lastOperation.getType() == null || lastOperation.getState() == null) {
+ return null;
+ }
+ Type type = Type.fromString(lastOperation.getType());
+ State state = State.fromString(lastOperation.getState());
+ String description = lastOperation.getDescription();
+ return new ServiceOperation(type, description, state);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ ServiceOperation that = (ServiceOperation) o;
+ return type == that.type && state == that.state;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type, state);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s %s", type, state);
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServicePlanVisibility.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServicePlanVisibility.java
new file mode 100644
index 0000000000..4d550e1fad
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/ServicePlanVisibility.java
@@ -0,0 +1,11 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+public enum ServicePlanVisibility {
+
+ PUBLIC, ADMIN, ORGANIZATION;
+
+ @Override
+ public String toString() {
+ return name().toLowerCase();
+ }
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Staging.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Staging.java
new file mode 100644
index 0000000000..a613849536
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Staging.java
@@ -0,0 +1,77 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.List;
+
+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.multiapps.controller.client.facade.SkipNulls;
+import org.immutables.value.Value;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableStaging.class)
+@JsonDeserialize(as = ImmutableStaging.class)
+public interface Staging {
+ /**
+ * @return The buildpacks, or empty to use the default buildpack detected based on application content
+ */
+ @SkipNulls
+ List getBuildpacks();
+
+ /**
+ * @return The start command to use
+ */
+ @Nullable
+ String getCommand();
+
+ /**
+ * @return Raw, free-form information regarding a detected buildpack, or null if no detected buildpack was resolved. For example, if the
+ * application is stopped, the detected buildpack may be null.
+ */
+ @Nullable
+ String getDetectedBuildpack();
+
+ /**
+ * @return the health check timeout value
+ */
+ @Nullable
+ Integer getHealthCheckTimeout();
+
+ /**
+ * @return health check type
+ */
+ @Nullable
+ String getHealthCheckType();
+
+ /**
+ * @return health check http endpoint value
+ */
+ @Nullable
+ String getHealthCheckHttpEndpoint();
+
+ /**
+ * @return boolean value to see if ssh is enabled
+ */
+ @Nullable
+ Boolean isSshEnabled();
+
+ /**
+ * @return the stack to use when staging the application, or null to use the default stack
+ */
+ @Nullable
+ String getStackName();
+
+ @Nullable
+ DockerInfo getDockerInfo();
+
+ @Nullable
+ Integer getInvocationTimeout();
+
+ @Nullable
+ LifecycleType getLifecycleType();
+
+ default String getBuildpack() {
+ return getBuildpacks().isEmpty() ? null : getBuildpacks().get(0);
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Status.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Status.java
new file mode 100644
index 0000000000..bead1fdff0
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Status.java
@@ -0,0 +1,5 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+public enum Status {
+ AWAITING_UPLOAD, COPYING, EXPIRED, PROCESSING_UPLOAD, READY, FAILED,
+}
\ No newline at end of file
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Upload.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Upload.java
new file mode 100644
index 0000000000..bcd8f3afc9
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/Upload.java
@@ -0,0 +1,19 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+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;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableUpload.class)
+@JsonDeserialize(as = ImmutableUpload.class)
+public interface Upload {
+
+ Status getStatus();
+
+ @Nullable
+ ErrorDetails getErrorDetails();
+
+}
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
new file mode 100644
index 0000000000..3ee5e84386
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/UserRole.java
@@ -0,0 +1,35 @@
+package org.cloudfoundry.multiapps.controller.client.facade.domain;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.cloudfoundry.client.v3.roles.RoleType;
+
+public enum UserRole {
+
+ ORGANIZATION_AUDITOR,
+ ORGANIZATION_BILLING_MANAGER,
+ ORGANIZATION_MANAGER,
+ ORGANIZATION_USER,
+ SPACE_AUDITOR,
+ SPACE_DEVELOPER,
+ SPACE_MANAGER;
+
+ private static final Map NAMES_TO_VALUES = Arrays.stream(values())
+ .collect(Collectors.toMap(UserRole::getName,
+ roleType -> roleType));
+
+ public static UserRole fromRoleType(RoleType roleType) {
+ UserRole userRole = NAMES_TO_VALUES.get(roleType.getValue());
+ if (userRole == null) {
+ throw new IllegalArgumentException("Unknown user role: " + roleType.getValue());
+ }
+ return userRole;
+ }
+
+ public String getName() {
+ return name().toLowerCase();
+ }
+
+}
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
new file mode 100644
index 0000000000..e23244f2f4
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/dto/ApplicationToCreateDto.java
@@ -0,0 +1,40 @@
+package org.cloudfoundry.multiapps.controller.client.facade.dto;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.cloudfoundry.client.v3.Metadata;
+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.cloudfoundry.multiapps.controller.client.facade.domain.CloudRoute;
+import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableApplicationToCreateDto.class)
+@JsonDeserialize(as = ImmutableApplicationToCreateDto.class)
+public interface ApplicationToCreateDto {
+
+ String getName();
+
+ @Nullable
+ Staging getStaging();
+
+ @Nullable
+ Integer getDiskQuotaInMb();
+
+ @Nullable
+ Integer getMemoryInMb();
+
+ @Nullable
+ Metadata getMetadata();
+
+ @Nullable
+ Set getRoutes();
+
+ @Nullable
+ Map getEnv();
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/oauth2/OAuth2AccessTokenWithAdditionalInfo.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/oauth2/OAuth2AccessTokenWithAdditionalInfo.java
new file mode 100644
index 0000000000..79fa50da32
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/oauth2/OAuth2AccessTokenWithAdditionalInfo.java
@@ -0,0 +1,35 @@
+package org.cloudfoundry.multiapps.controller.client.facade.oauth2;
+
+import java.util.Map;
+
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+
+public class OAuth2AccessTokenWithAdditionalInfo {
+
+ private OAuth2AccessToken oAuth2AccessToken;
+ private Map additionalInfo;
+
+ public OAuth2AccessTokenWithAdditionalInfo(OAuth2AccessToken oAuth2AccessToken) {
+ this.oAuth2AccessToken = oAuth2AccessToken;
+ }
+
+ public OAuth2AccessTokenWithAdditionalInfo(OAuth2AccessToken oAuth2AccessToken, Map additionalInfo) {
+ this.oAuth2AccessToken = oAuth2AccessToken;
+ this.additionalInfo = additionalInfo;
+ }
+
+ public OAuth2AccessToken getOAuth2AccessToken() {
+ return oAuth2AccessToken;
+ }
+
+ public Map getAdditionalInfo() {
+ return additionalInfo;
+ }
+
+ public String getAuthorizationHeaderValue() {
+ return getOAuth2AccessToken().getTokenType()
+ .getValue()
+ + " " + getOAuth2AccessToken().getTokenValue();
+ }
+
+}
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
new file mode 100644
index 0000000000..79ccfe3e59
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/oauth2/OAuthClient.java
@@ -0,0 +1,135 @@
+package org.cloudfoundry.multiapps.controller.client.facade.oauth2;
+
+import java.net.URL;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.cloudfoundry.multiapps.controller.client.facade.Constants;
+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;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClientResponseException;
+import org.springframework.web.server.ResponseStatusException;
+
+import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials;
+import org.cloudfoundry.multiapps.controller.client.facade.adapters.OAuthTokenProvider;
+import reactor.util.retry.Retry;
+import reactor.util.retry.RetryBackoffSpec;
+
+/**
+ * Client that can handle authentication against a UAA instance
+ */
+public class OAuthClient {
+
+ private static final long MAX_RETRY_ATTEMPTS = 3;
+ private static final Duration RETRY_INTERVAL = Duration.ofSeconds(3);
+
+ private final URL authorizationUrl;
+ protected OAuth2AccessTokenWithAdditionalInfo token;
+ protected CloudCredentials credentials;
+ protected final WebClient webClient;
+ protected final TokenFactory tokenFactory;
+
+ public OAuthClient(URL authorizationUrl, WebClient webClient) {
+ this.authorizationUrl = authorizationUrl;
+ this.webClient = webClient;
+ this.tokenFactory = new TokenFactory();
+ }
+
+ public void init(CloudCredentials credentials) {
+ if (credentials != null) {
+ this.credentials = credentials;
+ if (credentials.getToken() != null) {
+ this.token = credentials.getToken();
+ } else {
+ this.token = createToken();
+ }
+ }
+ }
+
+ public void clear() {
+ this.token = null;
+ this.credentials = null;
+ }
+
+ public OAuth2AccessTokenWithAdditionalInfo getToken() {
+ if (token == null) {
+ return null;
+ }
+ if (shouldRefreshToken()) {
+ token = createToken();
+ }
+ return token;
+ }
+
+ public String getAuthorizationHeaderValue() {
+ OAuth2AccessTokenWithAdditionalInfo accessToken = getToken();
+ if (accessToken != null) {
+ return accessToken.getAuthorizationHeaderValue();
+ }
+ return null;
+ }
+
+ public TokenProvider getTokenProvider() {
+ return new OAuthTokenProvider(this);
+ }
+
+ private boolean shouldRefreshToken() {
+ return credentials.isRefreshable() && token.getOAuth2AccessToken()
+ .getExpiresAt()
+ .isBefore(Instant.now()
+ .plus(50, ChronoUnit.SECONDS));
+ }
+
+ protected OAuth2AccessTokenWithAdditionalInfo createToken() {
+ MultiValueMap formData = new LinkedMultiValueMap<>();
+ formData.add("grant_type", "password");
+ formData.add("client_id", credentials.getClientId());
+ formData.add("client_secret", credentials.getClientSecret());
+ formData.add("username", credentials.getEmail());
+ formData.add("password", credentials.getPassword());
+ formData.add("response_type", "token");
+ addLoginHintIfPresent(formData);
+
+ Oauth2AccessTokenResponse oauth2AccessTokenResponse = fetchOauth2AccessToken(formData);
+ return tokenFactory.createToken(oauth2AccessTokenResponse);
+ }
+
+ private void addLoginHintIfPresent(MultiValueMap formData) {
+ if (StringUtils.hasLength(credentials.getOrigin())) {
+ Map loginHintMap = new HashMap<>();
+ loginHintMap.put(Constants.ORIGIN_KEY, credentials.getOrigin());
+ formData.add("login_hint", JsonUtil.convertToJson(loginHintMap));
+ }
+ }
+
+ private Oauth2AccessTokenResponse fetchOauth2AccessToken(MultiValueMap formData) {
+ try {
+ return webClient.post()
+ .uri(authorizationUrl + "/oauth/token")
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
+ .body(BodyInserters.fromFormData(formData))
+ .retrieve()
+ .bodyToFlux(Oauth2AccessTokenResponse.class)
+ .retryWhen(Retry.fixedDelay(MAX_RETRY_ATTEMPTS, RETRY_INTERVAL)
+ .onRetryExhaustedThrow(this::throwOriginalError))
+ .blockFirst();
+ } catch (WebClientResponseException e) {
+ throw new ResponseStatusException(e.getStatusCode(), e.getMessage(), e);
+ }
+ }
+
+ private Throwable throwOriginalError(RetryBackoffSpec retrySpec, Retry.RetrySignal signal) {
+ return signal.failure();
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/oauth2/Oauth2AccessTokenResponse.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/oauth2/Oauth2AccessTokenResponse.java
new file mode 100644
index 0000000000..c38781e8cd
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/oauth2/Oauth2AccessTokenResponse.java
@@ -0,0 +1,32 @@
+package org.cloudfoundry.multiapps.controller.client.facade.oauth2;
+
+import org.immutables.value.Value;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+@Value.Immutable
+@JsonSerialize(as = ImmutableOauth2AccessTokenResponse.class)
+@JsonDeserialize(as = ImmutableOauth2AccessTokenResponse.class)
+public interface Oauth2AccessTokenResponse {
+
+ @JsonProperty("access_token")
+ String getAccessToken();
+
+ @JsonProperty("token_type")
+ String getTokenType();
+
+ @JsonProperty("id_token")
+ String getIdToken();
+
+ @JsonProperty("refresh_token")
+ String getRefreshToken();
+
+ @JsonProperty("expires_in")
+ long getExpiresIn();
+
+ String getScope();
+
+ String getJti();
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/oauth2/TokenFactory.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/oauth2/TokenFactory.java
new file mode 100644
index 0000000000..6d9f8c25e2
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/oauth2/TokenFactory.java
@@ -0,0 +1,83 @@
+package org.cloudfoundry.multiapps.controller.client.facade.oauth2;
+
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Base64.Decoder;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.web.server.ResponseStatusException;
+
+import org.cloudfoundry.multiapps.controller.client.facade.util.JsonUtil;
+
+public class TokenFactory {
+
+ private static final int JWT_TOKEN_PARTS_COUNT = 3;
+
+ // Scopes:
+ public static final String SCOPE_CC_READ = "cloud_controller.read";
+ public static final String SCOPE_CC_WRITE = "cloud_controller.write";
+ public static final String SCOPE_CC_ADMIN = "cloud_controller.admin";
+
+ // Token Body elements:
+ public static final String SCOPE = "scope";
+ public static final String EXPIRES_AT_KEY = "exp";
+ public static final String ISSUED_AT_KEY = "iat";
+ public static final String USER_NAME = "user_name";
+ public static final String USER_ID = "user_id";
+ public static final String CLIENT_ID = "client_id";
+
+ public OAuth2AccessTokenWithAdditionalInfo createToken(String tokenString) {
+ Map tokenInfo = parseToken(tokenString);
+ return createToken(tokenString, tokenInfo);
+ }
+
+ @SuppressWarnings("unchecked")
+ public OAuth2AccessTokenWithAdditionalInfo createToken(String tokenString, Map tokenInfo) {
+ List scope = (List) tokenInfo.get(SCOPE);
+ Number expiresAt = (Number) tokenInfo.get(EXPIRES_AT_KEY);
+ Number instantiatedAt = (Number) tokenInfo.get(ISSUED_AT_KEY);
+ if (scope == null || expiresAt == null || instantiatedAt == null) {
+ throw new IllegalStateException(MessageFormat.format("One or more of the following elements are missing from the token: \"{0}\"",
+ List.of(SCOPE, EXPIRES_AT_KEY, ISSUED_AT_KEY)));
+ }
+ return new OAuth2AccessTokenWithAdditionalInfo(createOAuth2AccessToken(tokenString, scope, expiresAt, instantiatedAt), tokenInfo);
+ }
+
+ private OAuth2AccessToken createOAuth2AccessToken(String tokenString, List scope, Number expiresAt, Number instantiatedAt) {
+ try {
+ return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
+ tokenString,
+ Instant.ofEpochSecond(instantiatedAt.longValue()),
+ Instant.ofEpochSecond(expiresAt.longValue()),
+ new HashSet<>(scope));
+ } catch (IllegalArgumentException e) {
+ throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, e.getMessage(), e);
+ }
+ }
+
+ private Map parseToken(String tokenString) {
+ String[] headerBodySignature = tokenString.split("\\.");
+ if (headerBodySignature.length != JWT_TOKEN_PARTS_COUNT) {
+ return Collections.emptyMap();
+ }
+ String body = decode(headerBodySignature[1]);
+ return JsonUtil.convertJsonToMap(body);
+ }
+
+ private String decode(String string) {
+ Decoder decoder = Base64.getUrlDecoder();
+ return new String(decoder.decode(string), StandardCharsets.UTF_8);
+ }
+
+ public OAuth2AccessTokenWithAdditionalInfo createToken(Oauth2AccessTokenResponse oauth2AccessTokenResponse) {
+ return createToken(oauth2AccessTokenResponse.getAccessToken());
+ }
+
+}
diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerResponseErrorHandler.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerResponseErrorHandler.java
new file mode 100644
index 0000000000..006e6051f4
--- /dev/null
+++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/rest/CloudControllerResponseErrorHandler.java
@@ -0,0 +1,76 @@
+package org.cloudfoundry.multiapps.controller.client.facade.rest;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException;
+import org.cloudfoundry.multiapps.controller.client.facade.util.CloudUtil;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.web.client.DefaultResponseErrorHandler;
+import org.springframework.web.client.RestClientException;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class CloudControllerResponseErrorHandler extends DefaultResponseErrorHandler {
+
+ private static CloudOperationException getException(ClientHttpResponse response) throws IOException {
+ HttpStatus statusCode = HttpStatus.valueOf(response.getStatusCode()
+ .value());
+ String statusText = response.getStatusText();
+
+ ObjectMapper mapper = new ObjectMapper(); // can reuse, share globally
+
+ if (response.getBody() != null) {
+ try {
+ @SuppressWarnings("unchecked") Map responseBody = mapper.readValue(response.getBody(), Map.class);
+ String description = getTrimmedDescription(responseBody);
+ return new CloudOperationException(statusCode, statusText, description);
+ } catch (IOException e) {
+ // Fall through. Handled below.
+ }
+ }
+ return new CloudOperationException(statusCode, statusText);
+ }
+
+ private static String getTrimmedDescription(Map responseBody) {
+ String description = getDescription(responseBody);
+ return description == null ? null : description.trim();
+ }
+
+ private static String getDescription(Map responseBody) {
+ String description = getV2Description(responseBody);
+ return description == null ? getV3Description(responseBody) : description;
+ }
+
+ private static String getV2Description(Map responseBody) {
+ return CloudUtil.parse(String.class, responseBody.get("description"));
+ }
+
+ @SuppressWarnings("unchecked")
+ private static String getV3Description(Map responseBody) {
+ List