/*
* Copyright © 2015-2016 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.test;
import co.cask.cdap.api.Config;
import co.cask.cdap.api.app.Application;
import co.cask.cdap.api.artifact.ArtifactVersion;
import co.cask.cdap.api.dataset.DatasetAdmin;
import co.cask.cdap.api.dataset.DatasetProperties;
import co.cask.cdap.api.dataset.module.DatasetModule;
import co.cask.cdap.api.plugin.PluginClass;
import co.cask.cdap.app.DefaultApplicationContext;
import co.cask.cdap.app.MockAppConfigurer;
import co.cask.cdap.app.program.ManifestFields;
import co.cask.cdap.app.runtime.spark.SparkRuntimeUtils;
import co.cask.cdap.client.ApplicationClient;
import co.cask.cdap.client.ArtifactClient;
import co.cask.cdap.client.DatasetClient;
import co.cask.cdap.client.DatasetModuleClient;
import co.cask.cdap.client.NamespaceClient;
import co.cask.cdap.client.ProgramClient;
import co.cask.cdap.client.config.ClientConfig;
import co.cask.cdap.client.config.ConnectionConfig;
import co.cask.cdap.client.util.RESTClient;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.io.Locations;
import co.cask.cdap.common.lang.ProgramResources;
import co.cask.cdap.explore.jdbc.ExploreDriver;
import co.cask.cdap.internal.app.runtime.artifact.Artifacts;
import co.cask.cdap.internal.test.AppJarHelper;
import co.cask.cdap.internal.test.PluginJarHelper;
import co.cask.cdap.proto.DatasetInstanceConfiguration;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.NamespaceMeta;
import co.cask.cdap.proto.artifact.AppRequest;
import co.cask.cdap.proto.artifact.ArtifactRange;
import co.cask.cdap.proto.id.ApplicationId;
import co.cask.cdap.proto.id.ArtifactId;
import co.cask.cdap.proto.id.Ids;
import co.cask.cdap.proto.id.NamespaceId;
import co.cask.cdap.test.remote.RemoteApplicationManager;
import co.cask.cdap.test.remote.RemoteArtifactManager;
import co.cask.cdap.test.remote.RemoteStreamManager;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.io.Files;
import com.google.common.io.InputSupplier;
import com.google.common.io.Resources;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import org.apache.twill.api.ClassAcceptor;
import org.apache.twill.filesystem.LocalLocationFactory;
import org.apache.twill.filesystem.Location;
import org.apache.twill.filesystem.LocationFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.net.JarURLConnection;
import java.net.URL;
import java.sql.Connection;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Manifest;
import javax.annotation.Nullable;
/**
* {@link TestManager} for integration tests.
*/
public class IntegrationTestManager implements TestManager {
private static final Logger LOG = LoggerFactory.getLogger(IntegrationTestManager.class);
private static final Gson GSON = new Gson();
private static final ClassAcceptor CLASS_ACCEPTOR = new ClassAcceptor() {
final Set<String> visibleResources = ProgramResources.getVisibleResources();
@Override
public boolean accept(String className, URL classUrl, URL classPathUrl) {
String resourceName = className.replace('.', '/') + ".class";
if (visibleResources.contains(resourceName)) {
return false;
}
// Always includes Scala class. It is for CDAP-5168
if (resourceName.startsWith("scala/")) {
return true;
}
// If it is loading by spark framework, don't include it in the app JAR
return !SparkRuntimeUtils.SPARK_PROGRAM_CLASS_LOADER_FILTER.acceptResource(resourceName);
}
};
private final ApplicationClient applicationClient;
private final ArtifactClient artifactClient;
private final DatasetClient datasetClient;
private final DatasetModuleClient datasetModuleClient;
private final NamespaceClient namespaceClient;
private final ProgramClient programClient;
private final ClientConfig clientConfig;
private final RESTClient restClient;
private final LocationFactory locationFactory;
private final File tmpFolder;
public IntegrationTestManager(ClientConfig clientConfig, RESTClient restClient, File tmpFolder) {
this.clientConfig = clientConfig;
this.restClient = restClient;
this.tmpFolder = tmpFolder;
this.locationFactory = new LocalLocationFactory(tmpFolder);
this.applicationClient = new ApplicationClient(clientConfig, restClient);
this.artifactClient = new ArtifactClient(clientConfig, restClient);
this.datasetClient = new DatasetClient(clientConfig, restClient);
this.datasetModuleClient = new DatasetModuleClient(clientConfig, restClient);
this.namespaceClient = new NamespaceClient(clientConfig, restClient);
this.programClient = new ProgramClient(clientConfig, restClient);
}
@Override
public ApplicationManager deployApplication(Id.Namespace namespace,
Class<? extends Application> applicationClz,
File... bundleEmbeddedJars) {
return deployApplication(namespace, applicationClz, null, bundleEmbeddedJars);
}
@Override
@SuppressWarnings("unchecked")
public ApplicationManager deployApplication(Id.Namespace namespace, Class<? extends Application> applicationClz,
@Nullable Config configObject, File... bundleEmbeddedJars) {
// See if the application class comes from file or jar.
// If it's from JAR, no need to trace dependency since it should already be in an application jar.
URL appClassURL = applicationClz.getClassLoader()
.getResource(applicationClz.getName().replace('.', '/') + ".class");
// Should never happen, otherwise the ClassLoader is broken
Preconditions.checkNotNull(appClassURL, "Cannot find class %s from the classloader", applicationClz);
String appConfig = "";
TypeToken typeToken = TypeToken.of(applicationClz);
Type configType = Artifacts.getConfigType(applicationClz);
try {
if (configObject != null) {
appConfig = GSON.toJson(configObject);
} else {
configObject = (Config) TypeToken.of(configType).getRawType().newInstance();
}
// Create and deploy application jar
File appJarFile = new File(tmpFolder, String.format("%s-1.0.0-SNAPSHOT.jar", applicationClz.getSimpleName()));
try {
if ("jar".equals(appClassURL.getProtocol())) {
copyJarFile(appClassURL, appJarFile);
} else {
Location appJar = AppJarHelper.createDeploymentJar(locationFactory, applicationClz, new Manifest(),
CLASS_ACCEPTOR, bundleEmbeddedJars);
Files.copy(Locations.newInputSupplier(appJar), appJarFile);
}
applicationClient.deploy(namespace, appJarFile, appConfig);
} finally {
if (!appJarFile.delete()) {
LOG.warn("Failed to delete temporary app jar {}", appJarFile.getAbsolutePath());
}
}
// Extracts application id from the application class
Application application = applicationClz.newInstance();
MockAppConfigurer configurer = new MockAppConfigurer(application);
application.configure(configurer, new DefaultApplicationContext<>(configObject));
String applicationId = configurer.getName();
return new RemoteApplicationManager(Id.Application.from(namespace, applicationId), clientConfig, restClient);
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
@Override
public ApplicationManager deployApplication(Id.Application appId,
AppRequest appRequest) throws Exception {
applicationClient.deploy(appId, appRequest);
return new RemoteApplicationManager(appId, clientConfig, restClient);
}
@Override
public ApplicationManager getApplicationManager(ApplicationId applicationId) {
return new RemoteApplicationManager(applicationId.toId(), clientConfig, restClient);
}
@Override
public void addArtifact(Id.Artifact artifactId, File artifactFile) throws Exception {
artifactClient.add(artifactId, null, Files.newInputStreamSupplier(artifactFile));
}
@Override
public ArtifactManager addArtifact(ArtifactId artifactId, final File artifactFile) throws Exception {
artifactClient.add(artifactId.toId(), null, new InputSupplier<InputStream>() {
@Override
public InputStream getInput() throws IOException {
return new FileInputStream(artifactFile);
}
});
return new RemoteArtifactManager(clientConfig, restClient, artifactId);
}
@Override
public void addAppArtifact(Id.Artifact artifactId, Class<?> appClass) throws Exception {
addAppArtifact(artifactId.toEntityId(), appClass);
}
@Override
public ArtifactManager addAppArtifact(ArtifactId artifactId, Class<?> appClass) throws Exception {
addAppArtifact(artifactId, appClass, new Manifest());
return new RemoteArtifactManager(clientConfig, restClient, artifactId);
}
@Override
public void addAppArtifact(Id.Artifact artifactId, Class<?> appClass, String... exportPackages) throws Exception {
addAppArtifact(artifactId.toEntityId(), appClass, exportPackages);
}
@Override
public ArtifactManager addAppArtifact(ArtifactId artifactId, Class<?> appClass,
String... exportPackages) throws Exception {
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(ManifestFields.EXPORT_PACKAGE, Joiner.on(',').join(exportPackages));
addAppArtifact(artifactId, appClass, manifest);
return new RemoteArtifactManager(clientConfig, restClient, artifactId);
}
@Override
public void addAppArtifact(Id.Artifact artifactId, Class<?> appClass, Manifest manifest) throws Exception {
addAppArtifact(artifactId.toEntityId(), appClass, manifest);
}
@Override
public ArtifactManager addAppArtifact(ArtifactId artifactId, Class<?> appClass,
Manifest manifest) throws Exception {
final Location appJar = AppJarHelper.createDeploymentJar(locationFactory, appClass, manifest, CLASS_ACCEPTOR);
artifactClient.add(artifactId.toId(), null, new InputSupplier<InputStream>() {
@Override
public InputStream getInput() throws IOException {
return appJar.getInputStream();
}
});
appJar.delete();
return new RemoteArtifactManager(clientConfig, restClient, artifactId);
}
@Override
public void addPluginArtifact(Id.Artifact artifactId, Id.Artifact parent,
Class<?> pluginClass, Class<?>... pluginClasses) throws Exception {
addPluginArtifact(artifactId.toEntityId(), parent.toEntityId(), pluginClass, pluginClasses);
}
@Override
public ArtifactManager addPluginArtifact(ArtifactId artifactId, ArtifactId parent,
Class<?> pluginClass, Class<?>... pluginClasses) throws Exception {
Set<ArtifactRange> parents = new HashSet<>();
parents.add(new ArtifactRange(
Ids.namespace(parent.getNamespace()).toId(), parent.getArtifact(), new ArtifactVersion(parent.getVersion()),
true, new ArtifactVersion(parent.getVersion()), true));
addPluginArtifact(artifactId, parents, pluginClass, pluginClasses);
return new RemoteArtifactManager(clientConfig, restClient, artifactId);
}
@Override
public void addPluginArtifact(Id.Artifact artifactId, Set<ArtifactRange> parents,
Class<?> pluginClass, Class<?>... pluginClasses) throws Exception {
addPluginArtifact(artifactId.toEntityId(), parents, pluginClass, pluginClasses);
}
@Override
public ArtifactManager addPluginArtifact(ArtifactId artifactId, Set<ArtifactRange> parents,
Class<?> pluginClass, Class<?>... pluginClasses) throws Exception {
Manifest manifest = createManifest(pluginClass, pluginClasses);
final Location appJar = PluginJarHelper.createPluginJar(locationFactory, manifest, pluginClass, pluginClasses);
artifactClient.add(artifactId.toId(), parents, new InputSupplier<InputStream>() {
@Override
public InputStream getInput() throws IOException {
return appJar.getInputStream();
}
});
appJar.delete();
return new RemoteArtifactManager(clientConfig, restClient, artifactId);
}
@Override
public void addPluginArtifact(Id.Artifact artifactId, Id.Artifact parent,
Set<PluginClass> additionalPlugins,
Class<?> pluginClass,
Class<?>... pluginClasses) throws Exception {
addPluginArtifact(artifactId.toEntityId(), parent.toEntityId(), additionalPlugins, pluginClass, pluginClasses);
}
@Override
public ArtifactManager addPluginArtifact(ArtifactId artifactId, ArtifactId parent,
@Nullable Set<PluginClass> additionalPlugins, Class<?> pluginClass,
Class<?>... pluginClasses) throws Exception {
Set<ArtifactRange> parents = new HashSet<>();
parents.add(new ArtifactRange(
Ids.namespace(parent.getNamespace()).toId(), parent.getArtifact(), new ArtifactVersion(parent.getVersion()),
true, new ArtifactVersion(parent.getVersion()), true));
addPluginArtifact(artifactId, parents, additionalPlugins, pluginClass, pluginClasses);
return new RemoteArtifactManager(clientConfig, restClient, artifactId);
}
@Override
public void addPluginArtifact(Id.Artifact artifactId, Set<ArtifactRange> parents,
@Nullable Set<PluginClass> additionalPlugins,
Class<?> pluginClass, Class<?>... pluginClasses) throws Exception {
addPluginArtifact(artifactId.toEntityId(), parents, additionalPlugins, pluginClass, pluginClasses);
}
@Override
public ArtifactManager addPluginArtifact(ArtifactId artifactId, Set<ArtifactRange> parents,
@Nullable Set<PluginClass> additionalPlugins, Class<?> pluginClass,
Class<?>... pluginClasses) throws Exception {
Manifest manifest = createManifest(pluginClass, pluginClasses);
final Location appJar = PluginJarHelper.createPluginJar(locationFactory, manifest, pluginClass, pluginClasses);
artifactClient.add(
Ids.namespace(artifactId.getNamespace()).toId(),
artifactId.getArtifact(),
new InputSupplier<InputStream>() {
@Override
public InputStream getInput() throws IOException {
return appJar.getInputStream();
}
},
artifactId.getVersion(),
parents,
additionalPlugins
);
appJar.delete();
return new RemoteArtifactManager(clientConfig, restClient, artifactId);
}
@Override
public void deleteArtifact(Id.Artifact artifactId) throws Exception {
artifactClient.delete(artifactId);
}
@Override
public void clear() throws Exception {
for (NamespaceMeta namespace : namespaceClient.list()) {
programClient.stopAll(Id.Namespace.from(namespace.getName()));
}
namespaceClient.deleteAll();
}
@Override
public void deployDatasetModule(Id.Namespace namespace, String moduleName,
Class<? extends DatasetModule> datasetModule) throws Exception {
datasetModuleClient.add(Id.DatasetModule.from(namespace, moduleName),
datasetModule.getName(),
createModuleJarFile(datasetModule));
}
private File createModuleJarFile(Class<?> cls) throws IOException {
String version = String.format("1.0.%d-SNAPSHOT", System.currentTimeMillis());
File moduleJarFile = new File(tmpFolder, String.format("%s-%s.jar", cls.getSimpleName(), version));
Location deploymentJar = AppJarHelper.createDeploymentJar(locationFactory, cls, new Manifest(), CLASS_ACCEPTOR);
Files.copy(Locations.newInputSupplier(deploymentJar), moduleJarFile);
return moduleJarFile;
}
@Override
@SuppressWarnings("unchecked")
public <T extends DatasetAdmin> T addDatasetInstance(Id.Namespace namespace,
String datasetTypeName, String datasetInstanceName,
DatasetProperties props) throws Exception {
Id.DatasetInstance datasetInstance = Id.DatasetInstance.from(namespace, datasetInstanceName);
DatasetInstanceConfiguration dsConf = new DatasetInstanceConfiguration(datasetTypeName, props.getProperties(),
props.getDescription());
datasetClient.create(datasetInstance, dsConf);
return (T) new RemoteDatasetAdmin(datasetClient, datasetInstance, dsConf);
}
@Override
public <T extends DatasetAdmin> T addDatasetInstance(Id.Namespace namespace,
String datasetTypeName,
String datasetInstanceName) throws Exception {
return addDatasetInstance(namespace, datasetTypeName, datasetInstanceName, DatasetProperties.EMPTY);
}
@Override
public <T> DataSetManager<T> getDataset(Id.Namespace namespace, String datasetInstanceName) throws Exception {
throw new UnsupportedOperationException();
}
@Override
public Connection getQueryClient(Id.Namespace namespace) throws Exception {
ConnectionConfig connConfig = clientConfig.getConnectionConfig();
String url = String.format("%s%s:%d?namespace=%s", Constants.Explore.Jdbc.URL_PREFIX, connConfig.getHostname(),
connConfig.getPort(), namespace.getId());
return new ExploreDriver().connect(url, new Properties());
}
@Override
public void createNamespace(NamespaceMeta namespaceMeta) throws Exception {
namespaceClient.create(namespaceMeta);
}
@Override
public void deleteNamespace(Id.Namespace namespace) throws Exception {
namespaceClient.delete(namespace);
}
@Override
public StreamManager getStreamManager(Id.Stream streamId) {
return new RemoteStreamManager(clientConfig, restClient, streamId);
}
@Override
public void deleteAllApplications(NamespaceId namespaceId) throws Exception {
applicationClient.deleteAll(namespaceId.toId());
}
/**
* Copies the jar content to a local file
*
* @param jarURL URL representing the jar location or an entry in the jar. An entry URL has format of
* {@code jar:[jarURL]!/path/to/entry}
* @param file the local file to copy to
*/
private void copyJarFile(URL jarURL, File file) {
try {
JarURLConnection jarConn = (JarURLConnection) jarURL.openConnection();
try {
Files.copy(Resources.newInputStreamSupplier(jarConn.getJarFileURL()), file);
} finally {
jarConn.getJarFile().close();
}
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
private Manifest createManifest(Class<?> cls, Class<?>... classes) {
Manifest manifest = new Manifest();
Set<String> exportPackages = new HashSet<>();
exportPackages.add(cls.getPackage().getName());
for (Class<?> clz : classes) {
exportPackages.add(clz.getPackage().getName());
}
manifest.getMainAttributes().put(ManifestFields.EXPORT_PACKAGE, Joiner.on(',').join(exportPackages));
return manifest;
}
}