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.
* The response will be an array of {@link ApplicationUpdateDetail} object, which either indicates a success (200) or
* failure for each of the requested application in the same order as the request. The failure also indicates reason
* for the error. The response will be sent via ChunkResponder to continuously stream upgrade result per application.
*/
@POST
@Path("/upgrade")
@AuditPolicy(AuditDetail.REQUEST_BODY)
public void upgradeApplications(FullHttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@QueryParam("artifactScope") Set artifactScopes,
@QueryParam("allowSnapshot") boolean allowSnapshot) throws Exception {
// TODO: (CDAP-16910) Improve batch API performance as each application upgrade is an event independent of each
// other.
List appIds = decodeAndValidateBatchApplicationRecord(validateNamespace(namespaceId), request);
Set allowedArtifactScopes = getArtifactScopes(artifactScopes);
try (ChunkResponder chunkResponder = responder.sendChunkStart(HttpResponseStatus.OK)) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (JsonWriter jsonWriter = new JsonWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
jsonWriter.beginArray();
for (ApplicationId appId : appIds) {
ApplicationUpdateDetail updateDetail;
try {
ApplicationId newAppId = ApplicationId.DEFAULT_VERSION.equals(appId.getVersion())
? applicationLifecycleService.upgradeLatestApplication(appId.getAppReference(),
allowedArtifactScopes, allowSnapshot)
: applicationLifecycleService.upgradeApplication(appId, allowedArtifactScopes, allowSnapshot);
updateDetail = new ApplicationUpdateDetail(newAppId);
} catch (UnsupportedOperationException e) {
String errorMessage = String.format("Application %s does not support upgrade.", appId);
updateDetail = new ApplicationUpdateDetail(appId, new NotImplementedException(errorMessage));
} catch (InvalidArtifactException | NotFoundException e) {
updateDetail = new ApplicationUpdateDetail(appId, e);
} catch (Exception e) {
updateDetail = new ApplicationUpdateDetail(appId,
new ServiceException("Upgrade failed due to internal error.", e,
HttpResponseStatus.INTERNAL_SERVER_ERROR));
LOG.error("Application upgrade failed with exception", e);
}
GSON.toJson(updateDetail, ApplicationUpdateDetail.class, jsonWriter);
jsonWriter.flush();
chunkResponder.sendChunk(Unpooled.wrappedBuffer(outputStream.toByteArray()));
outputStream.reset();
chunkResponder.flush();
}
jsonWriter.endArray();
}
chunkResponder.sendChunk(Unpooled.wrappedBuffer(outputStream.toByteArray()));
}
}
/**
* Gets {@link ApplicationDetail} for a set of applications. It expects a post body as a array of object, with each
* object specifying the applciation id and an optional version. E.g.
*
*
* The response will be an array of {@link BatchApplicationDetail} object, which either indicates a success (200) or
* failure for each of the requested application in the same order as the request.
*/
@POST
@Path("/appdetail")
public void getApplicationDetails(FullHttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespace) throws Exception {
List appIds = decodeAndValidateBatchApplication(validateNamespace(namespace), request);
Map details = applicationLifecycleService.getAppDetails(appIds);
List result = new ArrayList<>();
for (ApplicationId appId : appIds) {
ApplicationDetail detail = details.get(appId);
if (detail == null) {
result.add(new BatchApplicationDetail(new NotFoundException(appId)));
} else {
result.add(new BatchApplicationDetail(detail));
}
}
responder.sendJson(HttpResponseStatus.OK, GSON.toJson(result));
}
/**
* Decodes request coming from the {@link #getApplicationDetails(FullHttpRequest, HttpResponder, String)} call.
*/
private List decodeAndValidateBatchApplication(NamespaceId namespaceId,
FullHttpRequest request) throws BadRequestException {
try {
List result = new ArrayList<>();
JsonArray array = DECODE_GSON.fromJson(request.content().toString(StandardCharsets.UTF_8), JsonArray.class);
if (array == null) {
throw new BadRequestException("Request body is invalid json, please check that it is a json array.");
}
for (JsonElement element : array) {
if (!element.isJsonObject()) {
throw new BadRequestException("Request element is expected to be a json object.");
}
JsonObject obj = element.getAsJsonObject();
if (!obj.has("appId")) {
throw new BadRequestException("Missing 'appId' in the request element.");
}
String appId = obj.get("appId").getAsString();
String version = Optional.ofNullable(obj.get("version"))
.map(JsonElement::getAsString)
.orElse(ApplicationId.DEFAULT_VERSION);
result.add(validateApplicationVersionId(namespaceId, appId, version));
}
return result;
} catch (JsonSyntaxException e) {
throw new BadRequestException("Request body is invalid json: " + e.getMessage());
}
}
/**
* Decodes request coming from the {@link #upgradeApplications(FullHttpRequest, HttpResponder, String, Set, boolean)}
* call.
*/
private List decodeAndValidateBatchApplicationRecord(NamespaceId namespaceId,
FullHttpRequest request)
throws BadRequestException {
try {
List appIds = new ArrayList<>();
List records =
DECODE_GSON.fromJson(request.content().toString(StandardCharsets.UTF_8),
new TypeToken>() { }.getType());
if (records == null) {
throw new BadRequestException("Request body is invalid json, please check that it is a json array.");
}
for (ApplicationRecord element : records) {
if (element.getName() != null && element.getName().isEmpty()) {
throw new BadRequestException("Missing 'name' in the request element for app-id.");
}
if (element.getAppVersion() == null) {
validateApplicationId(namespaceId, element.getName());
appIds.add(namespaceId.app(element.getName()));
} else {
appIds.add(validateApplicationVersionId(namespaceId, element.getName(), element.getAppVersion()));
}
}
return appIds;
} catch (JsonSyntaxException e) {
throw new BadRequestException("Request body is invalid json: " + e.getMessage());
}
}
// normally we wouldn't want to use a body consumer but would just want to read the request body directly
// since it wont be big. But the deploy app API has one path with different behavior based on content type
// the other behavior requires a BodyConsumer and only have one method per path is allowed,
// so we have to use a BodyConsumer
private BodyConsumer deployAppFromArtifact(final ApplicationId appId) throws IOException {
// Perform auth checks outside BodyConsumer as only the first http request containing auth header
// to populate SecurityRequestContext while http chunk doesn't. BodyConsumer runs in the thread
// that processes the last http chunk.
accessEnforcer.enforce(appId, authenticationContext.getPrincipal(), StandardPermission.CREATE);
LOG.info("Start to deploy app {} in namespace {} by user {}", appId.getApplication(), appId.getParent(),
applicationLifecycleService.decodeUserId(authenticationContext));
// createTempFile() needs a prefix of at least 3 characters
return new AbstractBodyConsumer(File.createTempFile("apprequest-" + appId, ".json", tmpDir)) {
@Override
protected void onFinish(HttpResponder responder, File uploadedFile) {
try (FileReader fileReader = new FileReader(uploadedFile)) {
AppRequest appRequest = DECODE_GSON.fromJson(fileReader, AppRequest.class);
ArtifactSummary artifactSummary = appRequest.getArtifact();
KerberosPrincipalId ownerPrincipalId =
appRequest.getOwnerPrincipal() == null ? null : new KerberosPrincipalId(appRequest.getOwnerPrincipal());
// if we don't null check, it gets serialized to "null"
Object config = appRequest.getConfig();
String configString = config == null ? null :
config instanceof String ? (String) config : GSON.toJson(config);
ChangeSummary changeSummary = appRequest.getChange();
try {
ApplicationWithPrograms app = applicationLifecycleService.deployApp(
appId.getParent(), appId.getApplication(), appId.getVersion(), artifactSummary, configString,
changeSummary, createProgramTerminator(), ownerPrincipalId, appRequest.canUpdateSchedules(),
false, Collections.emptyMap());
LOG.info("Successfully deployed app {} in namespace {} from artifact {} with configuration {} and " +
"principal {}", app.getApplicationId().getApplication(), app.getApplicationId().getNamespace(),
app.getArtifactId(), configString, app.getOwnerPrincipal());
responder.sendJson(HttpResponseStatus.OK, GSON.toJson(getApplicationRecord(app)));
} catch (DatasetManagementException e) {
if (e.getCause() instanceof UnauthorizedException) {
throw (UnauthorizedException) e.getCause();
} else {
throw e;
}
}
} catch (ArtifactNotFoundException e) {
responder.sendString(HttpResponseStatus.NOT_FOUND, e.getMessage());
} catch (ConflictException e) {
responder.sendString(HttpResponseStatus.CONFLICT, e.getMessage());
} catch (UnauthorizedException e) {
responder.sendString(HttpResponseStatus.FORBIDDEN, e.getMessage());
} catch (InvalidArtifactException e) {
responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage());
} catch (IOException e) {
LOG.error("Error reading request body for creating app {}.", appId);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, String.format(
"Error while reading json request body for app %s.", appId));
} catch (Exception e) {
LOG.error("Deploy failure", e);
responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage());
}
}
};
}
private ApplicationRecord getApplicationRecord(ApplicationWithPrograms deployedApp) {
return new ApplicationRecord(
ArtifactSummary.from(deployedApp.getArtifactId().toApiArtifactId()),
deployedApp.getApplicationId().getApplication(),
deployedApp.getApplicationId().getVersion(),
deployedApp.getSpecification().getDescription(),
Optional.ofNullable(deployedApp.getOwnerPrincipal()).map(KerberosPrincipalId::getPrincipal).orElse(null),
deployedApp.getChangeDetail());
}
private BodyConsumer deployApplication(final HttpResponder responder,
final NamespaceId namespace,
final String appId,
final String archiveName,
final String configString,
@Nullable final String ownerPrincipal,
final boolean updateSchedules) throws IOException {
Id.Namespace idNamespace = Id.Namespace.fromEntityId(namespace);
Location namespaceHomeLocation = namespacePathLocator.get(namespace);
if (!namespaceHomeLocation.exists()) {
String msg = String.format("Home directory %s for namespace %s not found",
namespaceHomeLocation, namespace.getNamespace());
LOG.error(msg);
responder.sendString(HttpResponseStatus.NOT_FOUND, msg);
return null;
}
if (archiveName == null || archiveName.isEmpty()) {
responder.sendString(HttpResponseStatus.BAD_REQUEST,
String.format(
"%s header not present. Please include the header and set its value to the jar name.",
ARCHIVE_NAME_HEADER),
new DefaultHttpHeaders().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE));
return null;
}
// TODO: (CDAP-3258) error handling needs to be refactored here, should be able just to throw the exception,
// but the caller catches all exceptions and responds with a 500
final Id.Artifact artifactId;
try {
artifactId = Id.Artifact.parse(idNamespace, archiveName);
} catch (IllegalArgumentException e) {
responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage());
return null;
}
KerberosPrincipalId ownerPrincipalId = ownerPrincipal == null ? null : new KerberosPrincipalId(ownerPrincipal);
// Store uploaded content to a local temp file
String namespacesDir = configuration.get(Constants.Namespace.NAMESPACES_DIR);
File localDataDir = new File(configuration.get(Constants.CFG_LOCAL_DATA_DIR));
File namespaceBase = new File(localDataDir, namespacesDir);
File tempDir = new File(new File(namespaceBase, namespace.getNamespace()),
configuration.get(Constants.AppFabric.TEMP_DIR)).getAbsoluteFile();
if (!DirUtils.mkdirs(tempDir)) {
throw new IOException("Could not create temporary directory at: " + tempDir);
}
final KerberosPrincipalId finalOwnerPrincipalId = ownerPrincipalId;
return new AbstractBodyConsumer(File.createTempFile("app-", ".jar", tempDir)) {
@Override
protected void onFinish(HttpResponder responder, File uploadedFile) {
try {
// deploy app
ApplicationWithPrograms app =
applicationLifecycleService.deployAppAndArtifact(namespace, appId, artifactId, uploadedFile, configString,
finalOwnerPrincipalId, createProgramTerminator(),
updateSchedules);
LOG.info("Successfully deployed app {} in namespace {} from artifact {} with configuration {} and " +
"principal {}", app.getApplicationId().getApplication(), namespace.getNamespace(), artifactId,
configString, finalOwnerPrincipalId);
responder.sendJson(HttpResponseStatus.OK, GSON.toJson(getApplicationRecord(app)));
} catch (InvalidArtifactException e) {
responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage());
} catch (ArtifactAlreadyExistsException e) {
responder.sendString(HttpResponseStatus.CONFLICT, String.format(
"Artifact '%s' already exists. Please use the API that creates an application from an existing artifact. " +
"If you are trying to replace the artifact, please delete it and then try again.", artifactId));
} catch (WriteConflictException e) {
// don't really expect this to happen. It means after multiple retries there were still write conflicts.
LOG.warn("Write conflict while trying to add artifact {}.", artifactId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR,
"Write conflict while adding artifact. This can happen if multiple requests to add " +
"the same artifact occur simultaneously. Please try again.");
} catch (UnauthorizedException e) {
responder.sendString(HttpResponseStatus.FORBIDDEN, e.getMessage());
} catch (ConflictException e) {
responder.sendString(HttpResponseStatus.CONFLICT, e.getMessage());
} catch (Exception e) {
LOG.error("Deploy failure", e);
responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage());
}
}
};
}
private ProgramTerminator createProgramTerminator() {
return programId -> {
switch (programId.getType()) {
case SERVICE:
case WORKER:
killProgramIfRunning(programId);
break;
}
};
}
private void killProgramIfRunning(ProgramId programId) {
ProgramRuntimeService.RuntimeInfo programRunInfo = findRuntimeInfo(programId, runtimeService);
if (programRunInfo != null) {
ProgramController controller = programRunInfo.getController();
controller.kill();
}
}
private NamespaceId validateNamespace(@Nullable String namespace)
throws BadRequestException, NamespaceNotFoundException, AccessException {
if (namespace == null) {
throw new BadRequestException("Path parameter namespace-id cannot be empty");
}
NamespaceId namespaceId;
try {
namespaceId = new NamespaceId(namespace);
} catch (IllegalArgumentException e) {
throw new BadRequestException(String.format("Invalid namespace '%s'", namespace), e);
}
try {
if (!namespaceId.equals(NamespaceId.SYSTEM)) {
namespaceQueryAdmin.get(namespaceId);
}
} catch (NamespaceNotFoundException | AccessException e) {
throw e;
} catch (Exception e) {
// This can only happen when NamespaceAdmin uses HTTP calls to interact with namespaces.
// In AppFabricServer, NamespaceAdmin is bound to DefaultNamespaceAdmin, which interacts directly with the MDS.
// Hence, this exception will never be thrown
throw Throwables.propagate(e);
}
return namespaceId;
}
private void validateApplicationId(@Nullable String namespace, @Nullable String appId)
throws BadRequestException, NamespaceNotFoundException, AccessException {
validateApplicationId(validateNamespace(namespace), appId);
}
private void validateApplicationId(NamespaceId namespaceId, String appId) throws BadRequestException {
validateApplicationVersionId(namespaceId, appId, ApplicationId.DEFAULT_VERSION);
}
private ApplicationId validateApplicationVersionId(@Nullable String namespace,
@Nullable String appId,
@Nullable String versionId)
throws BadRequestException, NamespaceNotFoundException, AccessException {
return validateApplicationVersionId(validateNamespace(namespace), appId, versionId);
}
private ApplicationId validateApplicationVersionId(NamespaceId namespaceId, String appId,
String versionId) throws BadRequestException {
if (appId == null) {
throw new BadRequestException("Path parameter app-id cannot be empty");
}
if (!EntityId.isValidId(appId)) {
throw new BadRequestException(String.format("Invalid app name '%s'", appId));
}
if (versionId == null) {
throw new BadRequestException("Path parameter version-id cannot be empty");
}
if (EntityId.isValidVersionId(versionId)) {
return namespaceId.app(appId, versionId);
}
throw new BadRequestException(String.format("Invalid version '%s'", versionId));
}
}