/* * Copyright (c) 2012-2017 Snowflake Computing Inc. All rights reserved. */ package net.snowflake.ingest; import net.snowflake.ingest.connection.HistoryResponse; import net.snowflake.ingest.connection.IngestResponse; import net.snowflake.ingest.connection.IngestResponseException; import net.snowflake.ingest.connection.HistoryRangeResponse; import net.snowflake.ingest.connection.RequestBuilder; import net.snowflake.ingest.connection.ServiceResponseHandler; import net.snowflake.ingest.utils.BackOffException; import net.snowflake.ingest.utils.HttpUtil; import net.snowflake.ingest.utils.StagedFileWrapper; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URISyntaxException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.interfaces.RSAPrivateCrtKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.RSAPublicKeySpec; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; /** * This class provides a basic, low-level abstraction over * the Snowflake Ingest Service REST api * <p> * Usage of this class delegates all exception and state handling to the developer * * @author obabarinsa */ public class SimpleIngestManager implements AutoCloseable { /** * This Builder allows someone to configure a SimpleIngestManager * prior to instantiating the manager * * @author obabarinsa */ public static class Builder { //the account name we want to use private String account; //the user who will be loading data private String user; //the fully qualified pipe name private String pipe; //the key pair we want to use to authenticate private KeyPair keypair; /** * getAccount - returns the name of the account this builder will inject into the * IngestManager * * @return account name */ public String getAccount() { return account; } /** * setAccount - set the account for the ingest manager and return this builder * * @param account the account which will be loading into this table * @return this builder object */ public Builder setAccount(String account) { this.account = account; return this; } /** * getUser - get the user who will be loading using the ingest service * * @return the user name */ public String getUser() { return user; } /** * setUser - sets the user who will be loading with the ingest manager * * @param user the user who will be loading * @return the current builder with the user set */ public Builder setUser(String user) { this.user = user; return this; } /** * getPipe - get the pipe for the ingest manager this builder will create * * @return the target pipe for this ingest manager */ public String getPipe() { return pipe; } /** * setTable - sets the pipe which the SimpleIngestManager will be using * * @param pipe the target pipe for the ingest manager * @return the current builder with the target pipe */ public Builder setPipe(String pipe) { this.pipe = pipe; return this; } /** * getKeyPair - returns the key-pair we're using for authentication * * @return the RSA 2048 key-pair we use to sign tokens */ public KeyPair getKeypair() { return keypair; } /** * setKeypair - sets the RSA 2048 bit keypair we'll be using for token signing * * @param keypair the keypair we'll be using for auth * @return the current builder with the key set */ public Builder setKeypair(KeyPair keypair) { this.keypair = keypair; return this; } /** * build - returns a new instance of SimpleIngestManager using the information * set in this builder object */ public SimpleIngestManager build() { return new SimpleIngestManager(account, user, pipe, keypair); } } //logger object for this class private static final Logger LOGGER = LoggerFactory.getLogger(SimpleIngestManager.class); //HTTP Client that we use for sending requests to the service private HttpClient httpClient; //the account in which the user lives private String account; //the username of the user private String user; //the fully qualified name of the pipe private String pipe; //the keypair we're using for authentication private KeyPair keyPair; //the request builder who handles building the HttpRequests we send private final RequestBuilder builder; /** * init - Does the basic work of constructing a SimpleIngestManager that * is common across all constructors * * @param account The account into which we're loading * @param user the user performing this load * @param pipe the fully qualified name of pipe * @param keyPair the KeyPair we'll use to sign JWT tokens */ private void init(String account, String user, String pipe, KeyPair keyPair) { //set up our reference variables this.account = account; this.user = user; this.pipe = pipe; this.keyPair = keyPair; //make our client for sending requests httpClient = HttpUtil.getHttpClient(); //make the request builder we'll use to build messages to the service } /** * Constructs a SimpleIngestManager for a given user in a specific account * In addition, this also takes takes the target table and source stage * Finally, it also requires a valid KeyPair object registered with * Snowflake DB * * This method is deprecated, please use the constructor that only requires * PrivateKey instead of KeyPair. * * @param account The account into which we're loading * Note: account should not include region or cloud provider * info. e.g. if host is testaccount.us-east-1.azure * .snowflakecomputing.com, account should be testaccount * @param user the user performing this load * @param pipe the fully qualified name of the pipe * @param keyPair the KeyPair we'll use to sign JWT tokens */ @Deprecated public SimpleIngestManager(String account, String user, String pipe, KeyPair keyPair) { //call our initializer method init(account, user, pipe, keyPair); //create the request builder this.builder = new RequestBuilder(account, user, keyPair); } /** * Constructs a SimpleIngestManager for a given user in a specific account * In addition, this also takes takes the target table and source stage * Finally, it also requires a valid private key registered with * Snowflake DB * * Note: this method only takes in account parameter and derive the hostname, * i.e. testaccount.snowfakecomputing.com. If your deployment is not aws * us-west, please use the constructor that accept hostname as argument * * @param account The account into which we're loading * Note: account should not include region or cloud provider * info. e.g. if host is testaccount.us-east-1.azure * .snowflakecomputing.com, account should be testaccount. * If this is the case, you should use the constructor that * accepts hostname as argument * @param user the user performing this load * @param pipe the fully qualified name of the pipe * @param privateKey the private key we'll use to sign JWT tokens * @throws NoSuchAlgorithmException if can't create * key factory by using RSA algorithm * @throws InvalidKeySpecException if private key or public key is * invalid */ public SimpleIngestManager(String account, String user, String pipe, PrivateKey privateKey) throws InvalidKeySpecException, NoSuchAlgorithmException { KeyPair keyPair = createKeyPairFromPrivateKey(privateKey); //call our initializer method init(account, user, pipe, keyPair); //create the request builder this.builder = new RequestBuilder(account, user, keyPair); } /** * Constructs a SimpleIngestManager for a given user in a specific account * In addition, this also takes takes the target table and source stage * Finally, it also requires a valid KeyPair object registered with * Snowflake DB * * This method is deprecated, please use the constructor that only requires * PrivateKey instead of KeyPair. * * @param account the account into which we're loading * Note: account should not include region or cloud provider * info. e.g. if host is testaccount.us-east-1.azure * .snowflakecomputing.com account should be testaccount * If this is the case, you should use the constructor that * accepts hostname as argument * @param user the user performing this load * @param pipe the fully qualified name of the pipe * @param keyPair the KeyPair we'll use to sign JWT tokens * @param schemeName http or https * @param hostName the hostname * @param port the port number */ @Deprecated public SimpleIngestManager(String account, String user, String pipe, KeyPair keyPair, String schemeName, String hostName, int port) { //call our initializer method init(account, user, pipe, keyPair); //make the request builder we'll use to build messages to the service builder = new RequestBuilder(account, user, keyPair, schemeName, hostName, port); } /** * Constructs a SimpleIngestManager for a given user in a specific account * In addition, this also takes takes the target table and source stage * Finally, it also requires a valid private key registered with * Snowflake DB * * @param account the account into which we're loading * Note: account should not include region or cloud provider * info. e.g. if host is testaccount.us-east-1.azure * .snowflakecomputing.com, account should be testaccount * @param user the user performing this load * @param pipe the fully qualified name of the pipe * @param privateKey the private key we'll use to sign JWT tokens * @param schemeName http or https * @param hostName the hostname i.e. testaccount.us-east-1.azure * .snowflakecomputing.com * @param port the port number * @throws NoSuchAlgorithmException if can't create key factory by using * RSA algorithm * @throws InvalidKeySpecException if private key or public key is invalid */ public SimpleIngestManager(String account, String user, String pipe, PrivateKey privateKey, String schemeName, String hostName, int port) throws NoSuchAlgorithmException, InvalidKeySpecException { KeyPair keyPair = createKeyPairFromPrivateKey(privateKey); //call our initializer method init(account, user, pipe, keyPair); //make the request builder we'll use to build messages to the service builder = new RequestBuilder(account, user, keyPair, schemeName, hostName, port); } /** * generate key pair object from private key * * @param privateKey private key * @return a key pair object * @throws NoSuchAlgorithmException if can't create key factory by using * RSA algorithm * @throws InvalidKeySpecException if private key or public key is invalid */ private KeyPair createKeyPairFromPrivateKey(PrivateKey privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException { if(!(privateKey instanceof RSAPrivateCrtKey)) throw new IllegalArgumentException("Input private key is not a RSA private key"); KeyFactory kf = KeyFactory.getInstance("RSA"); //generate public key from private key RSAPrivateCrtKey privk = (RSAPrivateCrtKey) privateKey; RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(privk.getModulus(), privk.getPublicExponent()); PublicKey publicK = kf.generatePublic(publicKeySpec); //create key pairs return new KeyPair(publicK, privateKey); } /** * getAccount - Gives back the name of the account * that this IngestManager is targeting * * @return the name of the account */ public String getAccount() { return account; } /** * getUser - gives back the user on behalf of * which this ingest manager is loading * * @return the user name */ public String getUser() { return user; } /** * getPipe - gives back the pipe which we are using * * @return the pipe name */ public String getPipe() { return pipe; } /** * wrapFilepaths - convenience method to take a list of filenames and * produce a list of FileWrappers with unset size * * @param filenames the filenames you want to wrap up * @return a corresponding list of StagedFileWrapper objects */ public static List<StagedFileWrapper> wrapFilepaths(Set<String> filenames) { //if we get a null, throw if (filenames == null) { throw new IllegalArgumentException(); } return filenames.parallelStream() .map(fname -> new StagedFileWrapper(fname, null)). collect(Collectors.toList()); } /** * ingestFile - ingest a single file * * @param file - a wrapper around a filename and size * @param requestId - a requestId that we'll use to label - if null, * we generate one for the user * @return an insert response from the server * @throws BackOffException - if we have a 503 response * @throws IOException - if we have some other network failure * @throws URISyntaxException - if the provided account name was illegal and * caused a URI construction failure */ public IngestResponse ingestFile(StagedFileWrapper file, UUID requestId) throws URISyntaxException, IOException, Exception { return ingestFiles(Collections.singletonList(file), requestId); } /** * ingestFiles - synchronously sends a request to the ingest * service to enqueue these files * * @param files - list of wrappers around filenames and sizes * @param requestId - a requestId that we'll use to label - if null, * we generate one for the user * @return an insert response from the server * @throws BackOffException - if we have a 503 response * @throws IOException - if we have some other network failure * @throws IngestResponseException - if snowflake encountered * error during ingest * @throws URISyntaxException - if the provided account name was illegal and * caused a URI construction failure */ public IngestResponse ingestFiles(List<StagedFileWrapper> files, UUID requestId) throws URISyntaxException, IOException, IngestResponseException { //the request id we want to send with this payload UUID request = requestId == null ? UUID.randomUUID() : requestId; //We're about to send this request number LOGGER.info("Sending Request UUID - ", request.toString()); //send the request and get a response.... HttpResponse response = httpClient.execute( builder.generateInsertRequest(request, pipe, files)); LOGGER.info("Attempting to unmarshall insert response - {}", response); return ServiceResponseHandler.unmarshallIngestResponse(response); } /** * Pings the service to see the current ingest history for this table * * @param requestId a UUID we use to label the request, if null, one is * generated for the user * @param recentSeconds history only for items in the recentSeconds window * @param beginMark mark from which history should be fetched * * @return a response showing the available ingest history from the service * * @throws BackOffException - if we have a 503 response * @throws IOException - if we have some other network failure * @throws IngestResponseException - if snowflake encountered a service error * @throws URISyntaxException - if the provided account name was illegal and * caused a URI construction failure */ public HistoryResponse getHistory(UUID requestId, Integer recentSeconds, String beginMark) throws URISyntaxException, IOException, IngestResponseException { //if we have no requestId generate one if (requestId == null) { requestId = UUID.randomUUID(); } //send the request and get a response... HttpResponse response = httpClient.execute( builder.generateHistoryRequest(requestId, pipe, recentSeconds, beginMark) ); LOGGER.info("Attempting to unmarshall history response - {}", response); return ServiceResponseHandler.unmarshallHistoryResponse(response); } /** * Pings the service to see the current ingest history for this table * * @param requestId a UUID we use to label the request, if null, one is * generated for the user * * @return a response showing the available ingest history from the service * * @throws BackOffException - if we have a 503 response * @throws IOException - if we have some other network failure * @throws IngestResponseException -if snowflake encountered a service error * @throws URISyntaxException - if the provided account name was illegal * and caused a URI construction failure */ public HistoryResponse getHistory(UUID requestId) throws URISyntaxException, IOException, IngestResponseException { return getHistory(requestId, null, null); } /** * Pings the service to see the current ingest history for this table * * @param requestId a UUID we use to label the request, if null, one is * generated for the user ** @param startTimeInclusive Start time inclusive of scan range, * in ISO-8601 format. Missing millisecond part in string will lead * to a zero milliseconds. This is a required query parameter, and * a 400 will be returned if this query parameter is missing * @param endTimeExclusive End time exclusive of scan range. If this * query parameter is missing or user provided value is later * than current millis, then current millis is used. * * @return a response showing the available ingest history from the service * * @throws BackOffException - if we have a 503 response * @throws IOException - if we have some other network failure * @throws IngestResponseException -if snowflake encountered a service error * @throws URISyntaxException - if the provided account name was illegal * and caused a URI construction failure */ public HistoryRangeResponse getHistoryRange(UUID requestId, String startTimeInclusive, String endTimeExclusive) throws URISyntaxException, IOException, IngestResponseException { if (requestId == null) { requestId = UUID.randomUUID(); } HttpResponse response = httpClient.execute( builder.generateHistoryRangeRequest(requestId, pipe, startTimeInclusive, endTimeExclusive) ); LOGGER.info("Attempting to unmarshall history range response - {}", response); return ServiceResponseHandler.unmarshallHistoryRangeResponse(response); } /** * Closes the resources associated with this object. Resources cannot be * reopened, initialize new instance of this class * {@link SimpleIngestManager} to reopen and start ingesting/monitoring new * data. */ @Override public void close() { builder.closeResources(); } }