All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cloud.mt.subscription.ServiceManager Maven / Gradle / Ivy

There is a newer version: 3.3.1
Show newest version
/*******************************************************************************
 *   © 2019-2024 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.OAuth2Options;
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.resilience.ResilienceConfiguration;
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,
						  Duration oauthTimeout) 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, oauthTimeout);
		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);
		}
	}

	public ServiceManager(com.sap.cloud.environment.servicebinding.api.ServiceBinding serviceBinding, ServiceSpecification serviceSpecification, String serviceOfferingName, String planName) throws InternalError {
		this(serviceBinding, serviceSpecification, serviceOfferingName, planName, null);
	}

	/**
	 * 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,
												  Duration oauthTimeout) 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");
		}
		var timeLimiterConfiguration = ResilienceConfiguration.TimeLimiterConfiguration
				.of(oauthTimeout != null ? oauthTimeout : Duration.ofSeconds(30));
		return ServiceBindingDestinationLoader.defaultLoaderChain()
				.getDestination(ServiceBindingDestinationOptions.forService(binding)
						.withOption(OAuth2Options.TokenRetrievalTimeout.of(timeLimiterConfiguration))
						.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;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy