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
/******************************************************************************
 * © 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;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy