com.sap.cloud.mt.subscription.MtxTools Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of multi-tenant-subscription Show documentation
Show all versions of multi-tenant-subscription Show documentation
Spring Boot Enablement Parent
/*
* ----------------------------------------------------------------
* © 2022 SAP SE or an SAP affiliate company. All rights reserved.
* ----------------------------------------------------------------
*
*/
package com.sap.cloud.mt.subscription;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.sap.cloud.mt.subscription.HanaEncryptionTool.DbEncryptionMode;
import com.sap.cloud.mt.subscription.exceptions.AuthorityError;
import com.sap.cloud.mt.subscription.exceptions.InternalError;
import com.sap.cloud.mt.subscription.exceptions.NotFound;
import com.sap.cloud.mt.subscription.exceptions.ParameterError;
import com.sap.cloud.mt.subscription.exits.Exits;
import com.sap.cloud.mt.subscription.json.DeletePayload;
import com.sap.cloud.mt.subscription.json.SubscriptionPayload;
import com.sap.cloud.mt.tools.api.AsyncCallResult;
import com.sap.cloud.mt.tools.api.ServiceResponse;
import com.sap.cloud.mt.tools.exception.InternalException;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.NameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import static com.sap.cloud.mt.subscription.Tools.checkExternalTenantId;
import static com.sap.cloud.mt.subscription.Tools.getApplicationUrl;
import static com.sap.cloud.mt.subscription.Tools.waitSomeTime;
public class MtxTools {
//Wait a little, to avoid that this threads report to the saas registry before the initial update was finished.
//CIS runs into a problem when the callback comes before the initial CIS request is executed
public static final Duration SAAS_REGISTRY_WAIT_TIME = Duration.ofMillis(20);
private static final String X_JOB_ID = "x-job-id";
private static final String LOCATION = "Location";
private static Logger logger = LoggerFactory.getLogger(MtxTools.class);
private static final String STATUS = "status";
private static final String JOB_ID = "jobID";
private final SecurityChecker securityChecker;
//Domain from which the URL that is passed to CIC is calculated. It should be the URL of the application UI.
private final String baseUiUrl;
//Separator between subdomain and baseUIUrl: subdomain+urlSeparator+baseUiUrl
private final String urlSeparator;
private final PollingParameters pollingParameter;
private final DbEncryptionMode hanaEncryptionMode;
private static final ObjectMapper mapper = new ObjectMapper();
public MtxTools(SecurityChecker securityChecker, String baseUiUrl, String urlSeparator,
PollingParameters pollingParameter, DbEncryptionMode hanaEncryptionMode) {
this.securityChecker = securityChecker;
this.baseUiUrl = baseUiUrl;
this.urlSeparator = urlSeparator;
this.pollingParameter = pollingParameter;
this.hanaEncryptionMode = hanaEncryptionMode;
}
public static AsyncCallResult waitForCompletion(String jobId, StatusProvider statusProvider, PollingParameters pollingParameter) {
Instant start = Instant.now();
while (true) {
logger.debug("Wait for completion of job {}", jobId);
try {
Map result = statusProvider.getStatus(jobId);
if (StringUtils.isBlank((String) result.get(STATUS))) {
var resultStr = mapper.writeValueAsString(result);
logger.error("Mtx returned no status for job {}. Mtx returned {}", jobId, resultStr);
return new AsyncCallResult(new InternalError("Mtx returned no status for job %s. Mtx returned %s".formatted(jobId, resultStr)));
}
logger.debug("Mtx returned status {} for job {}", result.get(STATUS), jobId);
switch (((String) result.get(STATUS)).toUpperCase(Locale.ENGLISH)) {
case "FINISHED":
return AsyncCallResult.createOk();
case "INITIAL", "QUEUED", "RUNNING", "PROCESSING":
break;
case "FAILED":
return new AsyncCallResult(new InternalError(
"Provisioning service returned with status \"failed\". Mtx returned %s".formatted(mapper.writeValueAsString(result))));
default:
return new AsyncCallResult(new InternalError(
"Unexpected status %s. Mtx returned %s".formatted(result.get(STATUS), mapper.writeValueAsString(result))));
}
} catch (Exception e) {
return new AsyncCallResult(e);
}
if (Duration.between(start, Instant.now()).compareTo(pollingParameter.getRequestTimeout()) >= 0) {
logger.error("Maximum waiting time for job {} exceeded", jobId);
return new AsyncCallResult(new InternalError("Maximum waiting time on called service exceeded"));
}
waitSomeTime(pollingParameter.getInterval());
}
}
public static String extractJobId(ServiceResponse response) throws InternalException {
String jobId = null;
if (response.getHeaders() != null) {
jobId = getFromHeader(response, X_JOB_ID);
if (StringUtils.isNotBlank(jobId)) {
return jobId;
}
//toDo Delete this after old sidecar deprecation and raise an error if nothing is set in the header
//field
// This code is only relevant for old sidecar
String location = getFromHeader(response, LOCATION);
String[] parts = location.split("/jobs/");
if (parts.length == 2) {
jobId = parts[1];
if (StringUtils.isNotBlank(jobId)) {
if (jobId.startsWith("pollJob(")) {
//This is the format of mtxs sidecar which must set the x-job-id
throw new InternalException("The header x-job-id wasn't set");
}
return jobId;
}
}
}
jobId = (String) getResponseAsMap(response).get(JOB_ID);
if (StringUtils.isBlank(jobId)) {
throw new InternalException("No job id returned");
}
logger.debug("Returned jobId is {}", jobId);
return jobId;
}
private static String getFromHeader(ServiceResponse response, String fieldName) {
return Arrays.stream(response.getHeaders())
.filter(h -> h.getName().equalsIgnoreCase(fieldName))
.map(NameValuePair::getValue).findFirst().orElse("");
}
@SuppressWarnings("unchecked")
private static Map getResponseAsMap(ServiceResponse response) throws InternalException {
if (response.getPayload().isPresent()) {
try {
return new Gson().fromJson(response.getPayload().get(), Map.class);//NOSONAR
} catch (JsonSyntaxException e) {
throw new InternalException("No map returned from mtx service", e);
}
} else {
return new HashMap<>();
}
}
public void unsubscribe(String tenantId,
UnSubscribeExecutor unsubscribeExecutor,
StatusProvider statusProvider,
DeletePayload deletePayload,
boolean withoutAuthorityCheck,
Exits exits) throws InternalError, ParameterError, AuthorityError {
if (!withoutAuthorityCheck) {
securityChecker.checkSubscriptionAuthority();
}
checkExternalTenantId(tenantId);
//Only if this exit returns true, unsubscribe is performed
boolean isUnsubscribePossible = Boolean.TRUE.equals(exits.getBeforeUnSubscribeMethod().call(tenantId, deletePayload));
if (isUnsubscribePossible) {
String jobId = unsubscribeExecutor.execute();
AsyncCallResult asyncCallResult = waitForCompletion(jobId, statusProvider, pollingParameter);
if (asyncCallResult.isOk()) {
exits.getAfterUnSubscribeMethod().call(tenantId, deletePayload);
} else {
throw new InternalError(asyncCallResult.getException());
}
} else {
logger.debug("Unsubscribe exit returned false => skipped unsubscribe for tenant {}", tenantId);
}
}
public String subscribe(String tenantId,
SubscribeExecutor subscribeExecutor,
StatusProvider statusProvider,
SubscriptionPayload subscriptionPayload,
boolean withoutAuthorityCheck,
Exits exits) throws InternalError, ParameterError, AuthorityError {
if (!withoutAuthorityCheck) {
securityChecker.checkSubscriptionAuthority();
}
checkExternalTenantId(tenantId);
var serviceCreateOptions = new ServiceCreateOptions(exits.getBeforeSubscribeMethod().call(tenantId, subscriptionPayload));
var payloadAccess = SubscriptionPayloadAccess.create(subscriptionPayload.getMap());
HanaEncryptionTool.addEncryptionParameter(serviceCreateOptions, hanaEncryptionMode, payloadAccess);
String applicationUrl = getApplicationUrl(subscriptionPayload, exits.getSubscribeExit()::uiURL, exits.getSubscribeExit()::uiURL, baseUiUrl, urlSeparator);
String jobId;
try {
jobId = subscribeExecutor.execute(serviceCreateOptions);
} catch (InternalError e) {
exits.getAfterSubscribeMethod().call(tenantId, subscriptionPayload, false);
throw e;
}
AsyncCallResult asyncCallResult = waitForCompletion(jobId, statusProvider, pollingParameter);
exits.getAfterSubscribeMethod().call(tenantId, subscriptionPayload, asyncCallResult.isOk());
if (asyncCallResult.isNotOk()) {
throw new InternalError(asyncCallResult.getException());
}
return applicationUrl;
}
@FunctionalInterface
public interface UnSubscribeExecutor {
String execute() throws InternalError;
}
@FunctionalInterface
public interface SubscribeExecutor {
String execute(ServiceCreateOptions serviceCreateOptions) throws InternalError;
}
@FunctionalInterface
public interface StatusProvider {
Map getStatus(String jobId) throws InternalError, NotFound;
}
@FunctionalInterface
public interface SaasRegistryCaller {
void callSaasRegistry(boolean isOk, String message, String applicationUrl) throws InternalError;
}
@FunctionalInterface
public interface SupplierWithInternalError {
T get() throws InternalError;
}
}