Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.sap.cloud.mt.subscription.ServiceManager Maven / Gradle / Ivy
/******************************************************************************
* © 2020 SAP SE or an SAP affiliate company. All rights reserved. *
******************************************************************************/
package com.sap.cloud.mt.subscription;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sap.cloud.mt.subscription.exceptions.InternalError;
import com.sap.cloud.mt.tools.api.QueryParameter;
import com.sap.cloud.mt.tools.api.ServiceCall;
import com.sap.cloud.mt.tools.api.ServiceEndpoint;
import com.sap.cloud.mt.tools.api.ServiceResponse;
import com.sap.cloud.mt.tools.exception.InternalException;
import com.sap.cloud.mt.tools.exception.ServiceException;
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultOAuth2PropertySupplier;
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2ServiceBindingDestinationLoader;
import com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf;
import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationLoader;
import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions;
import com.sap.cloud.sdk.cloudplatform.thread.DefaultThreadContext;
import com.sap.cloud.sdk.cloudplatform.thread.Property;
import com.sap.cloud.sdk.cloudplatform.thread.ThreadContextAccessor;
import com.sap.cloud.sdk.cloudplatform.thread.ThreadContextExecutor;
import com.sap.cloud.sdk.cloudplatform.thread.exception.ThreadContextExecutionException;
import com.sap.cloud.security.config.ClientCertificate;
import com.sap.cloud.security.config.ClientCredentials;
import com.sap.cloud.security.config.ClientIdentity;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import static com.sap.cloud.mt.subscription.Tools.lazyJson;
import static com.sap.cloud.mt.subscription.Tools.waitSomeTime;
import static org.apache.http.HttpStatus.SC_ACCEPTED;
import static org.apache.http.HttpStatus.SC_BAD_GATEWAY;
import static org.apache.http.HttpStatus.SC_CREATED;
import static org.apache.http.HttpStatus.SC_GATEWAY_TIMEOUT;
import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.apache.http.HttpStatus.SC_NOT_FOUND;
import static org.apache.http.HttpStatus.SC_OK;
import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE;
public class ServiceManager {
public static final String SM_URL = "sm_url";
public static final String TOKEN = "token";
public static final Map CALLED_FROM;
public static final String COM_SAP_CLOUD_MT_SM_CALLED = "com.sap.cloud.mt.sm-called";
static {
ServiceManagerPropertySupplier.initialize();
CALLED_FROM = Map.of("Client-Name", "cap-java-client", "Client-Version", "3.0.0");
}
private static final Logger logger = LoggerFactory.getLogger(ServiceManager.class);
private static final String SERVICE_INSTANCES_ENDPOINT = "/v1/service_instances";
private static final String SERVICE_BINDINGS_ENDPOINT = "/v1/service_bindings";
private static final String SERVICE_PLANS_ENDPOINT = "/v1/service_plans";
private static final String SERVICE_OFFERINGS_ENDPOINT = "/v1/service_offerings";
private static final String FIELD_QUERY = "fieldQuery";
private static final String LABEL_QUERY = "labelQuery";
private static final String ID = "id";
private static final String ITEMS = "items";
public static final String UNEXPECTED_RETURN_CODE = "unexpected return code %d";
public static final String ASYNC = "async";
public static final String SUCCEEDED = "succeeded";
public static final String IN_PROGRESS = "in progress";
public static final String FAILED = "failed";
public static final String LOCATION = "location";
public static final String INSTANCE_ID_IS_EMPTY = "Instance id is empty";
public static final String TENANT_ID = "tenant_id";
public static final String MANAGING_CLIENT_LIB = "managing_client_lib";
public static final String SERVICE_PLAN_ID = "service_plan_id";
public static final String INSTANCE_MANAGER_CLIENT_LIB = "instance-manager-client-lib";
public static final String TENANT_ID_IS_EMPTY = "Tenant id is empty";
public static final String ATTACH_LAST_OPERATIONS = "attach_last_operations";
private final ObjectMapper mapper = new ObjectMapper();
private final ConcurrentHashMap planIdMap = new ConcurrentHashMap<>();
private final Set retryCodes = new HashSet<>();
private final ServiceEndpoint instancesEndpoint;
private final ServiceEndpoint bindingsEndpoint;
private final ServiceEndpoint oneInstanceEndpoint;
private final ServiceEndpoint oneBindingEndpoint;
private final ServiceEndpoint instancesAsyncEndpoint;
private final ServiceEndpoint bindingsAsyncEndpoint;
private final ServiceEndpoint plansEndpoint;
private final ServiceEndpoint offeringsEndpoint;
private final ServiceEndpoint locationEndpoint;
private final String serviceOfferingName;
private final String planName;
private final ServiceSpecification serviceSpecification;
private final String serviceInstanceName;
public ServiceManager(com.sap.cloud.environment.servicebinding.api.ServiceBinding serviceBinding, ServiceSpecification serviceSpecification, String serviceOfferingName, String planName) throws InternalError {
this.serviceOfferingName = serviceOfferingName;
this.planName = planName;
this.serviceSpecification = serviceSpecification;
this.serviceInstanceName = serviceBinding.getName().orElseThrow(() -> new InternalError("Service instance name is missing"));
retryCodes.add(SC_BAD_GATEWAY);
retryCodes.add(SC_GATEWAY_TIMEOUT);
retryCodes.add(SC_INTERNAL_SERVER_ERROR);
retryCodes.add(SC_SERVICE_UNAVAILABLE);
var destination = getDestination(serviceBinding);
try {
offeringsEndpoint = createEndpoint(destination, SERVICE_OFFERINGS_ENDPOINT, new HashSet<>(Arrays.asList(SC_OK)));
plansEndpoint = createEndpoint(destination, SERVICE_PLANS_ENDPOINT, new HashSet<>(Arrays.asList(SC_OK)));
instancesEndpoint = createEndpoint(destination, SERVICE_INSTANCES_ENDPOINT, new HashSet<>(Arrays.asList(SC_OK, SC_CREATED)));
oneInstanceEndpoint = createEndpoint(destination, SERVICE_INSTANCES_ENDPOINT, new HashSet<>(Arrays.asList(SC_OK, SC_NOT_FOUND)));
bindingsEndpoint = createEndpoint(destination, SERVICE_BINDINGS_ENDPOINT, new HashSet<>(Arrays.asList(SC_OK, SC_CREATED)));
oneBindingEndpoint = createEndpoint(destination, SERVICE_BINDINGS_ENDPOINT, new HashSet<>(Arrays.asList(SC_OK, SC_NOT_FOUND)));
instancesAsyncEndpoint = createEndpoint(destination, SERVICE_INSTANCES_ENDPOINT, new HashSet<>(Arrays.asList(SC_ACCEPTED)));
bindingsAsyncEndpoint = createEndpoint(destination, SERVICE_BINDINGS_ENDPOINT, new HashSet<>(Arrays.asList(SC_ACCEPTED)));
locationEndpoint = createEndpoint(destination, "", new HashSet<>(Arrays.asList(SC_OK)));
} catch (InternalException e) {
throw new InternalError(e);
}
}
/**
* Read all instances.
*
* @return List of service instances.
* @throws InternalError
*/
public List readInstances() throws InternalError {
return readInstances(null, null);
}
/**
* Read a single instance for one tenant.
*
* @param tenantId Tenant id for which instances are read
* @return Service instance
* @throws InternalError
*/
public Optional readInstanceForTenant(String tenantId) throws InternalError {
if (StringUtils.isBlank(tenantId)) {
throw new InternalError(TENANT_ID_IS_EMPTY);
}
return extractServiceInstance(readInstancesMaps(tenantId, null),
"Multiple instances found for tenant id %s".formatted(tenantId));
}
/**
* Read a single service instance via its service instance id.
*
* @param instanceId Service instance id
* @return Service instance
* @throws InternalError
*/
public Optional readInstance(String instanceId) throws InternalError {
if (StringUtils.isBlank(instanceId)) {
throw new InternalError(INSTANCE_ID_IS_EMPTY);
}
return extractServiceInstance(readInstancesMaps(null, instanceId),
"Multiple instances found for instance id %s".formatted(instanceId));
}
/**
* Read all bindings
*
* @return List of bindings.
* @throws InternalError
*/
public List readBindings() throws InternalError {
return readBindings(null, null, null);
}
/**
* Read bindings for one tenant.
*
* @param tenantId Tenant id for which instances are read
* @return List of bindings.
* @throws InternalError
*/
public List readBindingsForTenant(String tenantId) throws InternalError {
if (StringUtils.isBlank(tenantId)) {
throw new InternalError(TENANT_ID_IS_EMPTY);
}
return readBindings(tenantId, null, null);
}
/**
* Read bindings for service instance id
*
* @param instanceId Service instance id
* @return List of service bindings.
* @throws InternalError
*/
public List readBindingsForInstance(String instanceId) throws InternalError {
if (StringUtils.isBlank(instanceId)) {
throw new InternalError(INSTANCE_ID_IS_EMPTY);
}
return readBindings(null, instanceId, null);
}
/**
* Read a single service binding via its service binding id.
*
* @param bindingId Service binding id
* @return Service binding.
* @throws InternalError
*/
public Optional readBinding(String bindingId) throws InternalError {
return extractServiceBinding(readBindingsMaps(null, null, bindingId),
"Multiple bindings found for binding id %s".formatted(bindingId));
}
/**
* Create a service instance for a tenant.
*
* @param tenantId Tenant id for which service instance is created
* @param parameters Map of instance creation parameters
* @return Created instance
* @throws InternalError
*/
public Optional createInstance(String tenantId, ProvisioningParameters parameters) throws InternalError {
if (StringUtils.isBlank(tenantId)) {
throw new InternalError(TENANT_ID_IS_EMPTY);
}
var labels = new HashMap>();
labels.put(TENANT_ID, Arrays.asList(tenantId));
var payload = new CreateInstancePayload(getPlanId(), calculateServiceInstanceName(tenantId), parameters, labels);
var query = Arrays.asList(new QueryParameter(ASYNC, "true"));
try {
ServiceCall createInstances = instancesAsyncEndpoint.createServiceCall()
.http()
.post()
.payload(payload)
.noPathParameter()
.query(query)
.enhancer(serviceSpecification.getRequestEnhancer())
.insertHeaderFields(CALLED_FROM)
.end();
logger.debug("Call service manager to create service instances for tenant id {} and payload {}",
tenantId, lazyJson(() -> payload));
ServiceResponse response = callWithNewThreadContext(() -> createInstances.execute(Map.class));
var locationHeader = Arrays.stream(response.getHeaders()).filter(h -> LOCATION.equals(h.getName())).findFirst();
if (!locationHeader.isPresent()) {
throw new InternalError("No location header returned for asynchronous create instance operation");
}
var locationUrl = locationHeader.get().getValue();
var instanceId = waitForCompletionAndGetId(locationUrl, serviceSpecification.getPolling());
return readInstance(instanceId);
} catch (InternalException | ServiceException e) {
throw serviceErrorHandling(e);
}
}
/**
* Delete service instance
*
* @param instanceId Service instance id
* @return Service instance id
* @throws InternalError
*/
public String deleteInstance(String instanceId) throws InternalError {
if (StringUtils.isBlank(instanceId)) {
throw new InternalError(INSTANCE_ID_IS_EMPTY);
}
var query = Arrays.asList(new QueryParameter(ASYNC, "true"));
try {
ServiceCall deleteInstance = instancesAsyncEndpoint.createServiceCall()
.http()
.delete()
.withoutPayload()
.pathParameter(instanceId)
.query(query)
.enhancer(serviceSpecification.getRequestEnhancer())
.insertHeaderFields(CALLED_FROM)
.end();
logger.debug("Call service manager to delete service instance {} ", instanceId);
ServiceResponse response = callWithNewThreadContext(() -> deleteInstance.execute(Map.class));
var locationHeader = Arrays.stream(response.getHeaders()).filter(h -> LOCATION.equals(h.getName())).findFirst();
if (!locationHeader.isPresent()) {
throw new InternalError("No location header returned for asynchronous delete instance operation");
}
var locationUrl = locationHeader.get().getValue();
return waitForCompletionAndGetId(locationUrl, serviceSpecification.getPolling());
} catch (InternalException | ServiceException e) {
throw serviceErrorHandling(e);
}
}
/**
* Create a service binding for a tenant and a service instance.
*
* @param tenantId Tenant id for which binding is created
* @param serviceInstanceId Service instance id for which binding is created
* @param parameters Binding parameters
* @return Service binding
* @throws InternalError
*/
public Optional createBinding(String tenantId, String serviceInstanceId, BindingParameters parameters) throws InternalError {
if (StringUtils.isBlank(tenantId)) {
throw new InternalError(TENANT_ID_IS_EMPTY);
}
if (StringUtils.isBlank(serviceInstanceId)) {
throw new InternalError("Service instance id is empty");
}
var labels = new HashMap>();
labels.put(TENANT_ID, Arrays.asList(tenantId));
labels.put(MANAGING_CLIENT_LIB, Arrays.asList(INSTANCE_MANAGER_CLIENT_LIB));
labels.put(SERVICE_PLAN_ID, Arrays.asList(getPlanId()));
var payload = new CreateBindingPayload(serviceInstanceId, UUID.randomUUID().toString(), parameters, labels);
var query = Arrays.asList(new QueryParameter(ASYNC, "true"));
try {
ServiceCall createBindings = bindingsAsyncEndpoint.createServiceCall()
.http()
.post()
.payload(payload)
.noPathParameter()
.query(query)
.enhancer(serviceSpecification.getRequestEnhancer())
.insertHeaderFields(CALLED_FROM)
.end();
logger.debug("Call service manager to create new binding with payload {}", lazyJson(() -> payload));
ServiceResponse response = callWithNewThreadContext(() -> createBindings.execute(Map.class));
var locationHeader = Arrays.stream(response.getHeaders()).filter(h -> LOCATION.equals(h.getName())).findFirst();
if (!locationHeader.isPresent()) {
throw new InternalError("No location header returned for asynchronous create binding operation");
}
var locationUrl = locationHeader.get().getValue();
var bindingId = waitForCompletionAndGetId(locationUrl, serviceSpecification.getPolling());
return readBinding(bindingId);
} catch (InternalException | ServiceException e) {
throw serviceErrorHandling(e);
}
}
/**
* Delete service binding
*
* @param bindingId Service binding id
* @return Service binding id
* @throws InternalError
*/
public String deleteBinding(String bindingId) throws InternalError {
if (StringUtils.isBlank(bindingId)) {
throw new InternalError("Binding id is empty");
}
var query = Arrays.asList(new QueryParameter(ASYNC, "true"));
try {
ServiceCall deleteBindings = bindingsAsyncEndpoint.createServiceCall()
.http()
.delete()
.withoutPayload()
.pathParameter(bindingId)
.query(query)
.enhancer(serviceSpecification.getRequestEnhancer())
.insertHeaderFields(CALLED_FROM)
.end();
logger.debug("Call service manager to delete binding {}", bindingId);
ServiceResponse response = callWithNewThreadContext(() -> deleteBindings.execute(Map.class));
var locationHeader = Arrays.stream(response.getHeaders()).filter(h -> LOCATION.equals(h.getName())).findFirst();
if (!locationHeader.isPresent()) {
throw new InternalError("No location header returned for asynchronous delete binding operation");
}
var locationUrl = locationHeader.get().getValue();
return waitForCompletionAndGetId(locationUrl, serviceSpecification.getPolling());
} catch (InternalException | ServiceException e) {
throw serviceErrorHandling(e);
}
}
/**
* Destination name.
*
* @return destination name
*/
public String getServiceInstanceName() {
return serviceInstanceName;
}
/**
* Read service instances
*
* @param tenantId Tenant id
* @param instanceId Instance id
* @return List of service instances
* @throws InternalError
*/
private List readInstances(String tenantId, String instanceId) throws InternalError {
var result = new ArrayList();
readInstancesMaps(tenantId, instanceId).stream().forEach(map -> result.add(new ServiceInstance(map)));
return result;
}
/**
* Read service instances
*
* @param tenantId Tenant id for which instances are read, can be empty
* @param instanceId Service instance id, can be empty
* @return List of service instances as map.
* @throws InternalError
*/
private List> readInstancesMaps(String tenantId, String instanceId) throws InternalError {
return readInstancesMapsInt(tenantId, instanceId, Optional.empty());
}
private List> readInstancesMapsInt(String tenantId, String instanceId, Optional token) throws InternalError {
var query = new ArrayList();
token.ifPresent(t -> query.add(new QueryParameter(TOKEN, t)));
query.add(new QueryParameter(FIELD_QUERY, "service_plan_id eq '%s'".formatted(getPlanId())));
query.add(new QueryParameter(ATTACH_LAST_OPERATIONS, "true"));
if (StringUtils.isNotBlank(tenantId)) {
query.add(new QueryParameter(LABEL_QUERY, "tenant_id eq '%s'".formatted(tenantId)));
}
try {
ServiceCall getInstances;
if (StringUtils.isNotBlank(instanceId)) {
getInstances = oneInstanceEndpoint.createServiceCall()
.http()
.get()
.withoutPayload()
.pathParameter(instanceId)
.query(query)
.enhancer(serviceSpecification.getRequestEnhancer())
.insertHeaderFields(CALLED_FROM)
.end();
logger.debug("Call service manager to determine service instance with instance id {}", instanceId);
} else {
getInstances = instancesEndpoint.createServiceCall()
.http()
.get()
.withoutPayload()
.noPathParameter()
.query(query)
.enhancer(serviceSpecification.getRequestEnhancer())
.insertHeaderFields(CALLED_FROM)
.end();
if (StringUtils.isNotBlank(tenantId)) {
logger.debug("Call service manager to determine service instances with tenant id {}", tenantId);
} else {
logger.debug("Call service manager to determine all service instances");
}
}
ServiceResponse response = callWithNewThreadContext(() -> getInstances.execute(Map.class)); //NOSONAR
if (StringUtils.isNotBlank(instanceId)) {
if (response.getHttpStatusCode() == HttpStatus.SC_NOT_FOUND || response.getPayload().isEmpty()) {
return new ArrayList<>();
} else {
return Arrays.asList(response.getPayload().orElse(new HashMap<>()));
}
} else {
var instances = getItems(response.getPayload().orElse(new HashMap<>()));
var tokenOpt = response.getPayload().map(p -> (String) p.get(TOKEN));
if (tokenOpt.isPresent()) {
instances.addAll(readInstancesMapsInt(tenantId, null, tokenOpt));
}
return instances;
}
} catch (InternalException | ServiceException e) {
throw serviceErrorHandling(e);
}
}
/**
* Read bindings.
*
* @param tenantId Tenant id
* @param instanceId Instance id
* @param bindingId Binding id
* @return List of service bindings
* @throws InternalError
*/
private List readBindings(String tenantId, String instanceId, String bindingId) throws InternalError {
var bindings = new ArrayList();
readBindingsMaps(tenantId, instanceId, bindingId).stream().forEach(map -> bindings.add(new ServiceBinding(map)));
return bindings;
}
/**
* Read service bindings for a tenant
*
* @param tenantId Tenant id for which service binding was created, can be empty
* @param bindingId Service binding id, can be empty
* @param instanceId Service instance id, can be empty
* @return List of service bindings as map.
* @throws InternalError
*/
private List> readBindingsMaps(String tenantId, String instanceId, String bindingId) throws InternalError {
return readBindingsMapsInt(tenantId, instanceId, bindingId, Optional.empty());
}
private List> readBindingsMapsInt(String tenantId, String instanceId, String bindingId,
Optional token) throws InternalError {
try {
var query = new ArrayList();
token.ifPresent(t -> query.add(new QueryParameter(TOKEN, t)));
query.add(new QueryParameter(ATTACH_LAST_OPERATIONS, "true"));
if (StringUtils.isNotBlank(instanceId)) {
query.add(new QueryParameter(FIELD_QUERY, "service_instance_id eq '%s'".formatted(instanceId)));
}
// The parameter managing_client_lib was introduced by the instance manager client lib. This lib sets this
// parameter and use it as filter. To keep compatibility it is set here, too.
if (StringUtils.isNotBlank(tenantId)) {
query.add(new QueryParameter(LABEL_QUERY,
"service_plan_id eq '%s' and managing_client_lib eq 'instance-manager-client-lib' and tenant_id eq '%s'".formatted(getPlanId(), tenantId)));
} else {
query.add(new QueryParameter(LABEL_QUERY,
"service_plan_id eq '%s' and managing_client_lib eq 'instance-manager-client-lib'".formatted(getPlanId())));
}
ServiceCall getBindings;
if (StringUtils.isNotBlank(bindingId)) {
getBindings = oneBindingEndpoint.createServiceCall()
.http()
.get()
.withoutPayload()
.pathParameter(bindingId)
.query(query)
.enhancer(serviceSpecification.getRequestEnhancer())
.insertHeaderFields(CALLED_FROM)
.end();
} else {
getBindings = bindingsEndpoint.createServiceCall()
.http()
.get()
.withoutPayload()
.noPathParameter()
.query(query)
.enhancer(serviceSpecification.getRequestEnhancer())
.insertHeaderFields(CALLED_FROM)
.end();
}
logger.debug("Call service manager to determine service bindings for tenant id {} instance id {} binding id {}",
tenantId, instanceId, bindingId);
ServiceResponse response = callWithNewThreadContext(() -> getBindings.execute(Map.class));
if (StringUtils.isNotBlank(bindingId)) {
if (response.getHttpStatusCode() == SC_NOT_FOUND || response.getPayload().isEmpty()) {
return new ArrayList<>();
} else {
return Arrays.asList(response.getPayload().orElse(new HashMap<>()));
}
} else {
var bindings = getItems(response.getPayload().orElse(new HashMap<>()));
var tokenOpt = response.getPayload().map(p -> (String) p.get(TOKEN));
if (tokenOpt.isPresent()) {
bindings.addAll(readBindingsMapsInt(tenantId, instanceId, null, tokenOpt));
}
return bindings;
}
} catch (InternalException | ServiceException e) {
throw serviceErrorHandling(e);
}
}
private String readOfferingId(String offeringName) throws InternalError {
String offeringId = "";
try {
ServiceCall getOfferings = offeringsEndpoint.createServiceCall()
.http()
.get()
.withoutPayload()
.noPathParameter()
.query(Arrays.asList(new QueryParameter(FIELD_QUERY, "catalog_name eq '%s'".formatted(offeringName))))
.enhancer(serviceSpecification.getRequestEnhancer())
.insertHeaderFields(CALLED_FROM)
.end();
logger.debug("Call service manager to determine service offerings for {}", offeringName);
ServiceResponse response = callWithNewThreadContext(() -> getOfferings.execute(Map.class));
offeringId = getIdFromItems(response, "No service offering found for %s ".formatted(offeringName),
"Multiple offerings found for %s".formatted(offeringName),
"No service offering id is contained in payload");
} catch (InternalException | ServiceException e) {
throw serviceErrorHandling(e);
}
if (StringUtils.isBlank(offeringId)) {
throw new InternalError("Could not determine offering id for %s".formatted(offeringName));
}
return offeringId;
}
private String readPlanId(String serviceOfferingId) throws InternalError {
String planId = "";
try {
ServiceCall getPlans = plansEndpoint.createServiceCall()
.http()
.get()
.withoutPayload()
.noPathParameter()
.query(Arrays.asList(new QueryParameter(FIELD_QUERY, "catalog_name eq '%s' and service_offering_id eq '%s'".formatted(planName, serviceOfferingId))))
.enhancer(serviceSpecification.getRequestEnhancer())
.insertHeaderFields(CALLED_FROM)
.end();
logger.debug("Call service manager to determine plan id for {}", planName);
ServiceResponse response = callWithNewThreadContext(() -> getPlans.execute(Map.class));
planId = getIdFromItems(response, "No service plan found for %s".formatted(planName),
"Multiple plans found for %s".formatted(planName),
"No service plan id is contained in payload");
} catch (InternalException | ServiceException e) {
throw serviceErrorHandling(e);
}
if (StringUtils.isBlank(planId)) {
throw new InternalError("Could not determine plan id for %s".formatted(planName));
}
return planId;
}
/**
* Extract the content of field id in the items section
*
* @param response Response from service call
* @param noItemsText Error text for no items case
* @param toManyItemsText Error text for more than one item
* @param noIdText Error text if not id can be found
* @return The content of the id field in the items section
* @throws InternalError
*/
private String getIdFromItems(ServiceResponse response, String noItemsText, String toManyItemsText, String noIdText) throws InternalError {
List> items = getItems(response.getPayload().orElse(new HashMap()));
if (items.isEmpty()) {
throw new InternalError(noItemsText);
}
if (items.size() > 1) {
throw new InternalError(toManyItemsText);
}
Map item = items.get(0);
if (item.get(ID) == null) {
throw new InternalError(noIdText);
}
return (String) item.get(ID);
}
/**
* Get items section from map.
*
* @param payload The response of a service call containing an items section.
* @return Items section.
*/
private List> getItems(Map payload) {
if (payload.containsKey(ITEMS)) {
return ((List>) payload.get(ITEMS));
} else {
return new ArrayList<>();
}
}
/**
* Lazy determination of plan id. Thread safety must be guaranteed.
*
* @return Service plan id determined from service offering name.
* @throws InternalError
*/
private String getPlanId() throws InternalError {
try {
return planIdMap.computeIfAbsent(serviceOfferingName, key -> {
try {
String offeringId = readOfferingId(serviceOfferingName);
return readPlanId(offeringId);
} catch (InternalError error) {
throw new DeterminationError("Could not determine offering id", error);
}
});
} catch (Exception e) {
throw new InternalError(e);
}
}
/**
* Poll the operation endpoint until the operation is completed or the timeout is reached.
*
* @param path Location path returned by the triggered asynchronous operation
* @param pollingParameter Polling configuration
* @return id of processed entity
* @throws InternalError
*/
private String waitForCompletionAndGetId(String path, PollingParameters pollingParameter) throws InternalError {
Instant start = Instant.now();
while (true) {
logger.debug("Wait for completion of operation {}", path);
var result = getOperationResult(path);
if (result.isReady() && !IN_PROGRESS.equals(result.getState())) {
var state = result.getState();
if (SUCCEEDED.equals(state)) {
if (StringUtils.isBlank(result.getResourceId())) {
throw new InternalError("No id returned");
}
return result.getResourceId();
} else {
String errorJson = "";
if (result.getErrors() != null) {
try {
errorJson = mapper.writeValueAsString(result.getErrors());
} catch (JsonProcessingException e) {
errorJson = "";
}
}
throw new InternalError("Operation failed with state %s and error %s".formatted(state, errorJson));
}
}
if (Duration.between(start, Instant.now()).compareTo(pollingParameter.getRequestTimeout()) >= 0) {
throw new InternalError("Maximum waiting time on operation %s exceeded".formatted(path));
}
waitSomeTime(pollingParameter.getInterval());
}
}
/**
* Read result of an asynchronous operation.
*
* @param path Location path returned by the triggered asynchronous operation
* @return Result of operation call.
* @throws InternalError
*/
private ServiceOperation getOperationResult(String path) throws InternalError {
try {
ServiceCall getOperationResult = locationEndpoint.createServiceCall()
.http()
.get()
.withoutPayload()
.pathParameter(path)
.noQuery()
.enhancer(serviceSpecification.getRequestEnhancer())
.insertHeaderFields(CALLED_FROM)
.end();
logger.debug("Call service manager to determine operation status for {}", path);
ServiceResponse response = callWithNewThreadContext(() -> getOperationResult.execute(Map.class));
return new ServiceOperation(response.getPayload().orElse(new HashMap()));
} catch (InternalException | ServiceException e) {
throw serviceErrorHandling(e);
}
}
private ServiceEndpoint createEndpoint(HttpDestination destination, String path, Set expectedResponseCodes) throws InternalException {
return ServiceEndpoint.create()
.destination(destination)
.path(path)
.returnCodeChecker(c -> {
if (!expectedResponseCodes.contains(c)) {
return new InternalError(UNEXPECTED_RETURN_CODE.formatted(c));
}
return null;
}).retry().forReturnCodes(retryCodes)
.config(serviceSpecification.getResilienceConfig())
.end();
}
private InternalError serviceErrorHandling(Exception e) {
if (e.getCause() instanceof InternalError internalError) {
return internalError;
}
return new InternalError(e);
}
private Optional extractServiceInstance(List> instances, String tooManyErrorText) throws InternalError {
var instanceData = extractSingleItem(instances, tooManyErrorText);
return !instanceData.isEmpty() ? Optional.of(new ServiceInstance(instanceData)) : Optional.empty();
}
private Optional extractServiceBinding(List> bindings, String tooManyErrorText) throws InternalError {
var bindingData = extractSingleItem(bindings, tooManyErrorText);
return !bindingData.isEmpty() ? Optional.of(new ServiceBinding(bindingData)) : Optional.empty();
}
private Map extractSingleItem(List> instances, String tooManyErrorText) throws InternalError {
if (instances.isEmpty()) {
return new HashMap<>();
}
if (instances.size() > 1) {
throw new InternalError(tooManyErrorText);
}
return instances.get(0);
}
/**
* Hash of plan id and tenant id to calculate a unique key. Assures that only one instance can be created per tenant.
* Algorithm taken from instance manager client lib
*
* @param tenantId Tenant id
* @return Hash key.
* @throws InternalError
*/
private String calculateServiceInstanceName(String tenantId) throws InternalError {
byte[] hash = DigestUtils.sha256(getPlanId() + "_" + tenantId);
return Base64.encodeBase64String(hash);
}
private static record CreateInstancePayload(String service_plan_id, String name, Map parameters,
Map> labels) {
}
private static record CreateBindingPayload(String service_instance_id, String name,
Map parameters, Map> labels) {
}
private static HttpDestination getDestination(com.sap.cloud.environment.servicebinding.api.ServiceBinding binding) throws InternalError {
if (StringUtils.isBlank((String) binding.getCredentials().get(SM_URL))) {
throw new InternalError("Service manager url is missing");
}
if (binding.getName().isEmpty() || StringUtils.isBlank(binding.getName().get())) {
throw new InternalError("Service binding name is missing");
}
if (binding.getServiceName().isEmpty() || StringUtils.isBlank(binding.getServiceName().get())) {
throw new InternalError("Service name is missing");
}
return ServiceBindingDestinationLoader.defaultLoaderChain()
.getDestination(ServiceBindingDestinationOptions.forService(binding)
.onBehalfOf(OnBehalfOf.TECHNICAL_USER_PROVIDER).build());
}
//ToDo move to a central place in 3.0
private static class ServiceManagerPropertySupplier extends DefaultOAuth2PropertySupplier {
public static final String SERVICE_MANAGER = "service-manager";
private static boolean initialized = false;
public static synchronized void initialize() {
if (!initialized) {
OAuth2ServiceBindingDestinationLoader.registerPropertySupplier(
ServiceManagerPropertySupplier::matches,
ServiceManagerPropertySupplier::new);
initialized = true;
}
}
public ServiceManagerPropertySupplier(ServiceBindingDestinationOptions options) {
super(options, Collections.emptyList());
}
@Override
public URI getServiceUri() {
return this.getOAuthCredentialOrThrow(URI.class, SM_URL);
}
@Override
public URI getTokenUri() {
return this.getOAuthCredential(URI.class, "certurl")
.getOrElse(this.getOAuthCredentialOrThrow(URI.class, "url"));
}
@Override
public ClientIdentity getClientIdentity() {
return getOAuthCredential(String.class, "certurl").isDefined() ? getCertificateIdentity() : getSecretIdentity();
}
ClientIdentity getCertificateIdentity() {
final String clientid = getOAuthCredentialOrThrow(String.class, "clientid");
final String cert = getOAuthCredentialOrThrow(String.class, "certificate");
final String key = getOAuthCredentialOrThrow(String.class, "key");
return new ClientCertificate(cert, key, clientid);
}
ClientIdentity getSecretIdentity() {
final String clientid = getOAuthCredentialOrThrow(String.class, "clientid");
final String secret = getOAuthCredentialOrThrow(String.class, "clientsecret");
return new ClientCredentials(clientid, secret);
}
private static boolean matches(ServiceBindingDestinationOptions options) {
return matches(options.getServiceBinding(), SERVICE_MANAGER, SERVICE_MANAGER);
}
//ToDo replace by ServiceBindingUtils.matches when available in 3.0
public static boolean matches(com.sap.cloud.environment.servicebinding.api.ServiceBinding binding, String tagFilter, String serviceNameFilter) {
boolean tagsMatched = false;
if (tagFilter != null && binding.getTags() != null && !binding.getTags().isEmpty()) {
tagsMatched = binding.getTags().contains(tagFilter);
}
return tagsMatched
|| (serviceNameFilter != null && serviceNameFilter.equals(binding.getServiceName().orElse(null)));
}
}
private static class DeterminationError extends RuntimeException {
public DeterminationError() {
}
public DeterminationError(String message) {
super(message);
}
public DeterminationError(String message, Throwable cause) {
super(message, cause);
}
}
private ServiceResponse callWithNewThreadContext(SmCaller caller) throws ServiceException {
if (ThreadContextAccessor.tryGetCurrentContext().isSuccess()) {
var threadContext = ThreadContextAccessor.tryGetCurrentContext().get();
try {
threadContext.setPropertyIfAbsent(COM_SAP_CLOUD_MT_SM_CALLED, Property.of(true));
return caller.call();
} finally {
threadContext.removeProperty(COM_SAP_CLOUD_MT_SM_CALLED);
}
} else {
DefaultThreadContext threadContext = new DefaultThreadContext();
threadContext.setPropertyIfAbsent(COM_SAP_CLOUD_MT_SM_CALLED, Property.of(true));
try {
return ThreadContextExecutor.using(threadContext).execute(() -> caller.call());
} catch (ThreadContextExecutionException exception) {
if (exception.getCause() != null) {
if (exception.getCause() instanceof ServiceException serviceException) {
throw serviceException;
}
throw new ServiceException(exception.getCause(), null);
}
throw new ServiceException(exception, null);
}
}
}
@FunctionalInterface
public interface SmCaller {
ServiceResponse call() throws ServiceException;
}
}