/* * Copyright © 2014-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.data2.datafabric.dataset; import co.cask.cdap.api.dataset.DatasetManagementException; import co.cask.cdap.api.dataset.DatasetProperties; import co.cask.cdap.api.dataset.InstanceConflictException; import co.cask.cdap.api.dataset.InstanceNotFoundException; import co.cask.cdap.common.ServiceUnavailableException; import co.cask.cdap.common.conf.CConfiguration; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.common.discovery.EndpointStrategy; import co.cask.cdap.common.discovery.RandomEndpointStrategy; import co.cask.cdap.common.io.Locations; import co.cask.cdap.data2.dataset2.ModuleConflictException; import co.cask.cdap.proto.DatasetInstanceConfiguration; import co.cask.cdap.proto.DatasetMeta; import co.cask.cdap.proto.DatasetModuleMeta; import co.cask.cdap.proto.DatasetSpecificationSummary; import co.cask.cdap.proto.DatasetTypeMeta; import co.cask.cdap.proto.Id; import co.cask.cdap.security.spi.authentication.SecurityRequestContext; import co.cask.common.http.HttpMethod; import co.cask.common.http.HttpRequest; import co.cask.common.http.HttpRequestConfig; import co.cask.common.http.HttpRequests; import co.cask.common.http.HttpResponse; import com.google.common.base.Joiner; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.common.io.InputSupplier; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import org.apache.twill.discovery.Discoverable; import org.apache.twill.discovery.DiscoveryServiceClient; import org.apache.twill.filesystem.Location; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; import java.net.InetSocketAddress; import java.net.URL; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; /** * Provides programmatic APIs to access {@link co.cask.cdap.data2.datafabric.dataset.service.DatasetService}. * Just a java wrapper for accessing service's REST API. */ class DatasetServiceClient { private static final Gson GSON = new Gson(); private static final Type SUMMARY_LIST_TYPE = new TypeToken<List<DatasetSpecificationSummary>>() { }.getType(); private static final Type MODULE_META_LIST_TYPE = new TypeToken<List<DatasetModuleMeta>>() { }.getType(); private final Supplier<EndpointStrategy> endpointStrategySupplier; private final Id.Namespace namespaceId; private final HttpRequestConfig httpRequestConfig; DatasetServiceClient(final DiscoveryServiceClient discoveryClient, Id.Namespace namespaceId, CConfiguration cConf) { this.endpointStrategySupplier = Suppliers.memoize(new Supplier<EndpointStrategy>() { @Override public EndpointStrategy get() { return new RandomEndpointStrategy(discoveryClient.discover(Constants.Service.DATASET_MANAGER)); } }); this.namespaceId = namespaceId; int httpTimeoutMs = cConf.getInt(Constants.HTTP_CLIENT_TIMEOUT_MS); this.httpRequestConfig = new HttpRequestConfig(httpTimeoutMs, httpTimeoutMs); } @Nullable public DatasetMeta getInstance(String instanceName, @Nullable Iterable<? extends Id> owners) throws DatasetManagementException { String query = ""; if (owners != null) { Set<String> ownerParams = Sets.newHashSet(); for (Id owner : owners) { ownerParams.add("owner=" + owner.toString()); } query = ownerParams.isEmpty() ? "" : "?" + Joiner.on("&").join(ownerParams); } HttpResponse response = doGet("datasets/" + instanceName + query); if (HttpResponseStatus.NOT_FOUND.getCode() == response.getResponseCode()) { return null; } if (HttpResponseStatus.OK.getCode() != response.getResponseCode()) { throw new DatasetManagementException(String.format("Cannot retrieve dataset instance %s info, details: %s", instanceName, response)); } return GSON.fromJson(response.getResponseBodyAsString(), DatasetMeta.class); } @Nullable public DatasetMeta getInstance(String instanceName) throws DatasetManagementException { return getInstance(instanceName, null); } public Collection<DatasetSpecificationSummary> getAllInstances() throws DatasetManagementException { HttpResponse response = doGet("datasets"); if (HttpResponseStatus.OK.getCode() != response.getResponseCode()) { throw new DatasetManagementException(String.format("Cannot retrieve all dataset instances, details: %s", response)); } return GSON.fromJson(response.getResponseBodyAsString(), SUMMARY_LIST_TYPE); } public Collection<DatasetModuleMeta> getAllModules() throws DatasetManagementException { HttpResponse response = doGet("modules"); if (HttpResponseStatus.OK.getCode() != response.getResponseCode()) { throw new DatasetManagementException(String.format("Cannot retrieve all dataset instances, details: %s", response)); } return GSON.fromJson(response.getResponseBodyAsString(), MODULE_META_LIST_TYPE); } public DatasetTypeMeta getType(String typeName) throws DatasetManagementException { HttpResponse response = doGet("types/" + typeName); if (HttpResponseStatus.NOT_FOUND.getCode() == response.getResponseCode()) { return null; } if (HttpResponseStatus.OK.getCode() != response.getResponseCode()) { throw new DatasetManagementException(String.format("Cannot retrieve dataset type %s info, details: %s", typeName, response)); } return GSON.fromJson(response.getResponseBodyAsString(), DatasetTypeMeta.class); } public void addInstance(String datasetInstanceName, String datasetType, DatasetProperties props) throws DatasetManagementException { DatasetInstanceConfiguration creationProperties = new DatasetInstanceConfiguration(datasetType, props.getProperties(), props.getDescription()); HttpResponse response = doPut("datasets/" + datasetInstanceName, GSON.toJson(creationProperties)); if (HttpResponseStatus.CONFLICT.getCode() == response.getResponseCode()) { throw new InstanceConflictException(String.format("Failed to add instance %s due to conflict, details: %s", datasetInstanceName, response)); } if (HttpResponseStatus.OK.getCode() != response.getResponseCode()) { throw new DatasetManagementException(String.format("Failed to add instance %s, details: %s", datasetInstanceName, response)); } } public void updateInstance(String datasetInstanceName, DatasetProperties props) throws DatasetManagementException { HttpResponse response = doPut("datasets/" + datasetInstanceName + "/properties", GSON.toJson(props.getProperties())); if (HttpResponseStatus.NOT_FOUND.getCode() == response.getResponseCode()) { throw new InstanceNotFoundException(datasetInstanceName); } if (HttpResponseStatus.CONFLICT.getCode() == response.getResponseCode()) { throw new InstanceConflictException(String.format("Failed to update instance %s due to conflict, details: %s", datasetInstanceName, response)); } if (HttpResponseStatus.OK.getCode() != response.getResponseCode()) { throw new DatasetManagementException(String.format("Failed to update instance %s, details: %s", datasetInstanceName, response)); } } public void truncateInstance(String datasetInstanceName) throws DatasetManagementException { HttpResponse response = doPost("datasets/" + datasetInstanceName + "/admin/truncate"); if (HttpResponseStatus.NOT_FOUND.getCode() == response.getResponseCode()) { throw new InstanceNotFoundException(datasetInstanceName); } if (HttpResponseStatus.OK.getCode() != response.getResponseCode()) { throw new DatasetManagementException(String.format("Failed to truncate instance %s, details: %s", datasetInstanceName, response)); } } public void deleteInstance(String datasetInstanceName) throws DatasetManagementException { HttpResponse response = doDelete("datasets/" + datasetInstanceName); if (HttpResponseStatus.NOT_FOUND.getCode() == response.getResponseCode()) { throw new InstanceNotFoundException(datasetInstanceName); } if (HttpResponseStatus.CONFLICT.getCode() == response.getResponseCode()) { throw new InstanceConflictException(String.format("Failed to delete instance %s due to conflict, details: %s", datasetInstanceName, response)); } if (HttpResponseStatus.OK.getCode() != response.getResponseCode()) { throw new DatasetManagementException(String.format("Failed to delete instance %s, details: %s", datasetInstanceName, response)); } } public void addModule(String moduleName, String className, Location jarLocation) throws DatasetManagementException { HttpResponse response = doRequest(HttpMethod.PUT, "modules/" + moduleName, ImmutableMultimap.of("X-Class-Name", className), Locations.newInputSupplier(jarLocation)); if (HttpResponseStatus.CONFLICT.getCode() == response.getResponseCode()) { throw new ModuleConflictException(String.format("Failed to add module %s due to conflict, details: %s", moduleName, response)); } if (HttpResponseStatus.OK.getCode() != response.getResponseCode()) { throw new DatasetManagementException(String.format("Failed to add module %s, details: %s", moduleName, response)); } } public void deleteModule(String moduleName) throws DatasetManagementException { HttpResponse response = doDelete("modules/" + moduleName); if (HttpResponseStatus.CONFLICT.getCode() == response.getResponseCode()) { throw new ModuleConflictException(String.format("Failed to delete module %s due to conflict, details: %s", moduleName, response)); } if (HttpResponseStatus.OK.getCode() != response.getResponseCode()) { throw new DatasetManagementException(String.format("Failed to delete module %s, details: %s", moduleName, response)); } } public void deleteModules() throws DatasetManagementException { HttpResponse response = doDelete("modules"); if (HttpResponseStatus.OK.getCode() != response.getResponseCode()) { throw new DatasetManagementException(String.format("Failed to delete modules, details: %s", response)); } } public void createNamespace() throws DatasetManagementException { HttpResponse response = doPut("admin/create", GSON.toJson(namespaceId)); if (HttpResponseStatus.OK.getCode() != response.getResponseCode()) { throw new DatasetManagementException(String.format("Failed to create namespace, details: %s", response)); } } public void deleteNamespace() throws DatasetManagementException { HttpResponse response = doDelete("admin/delete"); if (HttpResponseStatus.OK.getCode() != response.getResponseCode()) { throw new DatasetManagementException(String.format("Failed to delete namespace, details: %s", response)); } } private HttpResponse doGet(String resource) throws DatasetManagementException { return doRequest(HttpMethod.GET, resource); } private HttpResponse doGet(String resource, Multimap<String, String> headers) throws DatasetManagementException { return doRequest(HttpMethod.GET, resource, headers, (InputSupplier<? extends InputStream>) null); } private HttpResponse doPut(String resource, String body) throws DatasetManagementException { return doRequest(HttpMethod.PUT, resource, null, body); } private HttpResponse doPost(String resource) throws DatasetManagementException { return doRequest(HttpMethod.POST, resource); } private HttpResponse doDelete(String resource) throws DatasetManagementException { return doRequest(HttpMethod.DELETE, resource); } private HttpResponse doRequest(HttpMethod method, String resource, @Nullable Multimap<String, String> headers, @Nullable String body) throws DatasetManagementException { String url = resolve(resource); try { return HttpRequests.execute(processBuilder( HttpRequest.builder(method, new URL(url)) .addHeaders(headers) .withBody(body) ).build(), httpRequestConfig); } catch (IOException e) { throw new DatasetManagementException( String.format("Error during talking to Dataset Service at %s while doing %s with headers %s and body %s", url, method, headers == null ? "null" : Joiner.on(",").withKeyValueSeparator("=").join(headers.entries()), body == null ? "null" : body), e); } } private HttpResponse doRequest(HttpMethod method, String resource, @Nullable Multimap<String, String> headers, @Nullable InputSupplier<? extends InputStream> body) throws DatasetManagementException { String url = resolve(resource); try { return HttpRequests.execute(processBuilder( HttpRequest.builder(method, new URL(url)) .addHeaders(headers) .withBody(body) ).build()); } catch (IOException e) { throw new DatasetManagementException( String.format("Error during talking to Dataset Service at %s while doing %s with headers %s and body %s", url, method, headers == null ? "null" : Joiner.on(",").withKeyValueSeparator("=").join(headers.entries()), body == null ? "null" : body), e); } } private HttpRequest.Builder processBuilder(HttpRequest.Builder builder) { if (SecurityRequestContext.getUserId() != null) { builder.addHeader(Constants.Security.Headers.USER_ID, SecurityRequestContext.getUserId()); } return builder; } private HttpResponse doRequest(HttpMethod method, String url) throws DatasetManagementException { return doRequest(method, url, null, (InputSupplier<? extends InputStream>) null); } private String resolve(String resource) throws DatasetManagementException { Discoverable discoverable = endpointStrategySupplier.get().pick(3, TimeUnit.SECONDS); if (discoverable == null) { throw new ServiceUnavailableException("DatasetService"); } InetSocketAddress addr = discoverable.getSocketAddress(); return String.format("http://%s:%s%s/namespaces/%s/data/%s", addr.getHostName(), addr.getPort(), Constants.Gateway.API_VERSION_3, namespaceId.getId(), resource); } }