Skip to content

Commit 87c553c

Browse files
CASSANALYTICS-155: Add IAM credential support for S3 storage transport
Adds IAM-based authentication as an alternative to static credentials for S3 storage transport. Also fixes test report collection in CI: corrects Gradle output paths for both GitHub Actions (adds missing artifact uploads with if: always()) and CircleCI (fixes SRC_REPORT_DIR and store_test_results paths so real JUnit XML is collected instead of synthesized fallbacks). Patch by Jon Haddad; reviewed by Yifan Cai for CASSANALYTICS-155
1 parent 1fc500d commit 87c553c

48 files changed

Lines changed: 1047 additions & 332 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.circleci/config.yml

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ commands:
116116
# files on each run, so without aggregation only the last class's XML
117117
# would survive, hiding most results from the CircleCI dashboard.
118118
# Drain reports into a per-class subdirectory after each iteration.
119-
SRC_REPORT_DIR="$(pwd)/build/test-reports/integration"
119+
SRC_REPORT_DIR="$(pwd)/build/test-results/cassandra-analytics-integration-tests/test"
120+
HTML_REPORT_DIR="$(pwd)/build/reports/tests/cassandra-analytics-integration-tests/test"
120121
AGG_ROOT="$(pwd)/build/aggregated-test-reports/integration"
121122
mkdir -p "$AGG_ROOT"
122123
# collect up exit statuses for all of the test classes and exit with that result at the end.
@@ -133,10 +134,10 @@ commands:
133134
mv "$f" "$DEST"/
134135
MOVED=1
135136
done
136-
for f in "$SRC_REPORT_DIR"/*.html; do
137-
[ -e "$f" ] || continue
138-
mv "$f" "$DEST"/ 2>/dev/null || true
139-
done
137+
fi
138+
if [ -d "$HTML_REPORT_DIR" ]; then
139+
mkdir -p "$DEST/html"
140+
cp -r "$HTML_REPORT_DIR/." "$DEST/html/"
140141
fi
141142
# If Gradle produced no XML (e.g. class-level crash before any
142143
# @Test ran), synthesize a minimal JUnit record so CircleCI's
@@ -244,8 +245,8 @@ jobs:
244245

245246
- store_artifacts:
246247
when: always
247-
path: build/test-reports
248-
destination: test-reports
248+
path: build/test-results
249+
destination: test-results
249250

250251
- store_artifacts:
251252
when: always
@@ -254,7 +255,7 @@ jobs:
254255

255256
- store_test_results:
256257
when: always
257-
path: build/test-reports
258+
path: build/test-results
258259

259260
# Single parameterized integration-test job, invoked once per Cassandra/Scala
260261
# compatibility group from the workflow's `matrix:` block.
@@ -286,8 +287,8 @@ jobs:
286287

287288
- store_artifacts:
288289
when: always
289-
path: build/test-reports
290-
destination: test-reports
290+
path: build/aggregated-test-reports
291+
destination: test-results
291292

292293
- store_artifacts:
293294
when: always

.github/workflows/test.yaml

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,15 @@ jobs:
200200
fi
201201
202202
./gradlew --stacktrace clean assemble check -x cassandra-analytics-integration-tests:test -Dcassandra.analytics.bridges.sstable_format=${{ matrix.sstable-format }}
203+
- name: Upload test results
204+
if: always()
205+
uses: actions/upload-artifact@v4
206+
with:
207+
name: unit-test-results-s${{ matrix.scala }}-${{ matrix.sstable-format }}-C${{ matrix.cassandra }}-Spark${{ matrix.spark }}-JDK${{ matrix.jdk }}
208+
path: |
209+
build/test-results/**
210+
build/reports/tests/**
211+
retention-days: 30
203212

204213
integration-test:
205214
name: Integration test - ${{ matrix.config }} (${{ matrix.job_index }})
@@ -302,23 +311,23 @@ jobs:
302311
done
303312
304313
test_id=$(date +%H%M%S)
305-
mkdir -p test-reports/$test_id
314+
mkdir -p "test-reports/$test_id/html"
306315
307316
if [ $SKIP == "false" ]; then
308317
echo Executing test $TEST_NAME
309318
./gradlew --stacktrace cassandra-analytics-integration-tests:test --tests $TEST_NAME --no-daemon || EXIT_STATUS=$?;
310-
mv build/test-reports/* test-reports/$test_id
319+
find build/test-results -name "*.xml" -exec cp {} "test-reports/$test_id/" \; 2>/dev/null || true
320+
[ -d "build/reports/tests" ] && cp -r build/reports/tests/. "test-reports/$test_id/html/" 2>/dev/null || true
311321
else
312322
echo "Skipping test $TEST_NAME"
313323
fi
314324
done;
315325
316-
tar czf integration-tests.tar.gz test-reports/*
317-
318326
exit $EXIT_STATUS
319-
- name: Publish test results
327+
- name: Upload test results
328+
if: always()
320329
uses: actions/upload-artifact@v4
321-
if: (!cancelled())
322330
with:
323331
name: integration-tests-${{ matrix.config }}-${{ matrix.job_index }}
324-
path: integration-tests.tar.gz
332+
path: test-reports/
333+
retention-days: 30

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
0.5.0
22
-----
33
* Spark 4.0 Support (CASSANALYTICS-34)
4+
* Add IAM credential support for S3 storage transport (CASSANALYTICS-155)
45
* Make BulkWriterConfig extensible (CASSANALYTICS-168)
56

67
0.4.0

analytics-sidecar-client-common/build.gradle

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,6 @@ if (propertyWithDefault("artifactType", null) == "common") {
3434
test {
3535
useJUnitPlatform()
3636
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
37-
reports {
38-
junitXml.setRequired(true)
39-
def destDir = Paths.get(rootProject.rootDir.absolutePath, "build", "test-results", "client-common").toFile()
40-
println("Destination directory for client-common tests: ${destDir}")
41-
junitXml.getOutputLocation().set(destDir)
42-
html.setRequired(true)
43-
html.getOutputLocation().set(destDir)
44-
}
4537
}
4638

4739
dependencies {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.cassandra.sidecar.common.data;
20+
21+
/**
22+
* Declares the credential type used by a restore job to authenticate with cloud storage.
23+
*/
24+
public enum CredentialType
25+
{
26+
/** Static STS credentials: accessKeyId, secretAccessKey, and sessionToken must all be present. */
27+
STATIC,
28+
/** IAM instance profile (or ECS task role / IRSA): only region is required; key fields must be absent. */
29+
IAM
30+
}

