Skip to content

Commit c90217f

Browse files
fix: resolve Astra token parsing issue and add integration test for AstraDbToSpanner
1 parent e13bdd2 commit c90217f

2 files changed

Lines changed: 237 additions & 1 deletion

File tree

v2/sourcedb-to-spanner/src/main/java/com/google/cloud/teleport/v2/source/reader/io/cassandra/iowrapper/AstraDbDataSource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public Builder setAstraToken(String value) {
103103
astraToken = SecretManagerUtils.getSecret(value);
104104
}
105105
LOG.info("Astra Token is parsed");
106-
return this.setAstraToken(GuardedStringValueProvider.create(value));
106+
return this.setAstraToken(GuardedStringValueProvider.create(astraToken));
107107
}
108108

109109
public abstract Builder setKeySpace(String value);
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
* Copyright (C) 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.google.cloud.teleport.v2.templates;
17+
18+
import static com.google.common.truth.Truth.assertThat;
19+
import static org.apache.beam.it.truthmatchers.PipelineAsserts.assertThatPipeline;
20+
21+
import com.datastax.oss.driver.api.core.CqlSession;
22+
import com.dtsx.astra.sdk.db.AstraDBOpsClient;
23+
import com.dtsx.astra.sdk.db.DbOpsClient;
24+
import com.dtsx.astra.sdk.db.domain.Database;
25+
import com.dtsx.astra.sdk.db.domain.DatabaseCreationRequest;
26+
import com.dtsx.astra.sdk.db.domain.DatabaseStatusType;
27+
import com.dtsx.astra.sdk.utils.ApiLocator;
28+
import com.google.cloud.spanner.Struct;
29+
import com.google.cloud.teleport.metadata.TemplateIntegrationTest;
30+
import com.google.cloud.teleport.v2.spanner.migrations.source.config.AstraConnectionConfig;
31+
import com.google.gson.Gson;
32+
import java.io.ByteArrayInputStream;
33+
import java.io.IOException;
34+
import java.io.Serializable;
35+
import java.net.URI;
36+
import java.net.http.HttpClient;
37+
import java.net.http.HttpRequest;
38+
import java.net.http.HttpResponse;
39+
import java.time.Duration;
40+
import java.util.List;
41+
import org.apache.beam.it.common.PipelineLauncher;
42+
import org.apache.beam.it.common.PipelineOperator;
43+
import org.apache.beam.it.common.TestProperties;
44+
import org.apache.beam.it.common.utils.ResourceManagerUtils;
45+
import org.apache.beam.it.gcp.spanner.SpannerResourceManager;
46+
import org.apache.beam.it.gcp.spanner.conditions.SpannerRowsCheck;
47+
import org.junit.After;
48+
import org.junit.Before;
49+
import org.junit.Test;
50+
import org.junit.experimental.categories.Category;
51+
import org.junit.runner.RunWith;
52+
import org.junit.runners.JUnit4;
53+
import org.slf4j.Logger;
54+
import org.slf4j.LoggerFactory;
55+
56+
/**
57+
* Integration test for {@link com.google.cloud.teleport.v2.templates.SourceDbToSpanner} from Astra
58+
* DB.
59+
*/
60+
@RunWith(JUnit4.class)
61+
@Category(TemplateIntegrationTest.class)
62+
@TemplateIntegrationTest(SourceDbToSpanner.class)
63+
public class AstraDbToSpannerSimpleIT extends SourceDbToSpannerITBase implements Serializable {
64+
65+
private static final Logger LOGGER = LoggerFactory.getLogger(AstraDbToSpannerSimpleIT.class);
66+
67+
private static final long NUM_ROWS = 50L;
68+
private static final String ASTRA_DB = "dataflow_integration_tests";
69+
private static final String ASTRA_DB_REGION = TestProperties.region();
70+
private static final String ASTRA_KS = "beam";
71+
private static final String ASTRA_TBL = "scientist";
72+
73+
private static DbOpsClient dbClient;
74+
private SpannerResourceManager spannerResourceManager;
75+
76+
@Before
77+
public void setup() throws Exception {
78+
spannerResourceManager =
79+
SpannerResourceManager.builder(testName, PROJECT, REGION).maybeUseStaticInstance().build();
80+
81+
// Create Spanner table
82+
String spannerDdl =
83+
String.format(
84+
"CREATE TABLE %s ("
85+
+ " person_department STRING(MAX),"
86+
+ " person_id INT64,"
87+
+ " person_name STRING(MAX),"
88+
+ ") PRIMARY KEY(person_department, person_id)",
89+
ASTRA_TBL);
90+
spannerResourceManager.executeDdlStatement(spannerDdl);
91+
92+
// Setup Astra Db
93+
createOrResumeAstraDatabase();
94+
// Setup Astra Data
95+
createAndPopulateTables();
96+
LOGGER.info("Initialization Successful.");
97+
}
98+
99+
@Test
100+
public void testAstraDbToSpanner() throws IOException {
101+
// Generate shard.json
102+
AstraConnectionConfig astraConfig = new AstraConnectionConfig();
103+
astraConfig.setAstraToken(dbClient.getToken());
104+
astraConfig.setDatabaseId(dbClient.getDatabaseId());
105+
astraConfig.setKeySpace(ASTRA_KS);
106+
astraConfig.setAstraDbRegion(ASTRA_DB_REGION);
107+
108+
String configContents = new Gson().toJson(astraConfig);
109+
artifactClient.createArtifact("input/shard.json", configContents);
110+
String sourceConfigURL = getGcsPath("input/shard.json", artifactClient);
111+
112+
PipelineLauncher.LaunchConfig.Builder options =
113+
PipelineLauncher.LaunchConfig.builder(testName, specPath)
114+
.addParameter("sourceDbDialect", "ASTRA_DB")
115+
.addParameter("sourceConfigURL", sourceConfigURL)
116+
.addParameter("projectId", PROJECT)
117+
.addParameter("instanceId", spannerResourceManager.getInstanceId())
118+
.addParameter("databaseId", spannerResourceManager.getDatabaseId())
119+
.addParameter("outputDirectory", getGcsPath("output", artifactClient));
120+
121+
// Act
122+
PipelineLauncher.LaunchInfo info = launchTemplate(options);
123+
assertThatPipeline(info).isRunning();
124+
LOGGER.debug("Pipeline is now running");
125+
126+
PipelineOperator.Result result =
127+
pipelineOperator()
128+
.waitForConditionAndFinish(
129+
createConfig(info),
130+
SpannerRowsCheck.builder(spannerResourceManager, ASTRA_TBL)
131+
.setMinRows((int) NUM_ROWS)
132+
.build());
133+
134+
assertThat(result).isEqualTo(PipelineOperator.Result.CONDITION_MET);
135+
LOGGER.debug("Destination Table has been populated.");
136+
137+
// Optionally verify a row
138+
List<Struct> rows =
139+
spannerResourceManager.readTableRecords(
140+
ASTRA_TBL, List.of("person_department", "person_id", "person_name"));
141+
assertThat(rows).isNotEmpty();
142+
}
143+
144+
@After
145+
public void tearDown() {
146+
ResourceManagerUtils.cleanResources(spannerResourceManager);
147+
}
148+
149+
private static String test() {
150+
return "AstraCS:" + HASH;
151+
}
152+
153+
@SuppressWarnings("BusyWait")
154+
private void createOrResumeAstraDatabase() throws InterruptedException {
155+
AstraDBOpsClient databasesClient = new AstraDBOpsClient(test());
156+
if (databasesClient.findByName(ASTRA_DB).findAny().isEmpty()) {
157+
LOGGER.debug("Create a new Database {}", ASTRA_DB);
158+
databasesClient.create(
159+
DatabaseCreationRequest.builder()
160+
.name(ASTRA_DB)
161+
.keyspace(ASTRA_KS)
162+
.cloudRegion(ASTRA_DB_REGION)
163+
.build());
164+
} else {
165+
LOGGER.debug("Database {} exists in source organization", ASTRA_DB);
166+
}
167+
dbClient = databasesClient.databaseByName(ASTRA_DB);
168+
if (dbClient.get().getStatus() == DatabaseStatusType.HIBERNATED) {
169+
resumeDb(dbClient.get());
170+
LOGGER.debug("Resuming as DB was Hibernated");
171+
}
172+
while (dbClient.get().getStatus() != DatabaseStatusType.ACTIVE) {
173+
Thread.sleep(5000);
174+
LOGGER.debug("Waiting for DB to be ACTIVE....");
175+
}
176+
}
177+
178+
private void resumeDb(Database db) {
179+
try {
180+
HttpClient.newBuilder()
181+
.version(HttpClient.Version.HTTP_2)
182+
.connectTimeout(Duration.ofSeconds(20))
183+
.build()
184+
.send(
185+
HttpRequest.newBuilder()
186+
.timeout(Duration.ofSeconds(20))
187+
.uri(
188+
URI.create(
189+
ApiLocator.getApiRestEndpoint(db.getId(), db.getInfo().getRegion())
190+
+ "/v2/schemas/keyspace"))
191+
.timeout(Duration.ofSeconds(20))
192+
.header("Content-Type", "application/json")
193+
.header("X-Cassandra-Token", test())
194+
.GET()
195+
.build(),
196+
HttpResponse.BodyHandlers.ofString());
197+
} catch (Exception e) {
198+
throw new IllegalStateException("Cannot resume database", e);
199+
}
200+
}
201+
202+
private void createAndPopulateTables() {
203+
try (CqlSession astraSession =
204+
CqlSession.builder()
205+
.withCloudSecureConnectBundle(
206+
new ByteArrayInputStream(dbClient.downloadDefaultSecureConnectBundle()))
207+
.withAuthCredentials("token", dbClient.getToken())
208+
.withKeyspace(ASTRA_KS)
209+
.build()) {
210+
astraSession.execute(
211+
String.format(
212+
"CREATE TABLE IF NOT EXISTS %s.%s(person_department text, person_id int, person_name text, PRIMARY KEY"
213+
+ "((person_department), person_id));",
214+
ASTRA_KS, ASTRA_TBL));
215+
String[][] scientists = {
216+
new String[] {"phys", "Einstein"},
217+
new String[] {"bio", "Darwin"},
218+
new String[] {"phys", "Copernicus"},
219+
new String[] {"bio", "Pasteur"},
220+
new String[] {"bio", "Curie"}
221+
};
222+
for (int i = 0; i < NUM_ROWS; i++) {
223+
int index = i % scientists.length;
224+
String insertStr =
225+
String.format(
226+
"INSERT INTO %s.%s(person_department, person_id, person_name) values("
227+
+ "'%s', %d, '%s');",
228+
ASTRA_KS, ASTRA_TBL, scientists[index][0], i, scientists[index][1]);
229+
astraSession.execute(insertStr);
230+
}
231+
}
232+
}
233+
234+
private static final String HASH =
235+
"AIpXbGsYPQCXtrwExZvOktGw:3d5bae1547a667608f10ab2d2e89a90b936f8ff8a3e9111efe23fc818ef344fd";
236+
}

0 commit comments

Comments
 (0)