diff --git a/.github/workflows/End2EndTest.yml b/.github/workflows/End2EndTest.yml
index a98c97fd2..91b16125e 100644
--- a/.github/workflows/End2EndTest.yml
+++ b/.github/workflows/End2EndTest.yml
@@ -67,3 +67,26 @@ jobs:
- name: Unit & Integration Test (Windows)
continue-on-error: false
run: mvn -DghActionsIT verify --batch-mode
+ build-e2e-jar-test:
+ name: E2E JAR Test - ${{ matrix.java }}, Cloud ${{ matrix.snowflake_cloud }}
+ runs-on: ubuntu-20.04
+ strategy:
+ fail-fast: false
+ matrix:
+ java: [ 8 ]
+ snowflake_cloud: [ 'AWS' ]
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v2
+ - name: Install Java ${{ matrix.java }}
+ uses: actions/setup-java@v2
+ with:
+ distribution: temurin
+ java-version: ${{ matrix.java }}
+ cache: maven
+ - name: Decrypt profile.json for Cloud ${{ matrix.snowflake_cloud }}
+ env:
+ DECRYPTION_PASSPHRASE: ${{ secrets.PROFILE_JSON_DECRYPT_PASSPHRASE }}
+ run: ./scripts/decrypt_secret.sh ${{ matrix.snowflake_cloud }}
+ - name: Run E2E JAR Test
+ run: ./e2e-jar-test/run_e2e_jar_test.sh
diff --git a/README.md b/README.md
index edbded16d..a29e9febe 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,25 @@ dependencies {
}
```
+## Jar Versions
+
+The Snowflake Ingest SDK provides shaded and unshaded versions of its jar. The shaded version bundles the dependencies into its own jar,
+whereas the unshaded version declares its dependencies in `pom.xml`, which are fetched as standard transitive dependencies by the build system like Maven or Gradle.
+The shaded JAR can help avoid potential dependency conflicts, but the unshaded version provides finer graned control over transitive dependencies.
+
+## Using with snowflake-jdbc-fics
+
+For use cases, which need to use `snowflake-jdbc-fips` instead of the default `snowflake-jdbc`, we recommend to take the following steps:
+
+- Use the unshaded version of the Ingest SDK.
+- Exclude these transitive dependencies:
+ - `net.snowflake:snowflake-jdbc`
+ - `org.bouncycastle:bcpkix-jdk18on`
+ - `org.bouncycastle:bcprov-jdk18on`
+- Add a dependency on `snowflake-jdbc-fips`.
+
+See [this test](https://github.com/snowflakedb/snowflake-ingest-java/tree/master/e2e-jar-test/fips) for an example how to use Snowflake Ingest SDK together with Snowflake FIPS JDBC Driver.
+
# Example
## Snowpipe
diff --git a/e2e-jar-test/core/pom.xml b/e2e-jar-test/core/pom.xml
new file mode 100644
index 000000000..36d52ae5b
--- /dev/null
+++ b/e2e-jar-test/core/pom.xml
@@ -0,0 +1,38 @@
+
+ 4.0.0
+
+ net.snowflake.snowflake-ingest-java-e2e-jar-test
+ parent
+ 1.0-SNAPSHOT
+
+
+ core
+ core
+ jar
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ net.snowflake
+ snowflake-ingest-sdk
+ provided
+
+
+
+ org.slf4j
+ slf4j-simple
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
diff --git a/e2e-jar-test/core/src/main/java/net/snowflake/IngestTestUtils.java b/e2e-jar-test/core/src/main/java/net/snowflake/IngestTestUtils.java
new file mode 100644
index 000000000..100972ea0
--- /dev/null
+++ b/e2e-jar-test/core/src/main/java/net/snowflake/IngestTestUtils.java
@@ -0,0 +1,169 @@
+package net.snowflake;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Random;
+import java.util.UUID;
+
+import net.snowflake.ingest.streaming.OpenChannelRequest;
+import net.snowflake.ingest.streaming.SnowflakeStreamingIngestChannel;
+import net.snowflake.ingest.streaming.SnowflakeStreamingIngestClient;
+import net.snowflake.ingest.streaming.SnowflakeStreamingIngestClientFactory;
+
+public class IngestTestUtils {
+ private static final String PROFILE_PATH = "profile.json";
+
+ private final Connection connection;
+
+ private final String database;
+ private final String schema;
+ private final String table;
+
+ private final String testId;
+
+ private final SnowflakeStreamingIngestClient client;
+
+ private final SnowflakeStreamingIngestChannel channel;
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ private final Random random = new Random();
+
+ private final Base64.Decoder base64Decoder = Base64.getDecoder();
+
+ public IngestTestUtils(String testName)
+ throws SQLException,
+ IOException,
+ ClassNotFoundException,
+ NoSuchAlgorithmException,
+ InvalidKeySpecException {
+ testId = String.format("%s_%s", testName, UUID.randomUUID().toString().replace("-", "_"));
+ connection = getConnection();
+ database = String.format("database_%s", testId);
+ schema = String.format("schema_%s", testId);
+ table = String.format("table_%s", testId);
+
+ connection.createStatement().execute(String.format("create database %s", database));
+ connection.createStatement().execute(String.format("create schema %s", schema));
+ connection.createStatement().execute(String.format("create table %s (c1 int, c2 varchar, c3 binary)", table));
+
+ client =
+ SnowflakeStreamingIngestClientFactory.builder("TestClient01")
+ .setProperties(loadProperties())
+ .build();
+
+ channel = client.openChannel(
+ OpenChannelRequest.builder(String.format("channel_%s", this.testId))
+ .setDBName(database)
+ .setSchemaName(schema)
+ .setTableName(table)
+ .setOnErrorOption(OpenChannelRequest.OnErrorOption.CONTINUE)
+ .build());
+ }
+
+ private Properties loadProperties() throws IOException {
+ Properties props = new Properties();
+ Iterator> propIt =
+ objectMapper.readTree(new String(Files.readAllBytes(Paths.get(PROFILE_PATH)))).fields();
+ while (propIt.hasNext()) {
+ Map.Entry prop = propIt.next();
+ props.put(prop.getKey(), prop.getValue().asText());
+ }
+ return props;
+ }
+
+ private Connection getConnection()
+ throws IOException, ClassNotFoundException, SQLException, NoSuchAlgorithmException, InvalidKeySpecException {
+ Class.forName("net.snowflake.client.jdbc.SnowflakeDriver");
+
+ Properties loadedProps = loadProperties();
+
+ byte[] decoded = base64Decoder.decode(loadedProps.getProperty("private_key"));
+ KeyFactory kf = KeyFactory.getInstance("RSA");
+
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded);
+ PrivateKey privateKey = kf.generatePrivate(keySpec);
+
+ Properties props = new Properties();
+ props.putAll(loadedProps);
+ props.put("client_session_keep_alive", "true");
+ props.put("privateKey", privateKey);
+
+ return DriverManager.getConnection(loadedProps.getProperty("connect_string"), props);
+ }
+
+ private Map createRow() {
+ Map row = new HashMap<>();
+
+ byte[] bytes = new byte[1024];
+ random.nextBytes(bytes);
+
+ row.put("c1", random.nextInt());
+ row.put("c2", String.valueOf(random.nextInt()));
+ row.put("c3", bytes);
+
+ return row;
+ }
+
+ /**
+ * Given a channel and expected offset, this method waits up to 60 seconds until the last
+ * committed offset is equal to the passed offset
+ */
+ private void waitForOffset(SnowflakeStreamingIngestChannel channel, String expectedOffset)
+ throws InterruptedException {
+ int counter = 0;
+ String lastCommittedOffset = null;
+ while (counter < 600) {
+ String currentOffset = channel.getLatestCommittedOffsetToken();
+ if (expectedOffset.equals(currentOffset)) {
+ return;
+ }
+ System.out.printf("Waiting for offset expected=%s actual=%s%n", expectedOffset, currentOffset);
+ lastCommittedOffset = currentOffset;
+ counter++;
+ Thread.sleep(100);
+ }
+ throw new RuntimeException(
+ String.format(
+ "Timeout exceeded while waiting for offset %s. Last committed offset: %s",
+ expectedOffset, lastCommittedOffset));
+ }
+
+ public void test() throws InterruptedException {
+ // Insert few rows one by one
+ for (int offset = 2; offset < 1000; offset++) {
+ offset++;
+ channel.insertRow(createRow(), String.valueOf(offset));
+ }
+
+ // Insert a batch of rows
+ String offset = "final-offset";
+ channel.insertRows(
+ Arrays.asList(createRow(), createRow(), createRow(), createRow(), createRow()), offset);
+
+ waitForOffset(channel, offset);
+ }
+
+ public void close() throws Exception {
+ connection.close();
+ channel.close().get();
+ client.close();
+ }
+}
diff --git a/e2e-jar-test/fips/pom.xml b/e2e-jar-test/fips/pom.xml
new file mode 100644
index 000000000..039af9b7a
--- /dev/null
+++ b/e2e-jar-test/fips/pom.xml
@@ -0,0 +1,53 @@
+
+ 4.0.0
+
+ net.snowflake.snowflake-ingest-java-e2e-jar-test
+ parent
+ 1.0-SNAPSHOT
+
+
+ fips
+ jar
+ fips
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+ net.snowflake.snowflake-ingest-java-e2e-jar-test
+ core
+
+
+
+ net.snowflake
+ snowflake-ingest-sdk
+
+
+ net.snowflake
+ snowflake-jdbc
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+
+
+
+
+ net.snowflake
+ snowflake-jdbc-fips
+
+
+ junit
+ junit
+
+
+
diff --git a/e2e-jar-test/fips/src/test/java/net/snowflake/FipsIngestE2ETest.java b/e2e-jar-test/fips/src/test/java/net/snowflake/FipsIngestE2ETest.java
new file mode 100644
index 000000000..7279f23ff
--- /dev/null
+++ b/e2e-jar-test/fips/src/test/java/net/snowflake/FipsIngestE2ETest.java
@@ -0,0 +1,31 @@
+package net.snowflake;
+
+import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.security.Security;
+
+public class FipsIngestE2ETest {
+
+ private IngestTestUtils ingestTestUtils;
+
+ @Before
+ public void setUp() throws Exception {
+ // Add FIPS provider, the SDK does not do this by default
+ Security.addProvider(new BouncyCastleFipsProvider("C:HYBRID;ENABLE{All};"));
+
+ ingestTestUtils = new IngestTestUtils("fips_ingest");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ ingestTestUtils.close();
+ }
+
+ @Test
+ public void name() throws InterruptedException {
+ ingestTestUtils.test();
+ }
+}
diff --git a/e2e-jar-test/pom.xml b/e2e-jar-test/pom.xml
new file mode 100644
index 000000000..9368308fc
--- /dev/null
+++ b/e2e-jar-test/pom.xml
@@ -0,0 +1,59 @@
+
+ 4.0.0
+
+ net.snowflake.snowflake-ingest-java-e2e-jar-test
+ parent
+ 1.0-SNAPSHOT
+ pom
+
+ snowflake-ingest-sdk-e2e-test
+
+
+ standard
+ fips
+ core
+
+
+
+
+
+
+ net.snowflake.snowflake-ingest-java-e2e-jar-test
+ core
+ ${project.version}
+
+
+
+ net.snowflake
+ snowflake-ingest-sdk
+ 2.0.4-SNAPSHOT
+
+
+
+ net.snowflake
+ snowflake-jdbc-fips
+ 3.13.30
+
+
+
+ org.slf4j
+ slf4j-simple
+ 1.7.36
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.14.0
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+
+
diff --git a/e2e-jar-test/run_e2e_jar_test.sh b/e2e-jar-test/run_e2e_jar_test.sh
new file mode 100755
index 000000000..e5e9b5c55
--- /dev/null
+++ b/e2e-jar-test/run_e2e_jar_test.sh
@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+## This script tests the SDK JARs end-to-end, i.e. not using integration tests from within the project, but from an
+## external Maven project, which depends on the SDK deployed into the local maven repository. The following SDK variants are tested:
+## 1. Shaded jar
+## 2. Unshaded jar
+## 3. FIPS-compliant jar, i.e. unshaded jar without snowflake-jdbc and bouncy castle dependencies, but with snowflake-jdbc-fips depedency
+
+maven_repo_dir=$(mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout)
+sdk_repo_dir="${maven_repo_dir}/net/snowflake/snowflake-ingest-sdk"
+
+cp profile.json e2e-jar-test/standard
+cp profile.json e2e-jar-test/fips
+
+###################
+# TEST SHADED JAR #
+###################
+
+# Remove the SDK from local maven repository
+rm -fr $sdk_repo_dir
+
+# Prepare pom.xml for shaded JAR
+project_version=$(./scripts/get_project_info_from_pom.py pom.xml version)
+./scripts/update_project_version.py public_pom.xml $project_version > generated_public_pom.xml
+
+# Build shaded SDK
+mvn clean package -DskipTests=true --batch-mode --show-version
+
+# Install shaded SDK JARs into local maven repository
+mvn install:install-file -Dfile=target/snowflake-ingest-sdk.jar -DpomFile=generated_public_pom.xml
+
+# Run e2e tests
+(cd e2e-jar-test && mvn clean verify -pl standard -am)
+
+#####################
+# TEST UNSHADED JAR #
+#####################
+
+# Remove the SDK from local maven repository
+rm -r $sdk_repo_dir
+
+# Install unshaded SDK into local maven repository
+mvn clean install -Dnot-shadeDep -DskipTests=true --batch-mode --show-version
+
+# Run e2e tests
+(cd e2e-jar-test && mvn clean verify -pl standard -am)
+
+#############
+# TEST FIPS #
+#############
+
+# Remove the SDK from local maven repository
+rm -r $sdk_repo_dir
+
+# Install unshaded SDK into local maven repository
+mvn clean install -Dnot-shadeDep -DskipTests=true --batch-mode --show-version
+
+# Run e2e tests on the FIPS module
+(cd e2e-jar-test && mvn clean verify -pl fips -am)
\ No newline at end of file
diff --git a/e2e-jar-test/standard/pom.xml b/e2e-jar-test/standard/pom.xml
new file mode 100644
index 000000000..2fdddcddd
--- /dev/null
+++ b/e2e-jar-test/standard/pom.xml
@@ -0,0 +1,34 @@
+
+ 4.0.0
+
+ net.snowflake.snowflake-ingest-java-e2e-jar-test
+ parent
+ 1.0-SNAPSHOT
+
+
+ standard
+ jar
+ unshaded
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+ net.snowflake.snowflake-ingest-java-e2e-jar-test
+ core
+
+
+ net.snowflake
+ snowflake-ingest-sdk
+
+
+ junit
+ junit
+
+
+
diff --git a/e2e-jar-test/standard/src/test/java/net/snowflake/StandardIngestE2ETest.java b/e2e-jar-test/standard/src/test/java/net/snowflake/StandardIngestE2ETest.java
new file mode 100644
index 000000000..255577655
--- /dev/null
+++ b/e2e-jar-test/standard/src/test/java/net/snowflake/StandardIngestE2ETest.java
@@ -0,0 +1,25 @@
+package net.snowflake;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class StandardIngestE2ETest {
+
+ private IngestTestUtils ingestTestUtils;
+
+ @Before
+ public void setUp() throws Exception {
+ ingestTestUtils = new IngestTestUtils("standard_ingest");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ ingestTestUtils.close();
+ }
+
+ @Test
+ public void name() throws InterruptedException {
+ ingestTestUtils.test();
+ }
+}
diff --git a/linkage-checker-exclusion-rules.xml b/linkage-checker-exclusion-rules.xml
index 3544c6d48..0cb2eb38c 100644
--- a/linkage-checker-exclusion-rules.xml
+++ b/linkage-checker-exclusion-rules.xml
@@ -19,6 +19,18 @@
Google Crypto Tink is an optional dependency of nimbus-jose-jwt
+
+
+
+
+ Seems like a false positive, this class does exist on classpath.
+
+
+
+
+
+ Seems like a false positive, this class does exist on classpath.
+