analytics-sidecar-client-common/src/main/java/org/apache/cassandra/sidecar/common/data/RestoreJobConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public class RestoreJobConstants
5858
public static final String SLICE_COMPRESSED_SIZE = "sliceCompressedSize";
5959
public static final String SECRET_READ_CREDENTIALS = "readCredentials";
6060
public static final String SECRET_WRITE_CREDENTIALS = "writeCredentials";
61+
public static final String JOB_CREDENTIAL_TYPE = "credentialType";
6162
public static final String CREDENTIALS_ACCESS_KEY_ID = "accessKeyId";
6263
public static final String CREDENTIALS_SECRET_ACCESS_KEY = "secretAccessKey";
6364
public static final String CREDENTIALS_SESSION_TOKEN = "sessionToken";

analytics-sidecar-client-common/src/main/java/org/apache/cassandra/sidecar/common/data/RestoreJobSecrets.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@
3131
*/
3232
public class RestoreJobSecrets
3333
{
34+
/**
35+
* Creates a {@link RestoreJobSecrets} for IAM instance profile mode.
36+
* Only the regions are provided; the sidecar resolves credentials via the AWS default credential chain.
37+
*
38+
* @param readRegion the AWS region of the S3 bucket used for reading
39+
* @param writeRegion the AWS region of the S3 bucket used for writing
40+
* @return region-only {@link RestoreJobSecrets} signalling IAM mode
41+
*/
42+
public static RestoreJobSecrets iamMode(String readRegion, String writeRegion)
43+
{
44+
StorageCredentials read = StorageCredentials.builder().region(readRegion).build();
45+
StorageCredentials write = StorageCredentials.builder().region(writeRegion).build();
46+
return new RestoreJobSecrets(read, write);
47+
}
48+
3449
private final StorageCredentials writeCredentials;
3550
private final StorageCredentials readCredentials;
3651

analytics-sidecar-client-common/src/main/java/org/apache/cassandra/sidecar/common/data/StorageCredentials.java

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,26 @@
2121
import java.util.Objects;
2222

2323
import com.fasterxml.jackson.annotation.JsonCreator;
24+
import com.fasterxml.jackson.annotation.JsonInclude;
2425
import com.fasterxml.jackson.annotation.JsonProperty;
26+
import org.jetbrains.annotations.Nullable;
2527

2628
import static org.apache.cassandra.sidecar.common.data.RestoreJobConstants.CREDENTIALS_ACCESS_KEY_ID;
2729
import static org.apache.cassandra.sidecar.common.data.RestoreJobConstants.CREDENTIALS_REGION;
2830
import static org.apache.cassandra.sidecar.common.data.RestoreJobConstants.CREDENTIALS_SECRET_ACCESS_KEY;
2931
import static org.apache.cassandra.sidecar.common.data.RestoreJobConstants.CREDENTIALS_SESSION_TOKEN;
3032

3133
/**
32-
* Used for storing credential details needed for either reading/writing to S3
34+
* Used for storing credential details needed for either reading/writing to S3.
35+
* In IAM mode only {@code region} is required; the key fields are absent and the sidecar
36+
* resolves credentials via the AWS default credential chain.
3337
*/
38+
@JsonInclude(JsonInclude.Include.NON_NULL)
3439
public class StorageCredentials
3540
{
36-
private final String accessKeyId;
37-
private final String secretAccessKey;
38-
private final String sessionToken;
41+
private final @Nullable String accessKeyId;
42+
private final @Nullable String secretAccessKey;
43+
private final @Nullable String sessionToken;
3944
private final String region;
4045

4146
public static Builder builder()
@@ -44,14 +49,11 @@ public static Builder builder()
4449
}
4550

4651
@JsonCreator
47-
public StorageCredentials(@JsonProperty(CREDENTIALS_ACCESS_KEY_ID) String accessKeyId,
48-
@JsonProperty(CREDENTIALS_SECRET_ACCESS_KEY) String secretAccessKey,
49-
@JsonProperty(CREDENTIALS_SESSION_TOKEN) String sessionToken,
52+
public StorageCredentials(@JsonProperty(CREDENTIALS_ACCESS_KEY_ID) @Nullable String accessKeyId,
53+
@JsonProperty(CREDENTIALS_SECRET_ACCESS_KEY) @Nullable String secretAccessKey,
54+
@JsonProperty(CREDENTIALS_SESSION_TOKEN) @Nullable String sessionToken,
5055
@JsonProperty(CREDENTIALS_REGION) String region)
5156
{
52-
Objects.requireNonNull(accessKeyId, "accessKeyId must be supplied");
53-
Objects.requireNonNull(secretAccessKey, "secretAccessKey must be supplied");
54-
Objects.requireNonNull(sessionToken, "sessionToken must be supplied");
5557
Objects.requireNonNull(region, "region must be supplied");
5658
this.accessKeyId = accessKeyId;
5759
this.secretAccessKey = secretAccessKey;
@@ -60,19 +62,19 @@ public StorageCredentials(@JsonProperty(CREDENTIALS_ACCESS_KEY_ID) String access
6062
}
6163

6264
@JsonProperty(CREDENTIALS_ACCESS_KEY_ID)
63-
public String accessKeyId()
65+
public @Nullable String accessKeyId()
6466
{
6567
return accessKeyId;
6668
}
6769

6870
@JsonProperty(CREDENTIALS_SECRET_ACCESS_KEY)
69-
public String secretAccessKey()
71+
public @Nullable String secretAccessKey()
7072
{
7173
return secretAccessKey;
7274
}
7375

7476
@JsonProperty(CREDENTIALS_SESSION_TOKEN)
75-
public String sessionToken()
77+
public @Nullable String sessionToken()
7678
{
7779
return sessionToken;
7880
}
@@ -83,6 +85,15 @@ public String region()
8385
return region;
8486
}
8587

88+
/**
89+
* Returns true if all three key fields are non-null, indicating static AWS credentials are present.
90+
* A false result means key fields are absent, which is expected in IAM mode.
91+
*/
92+
public boolean hasStaticCredentials()
93+
{
94+
return accessKeyId != null && secretAccessKey != null && sessionToken != null;
95+
}
96+
8697
/**
8798
* @return secrets string with redacted values
8899
*/
@@ -123,13 +134,13 @@ public int hashCode()
123134
}
124135

125136
/**
126-
* Builds the RestoreJobSecrets
137+
* Builds the StorageCredentials
127138
*/
128139
public static class Builder
129140
{
130-
private String accessKeyId;
131-
private String secretAccessKey;
132-
private String sessionToken;
141+
private @Nullable String accessKeyId;
142+
private @Nullable String secretAccessKey;
143+
private @Nullable String sessionToken;
133144
private String region;
134145

135146
private Builder()

0 commit comments

Comments
 (0)