/*
* Copyright © 2014-2015 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package co.cask.cdap.client;
import co.cask.cdap.api.Config;
import co.cask.cdap.api.annotation.Beta;
import co.cask.cdap.client.config.ClientConfig;
import co.cask.cdap.client.util.RESTClient;
import co.cask.cdap.common.ApplicationNotFoundException;
import co.cask.cdap.common.BadRequestException;
import co.cask.cdap.common.NotFoundException;
import co.cask.cdap.common.UnauthenticatedException;
import co.cask.cdap.common.utils.Tasks;
import co.cask.cdap.proto.ApplicationDetail;
import co.cask.cdap.proto.ApplicationRecord;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.ProgramRecord;
import co.cask.cdap.proto.ProgramType;
import co.cask.cdap.proto.artifact.AppRequest;
import co.cask.common.http.HttpMethod;
import co.cask.common.http.HttpRequest;
import co.cask.common.http.HttpResponse;
import co.cask.common.http.ObjectResponse;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
/**
* Provides ways to interact with CDAP applications.
*/
@Beta
public class ApplicationClient {
private static final Gson GSON = new Gson();
private final RESTClient restClient;
private final ClientConfig config;
@Inject
public ApplicationClient(ClientConfig config, RESTClient restClient) {
this.config = config;
this.restClient = restClient;
}
public ApplicationClient(ClientConfig config) {
this(config, new RESTClient(config));
}
/**
* Lists all applications currently deployed.
*
* @param namespace the namespace to list applications from
* @return list of {@link ApplicationRecord ApplicationRecords}.
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public List<ApplicationRecord> list(Id.Namespace namespace) throws IOException, UnauthenticatedException {
HttpResponse response = restClient.execute(HttpMethod.GET,
config.resolveNamespacedURLV3(namespace, "apps"),
config.getAccessToken());
return ObjectResponse.fromJsonBody(response, new TypeToken<List<ApplicationRecord>>() { }).getResponseObject();
}
/**
* Lists all applications currently deployed, optionally filtering to only include applications that use the
* specified artifact name and version.
*
* @param namespace the namespace to list applications from
* @param artifactName the name of the artifact to filter by. If null, no filtering will be done
* @param artifactVersion the version of the artifact to filter by. If null, no filtering will be done
* @return list of {@link ApplicationRecord ApplicationRecords}.
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public List<ApplicationRecord> list(Id.Namespace namespace,
@Nullable String artifactName,
@Nullable String artifactVersion) throws IOException, UnauthenticatedException {
Set<String> names = new HashSet<>();
if (artifactName != null) {
names.add(artifactName);
}
return list(namespace, names, artifactVersion);
}
/**
* Lists all applications currently deployed, optionally filtering to only include applications that use one of
* the specified artifact names and the specified artifact version.
*
* @param namespace the namespace to list applications from
* @param artifactNames the set of artifact names to allow. If empty, no filtering will be done
* @param artifactVersion the version of the artifact to filter by. If null, no filtering will be done
* @return list of {@link ApplicationRecord ApplicationRecords}.
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public List<ApplicationRecord> list(Id.Namespace namespace,
Set<String> artifactNames,
@Nullable String artifactVersion) throws IOException, UnauthenticatedException {
if (artifactNames.isEmpty() && artifactVersion == null) {
return list(namespace);
}
String path;
if (!artifactNames.isEmpty() && artifactVersion != null) {
path = String.format("apps?artifactName=%s&artifactVersion=%s",
Joiner.on(',').join(artifactNames), artifactVersion);
} else if (!artifactNames.isEmpty()) {
path = "apps?artifactName=" + Joiner.on(',').join(artifactNames);
} else {
path = "apps?artifactVersion=" + artifactVersion;
}
HttpResponse response = restClient.execute(HttpMethod.GET,
config.resolveNamespacedURLV3(namespace, path),
config.getAccessToken());
return ObjectResponse.fromJsonBody(response, new TypeToken<List<ApplicationRecord>>() { }).getResponseObject();
}
/**
* Get details about the specified application.
*
* @param appId the id of the application to get
* @return details about the specified application
* @throws ApplicationNotFoundException if the application with the given ID was not found
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public ApplicationDetail get(Id.Application appId)
throws ApplicationNotFoundException, IOException, UnauthenticatedException {
HttpResponse response = restClient.execute(HttpMethod.GET,
config.resolveNamespacedURLV3(appId.getNamespace(), "apps/" + appId.getId()),
config.getAccessToken(),
HttpURLConnection.HTTP_NOT_FOUND);
if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
throw new ApplicationNotFoundException(appId);
}
return ObjectResponse.fromJsonBody(response, ApplicationDetail.class).getResponseObject();
}
/**
* Deletes an application.
*
* @param app the application to delete
* @throws ApplicationNotFoundException if the application with the given ID was not found
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public void delete(Id.Application app) throws ApplicationNotFoundException, IOException, UnauthenticatedException {
HttpResponse response = restClient.execute(HttpMethod.DELETE,
config.resolveNamespacedURLV3(app.getNamespace(), "apps/" + app.getId()),
config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND);
if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
throw new ApplicationNotFoundException(app);
}
}
/**
* Deletes all applications in a namespace.
*
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public void deleteAll(Id.Namespace namespace) throws IOException, UnauthenticatedException {
restClient.execute(HttpMethod.DELETE, config.resolveNamespacedURLV3(namespace, "apps"), config.getAccessToken());
}
/**
* Checks if an application exists.
*
* @param app the application to check
* @return true if the application exists
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public boolean exists(Id.Application app) throws IOException, UnauthenticatedException {
HttpResponse response = restClient.execute(HttpMethod.GET,
config.resolveNamespacedURLV3(app.getNamespace(), "apps/" + app.getId()),
config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND);
return response.getResponseCode() != HttpURLConnection.HTTP_NOT_FOUND;
}
/**
* Waits for an application to be deployed.
*
* @param app the application to check
* @param timeout time to wait before timing out
* @param timeoutUnit time unit of timeout
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
* @throws TimeoutException if the application was not yet deployed before {@code timeout} milliseconds
* @throws InterruptedException if interrupted while waiting
*/
public void waitForDeployed(final Id.Application app, long timeout, TimeUnit timeoutUnit)
throws IOException, UnauthenticatedException, TimeoutException, InterruptedException {
try {
Tasks.waitFor(true, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return exists(app);
}
}, timeout, timeoutUnit, 1, TimeUnit.SECONDS);
} catch (ExecutionException e) {
Throwables.propagateIfPossible(e.getCause(), IOException.class, UnauthenticatedException.class);
}
}
/**
* Waits for an application to be deleted.
*
* @param app the application to check
* @param timeout time to wait before timing out
* @param timeoutUnit time unit of timeout
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
* @throws TimeoutException if the application was not yet deleted before {@code timeout} milliseconds
* @throws InterruptedException if interrupted while waiting
*/
public void waitForDeleted(final Id.Application app, long timeout, TimeUnit timeoutUnit)
throws IOException, UnauthenticatedException, TimeoutException, InterruptedException {
try {
Tasks.waitFor(false, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return exists(app);
}
}, timeout, timeoutUnit, 1, TimeUnit.SECONDS);
} catch (ExecutionException e) {
Throwables.propagateIfPossible(e.getCause(), IOException.class, UnauthenticatedException.class);
}
}
/**
* Deploys an application.
*
* @param jarFile jar file of the application to deploy
* @throws IOException if a network error occurred
*/
public void deploy(Id.Namespace namespace, File jarFile) throws IOException, UnauthenticatedException {
Map<String, String> headers = ImmutableMap.of("X-Archive-Name", jarFile.getName());
deployApp(namespace, jarFile, headers);
}
/**
* Deploys an application with a serialized application configuration string.
*
* @param namespace namespace to which the application should be deployed
* @param jarFile jar file of the application to deploy
* @param appConfig serialized application configuration
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public void deploy(Id.Namespace namespace, File jarFile,
String appConfig) throws IOException, UnauthenticatedException {
Map<String, String> headers = ImmutableMap.of("X-Archive-Name", jarFile.getName(),
"X-App-Config", appConfig);
deployApp(namespace, jarFile, headers);
}
/**
* Deploys an application with an application configuration object.
*
* @param namespace namespace to which the application should be deployed
* @param jarFile jar file of the application to deploy
* @param appConfig application configuration object
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public void deploy(Id.Namespace namespace, File jarFile,
Config appConfig) throws IOException, UnauthenticatedException {
deploy(namespace, jarFile, GSON.toJson(appConfig));
}
/**
* Deploys an application using an existing artifact.
*
* @param appId the id of the application to add
* @param createRequest the request body, which contains the artifact to use and any application config
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public void deploy(Id.Application appId,
AppRequest<?> createRequest) throws IOException, UnauthenticatedException {
URL url = config.resolveNamespacedURLV3(appId.getNamespace(), "apps/" + appId.getId());
HttpRequest request = HttpRequest.put(url)
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.withBody(GSON.toJson(createRequest))
.build();
restClient.upload(request, config.getAccessToken());
}
/**
* Update an existing app to use a different artifact version or config.
*
* @param appId the id of the application to update
* @param updateRequest the request to update the application with
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
* @throws NotFoundException if the app or requested artifact could not be found
* @throws BadRequestException if the request is invalid
*/
public void update(Id.Application appId, AppRequest<?> updateRequest)
throws IOException, UnauthenticatedException, NotFoundException, BadRequestException {
URL url = config.resolveNamespacedURLV3(appId.getNamespace(), String.format("apps/%s/update", appId.getId()));
HttpRequest request = HttpRequest.post(url)
.withBody(GSON.toJson(updateRequest))
.build();
HttpResponse response = restClient.execute(request, config.getAccessToken(),
HttpURLConnection.HTTP_NOT_FOUND, HttpURLConnection.HTTP_BAD_REQUEST);
int responseCode = response.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
throw new NotFoundException("app or app artifact");
} else if (responseCode == HttpURLConnection.HTTP_BAD_REQUEST) {
throw new BadRequestException(String.format("Bad Request. Reason: %s",
response.getResponseBodyAsString()));
}
}
private void deployApp(Id.Namespace namespace, File jarFile, Map<String, String> headers)
throws IOException, UnauthenticatedException {
URL url = config.resolveNamespacedURLV3(namespace, "apps");
HttpRequest request = HttpRequest.post(url)
.addHeaders(headers)
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM)
.withBody(jarFile).build();
restClient.upload(request, config.getAccessToken());
}
/**
* Lists all programs of some type.
*
* @param programType type of the programs to list
* @return list of {@link ProgramRecord}s
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public List<ProgramRecord> listAllPrograms(Id.Namespace namespace, ProgramType programType)
throws IOException, UnauthenticatedException {
Preconditions.checkArgument(programType.isListable());
String path = programType.getCategoryName();
URL url = config.resolveNamespacedURLV3(namespace, path);
HttpRequest request = HttpRequest.get(url).build();
ObjectResponse<List<ProgramRecord>> response = ObjectResponse.fromJsonBody(
restClient.execute(request, config.getAccessToken()), new TypeToken<List<ProgramRecord>>() { });
return response.getResponseObject();
}
/**
* Lists all programs.
*
* @return list of {@link ProgramRecord}s
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public Map<ProgramType, List<ProgramRecord>> listAllPrograms(Id.Namespace namespace)
throws IOException, UnauthenticatedException {
ImmutableMap.Builder<ProgramType, List<ProgramRecord>> allPrograms = ImmutableMap.builder();
for (ProgramType programType : ProgramType.values()) {
if (programType.isListable()) {
List<ProgramRecord> programRecords = Lists.newArrayList();
programRecords.addAll(listAllPrograms(namespace, programType));
allPrograms.put(programType, programRecords);
}
}
return allPrograms.build();
}
/**
* Lists programs of some type belonging to an application.
*
* @param app the application
* @param programType type of the programs to list
* @return list of {@link ProgramRecord}s
* @throws ApplicationNotFoundException if the application with the given ID was not found
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public List<ProgramRecord> listPrograms(Id.Application app, ProgramType programType)
throws ApplicationNotFoundException, IOException, UnauthenticatedException {
Preconditions.checkArgument(programType.isListable());
List<ProgramRecord> programs = Lists.newArrayList();
for (ProgramRecord program : listPrograms(app)) {
if (programType.equals(program.getType())) {
programs.add(program);
}
}
return programs;
}
/**
* Lists programs of some type belonging to an application.
*
* @param app the application
* @return Map of {@link ProgramType} to list of {@link ProgramRecord}s
* @throws ApplicationNotFoundException if the application with the given ID was not found
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public Map<ProgramType, List<ProgramRecord>> listProgramsByType(Id.Application app)
throws ApplicationNotFoundException, IOException, UnauthenticatedException {
Map<ProgramType, List<ProgramRecord>> result = Maps.newHashMap();
for (ProgramType type : ProgramType.values()) {
result.put(type, Lists.<ProgramRecord>newArrayList());
}
for (ProgramRecord program : listPrograms(app)) {
result.get(program.getType()).add(program);
}
return result;
}
/**
* Lists programs belonging to an application.
*
* @param app the application
* @return List of all {@link ProgramRecord}s
* @throws ApplicationNotFoundException if the application with the given ID was not found
* @throws IOException if a network error occurred
* @throws UnauthenticatedException if the request is not authorized successfully in the gateway server
*/
public List<ProgramRecord> listPrograms(Id.Application app)
throws ApplicationNotFoundException, IOException, UnauthenticatedException {
String path = String.format("apps/%s", app.getId());
URL url = config.resolveNamespacedURLV3(app.getNamespace(), path);
HttpRequest request = HttpRequest.get(url).build();
HttpResponse response = restClient.execute(request, config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND);
if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
throw new ApplicationNotFoundException(app);
}
return ObjectResponse.fromJsonBody(response, ApplicationDetail.class).getResponseObject().getPrograms();
}
}