Skip to content

Commit d0a5547

Browse files
feat: clone sources and destinations API (#10396)
* feat: clone sources and destinations API * fix: source is pulled without secrets
1 parent ccb3fe2 commit d0a5547

File tree

7 files changed

+314
-0
lines changed

7 files changed

+314
-0
lines changed

airbyte-api/src/main/openapi/config.yaml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,29 @@ paths:
494494
$ref: "#/components/schemas/SourceReadList"
495495
"422":
496496
$ref: "#/components/responses/InvalidInputResponse"
497+
/v1/sources/clone:
498+
post:
499+
tags:
500+
- source
501+
summary: Clone source
502+
operationId: cloneSource
503+
requestBody:
504+
content:
505+
application/json:
506+
schema:
507+
$ref: "#/components/schemas/SourceIdRequestBody"
508+
required: true
509+
responses:
510+
"200":
511+
description: Successful operation
512+
content:
513+
application/json:
514+
schema:
515+
$ref: "#/components/schemas/SourceRead"
516+
"404":
517+
$ref: "#/components/responses/NotFoundResponse"
518+
"422":
519+
$ref: "#/components/responses/InvalidInputResponse"
497520
/v1/sources/delete:
498521
post:
499522
tags:
@@ -892,6 +915,29 @@ paths:
892915
$ref: "#/components/responses/NotFoundResponse"
893916
"422":
894917
$ref: "#/components/responses/InvalidInputResponse"
918+
/v1/destinations/clone:
919+
post:
920+
tags:
921+
- destination
922+
summary: Clone destination
923+
operationId: cloneDestination
924+
requestBody:
925+
content:
926+
application/json:
927+
schema:
928+
$ref: "#/components/schemas/DestinationIdRequestBody"
929+
required: true
930+
responses:
931+
"200":
932+
description: Successful operation
933+
content:
934+
application/json:
935+
schema:
936+
$ref: "#/components/schemas/DestinationRead"
937+
"404":
938+
$ref: "#/components/responses/NotFoundResponse"
939+
"422":
940+
$ref: "#/components/responses/InvalidInputResponse"
895941
/v1/connections/create:
896942
post:
897943
tags:

airbyte-server/src/main/java/io/airbyte/server/apis/ConfigurationApi.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,11 @@ public void deleteSource(final SourceIdRequestBody sourceIdRequestBody) {
403403
});
404404
}
405405

406+
@Override
407+
public SourceRead cloneSource(final SourceIdRequestBody sourceIdRequestBody) {
408+
return execute(() -> sourceHandler.cloneSource(sourceIdRequestBody));
409+
}
410+
406411
@Override
407412
public CheckConnectionRead checkConnectionToSource(final SourceIdRequestBody sourceIdRequestBody) {
408413
return execute(() -> schedulerHandler.checkSourceConnectionFromSourceId(sourceIdRequestBody));
@@ -507,6 +512,11 @@ public DestinationRead getDestination(final DestinationIdRequestBody destination
507512
return execute(() -> destinationHandler.getDestination(destinationIdRequestBody));
508513
}
509514

515+
@Override
516+
public DestinationRead cloneDestination(final DestinationIdRequestBody destinationIdRequestBody) {
517+
return execute(() -> destinationHandler.cloneDestination(destinationIdRequestBody));
518+
}
519+
510520
@Override
511521
public CheckConnectionRead checkConnectionToDestination(final DestinationIdRequestBody destinationIdRequestBody) {
512522
return execute(() -> schedulerHandler.checkDestinationConnectionFromDestinationId(destinationIdRequestBody));

airbyte-server/src/main/java/io/airbyte/server/handlers/DestinationHandler.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.airbyte.commons.json.Jsons;
2020
import io.airbyte.config.DestinationConnection;
2121
import io.airbyte.config.StandardDestinationDefinition;
22+
import io.airbyte.config.StandardSourceDefinition;
2223
import io.airbyte.config.persistence.ConfigNotFoundException;
2324
import io.airbyte.config.persistence.ConfigRepository;
2425
import io.airbyte.config.persistence.split_secrets.JsonSecretsProcessor;
@@ -153,6 +154,28 @@ public DestinationRead getDestination(final DestinationIdRequestBody destination
153154
return buildDestinationRead(destinationIdRequestBody.getDestinationId());
154155
}
155156

157+
public DestinationRead cloneDestination(final DestinationIdRequestBody destinationIdRequestBody)
158+
throws JsonValidationException, IOException, ConfigNotFoundException {
159+
// read destination configuration from db
160+
final DestinationRead destinationToClone = buildDestinationReadWithSecrets(destinationIdRequestBody.getDestinationId());
161+
final ConnectorSpecification spec = getSpec(destinationToClone.getDestinationDefinitionId());
162+
163+
// persist
164+
final UUID destinationId = uuidGenerator.get();
165+
final String copyText = " (Copy)";
166+
final String destinationName = destinationToClone.getName() + copyText;
167+
persistDestinationConnection(
168+
destinationName,
169+
destinationToClone.getDestinationDefinitionId(),
170+
destinationToClone.getWorkspaceId(),
171+
destinationId,
172+
destinationToClone.getConnectionConfiguration(),
173+
false);
174+
175+
// read configuration from db
176+
return buildDestinationRead(destinationId, spec);
177+
}
178+
156179
public DestinationReadList listDestinationsForWorkspace(final WorkspaceIdRequestBody workspaceIdRequestBody)
157180
throws ConfigNotFoundException, IOException, JsonValidationException {
158181
final List<DestinationRead> reads = Lists.newArrayList();
@@ -249,6 +272,16 @@ private DestinationRead buildDestinationRead(final UUID destinationId, final Con
249272
return toDestinationRead(dci, standardDestinationDefinition);
250273
}
251274

275+
private DestinationRead buildDestinationReadWithSecrets(final UUID destinationId)
276+
throws ConfigNotFoundException, IOException, JsonValidationException {
277+
278+
// remove secrets from config before returning the read
279+
final DestinationConnection dci = Jsons.clone(configRepository.getDestinationConnectionWithSecrets(destinationId));
280+
final StandardDestinationDefinition standardDestinationDefinition =
281+
configRepository.getStandardDestinationDefinition(dci.getDestinationDefinitionId());
282+
return toDestinationRead(dci, standardDestinationDefinition);
283+
}
284+
252285
protected static DestinationRead toDestinationRead(final DestinationConnection destinationConnection,
253286
final StandardDestinationDefinition standardDestinationDefinition) {
254287
return new DestinationRead()

airbyte-server/src/main/java/io/airbyte/server/handlers/SourceHandler.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,30 @@ public SourceRead getSource(final SourceIdRequestBody sourceIdRequestBody)
115115
return buildSourceRead(sourceIdRequestBody.getSourceId());
116116
}
117117

118+
public SourceRead cloneSource(final SourceIdRequestBody sourceIdRequestBody)
119+
throws JsonValidationException, IOException, ConfigNotFoundException {
120+
// read source configuration from db
121+
final SourceRead sourceToClone = buildSourceReadWithSecrets(sourceIdRequestBody.getSourceId());
122+
123+
// persist
124+
final UUID sourceId = uuidGenerator.get();
125+
final StandardSourceDefinition sourceDef = configRepository.getSourceDefinitionFromSource(sourceIdRequestBody.getSourceId());
126+
final ConnectorSpecification spec = sourceDef.getSpec();
127+
final String copyText = " (Copy)";
128+
final String sourceName = sourceToClone.getName() + copyText;
129+
persistSourceConnection(
130+
sourceName,
131+
sourceToClone.getSourceDefinitionId(),
132+
sourceToClone.getWorkspaceId(),
133+
sourceId,
134+
false,
135+
sourceToClone.getConnectionConfiguration(),
136+
spec);
137+
138+
// read configuration from db
139+
return buildSourceRead(sourceId, spec);
140+
}
141+
118142
public SourceReadList listSourcesForWorkspace(final WorkspaceIdRequestBody workspaceIdRequestBody)
119143
throws ConfigNotFoundException, IOException, JsonValidationException {
120144

@@ -220,6 +244,15 @@ private SourceRead buildSourceRead(final UUID sourceId, final ConnectorSpecifica
220244
return toSourceRead(sourceConnection, standardSourceDefinition);
221245
}
222246

247+
private SourceRead buildSourceReadWithSecrets(final UUID sourceId)
248+
throws ConfigNotFoundException, IOException, JsonValidationException {
249+
// read configuration from db
250+
final SourceConnection sourceConnection = configRepository.getSourceConnectionWithSecrets(sourceId);
251+
final StandardSourceDefinition standardSourceDefinition = configRepository
252+
.getStandardSourceDefinition(sourceConnection.getSourceDefinitionId());
253+
return toSourceRead(sourceConnection, standardSourceDefinition);
254+
}
255+
223256
private void validateSource(final ConnectorSpecification spec, final JsonNode implementationJson)
224257
throws JsonValidationException {
225258
validator.ensure(spec.getConnectionSpecification(), implementationJson);

airbyte-server/src/test/java/io/airbyte/server/handlers/DestinationHandlerTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,39 @@ void testSearchDestinations() throws JsonValidationException, ConfigNotFoundExce
251251
assertEquals(0, actualDestinationRead.getDestinations().size());
252252
}
253253

254+
@Test
255+
void testCloneDestination() throws JsonValidationException, ConfigNotFoundException, IOException {
256+
final DestinationConnection clonedConnection = DestinationHelpers.generateDestination(standardDestinationDefinition.getDestinationDefinitionId());
257+
final DestinationRead expectedDestinationRead = new DestinationRead()
258+
.name(clonedConnection.getName())
259+
.destinationDefinitionId(standardDestinationDefinition.getDestinationDefinitionId())
260+
.workspaceId(clonedConnection.getWorkspaceId())
261+
.destinationId(clonedConnection.getDestinationId())
262+
.connectionConfiguration(clonedConnection.getConfiguration())
263+
.destinationName(standardDestinationDefinition.getName());
264+
final DestinationRead destinationRead = new DestinationRead()
265+
.name(destinationConnection.getName())
266+
.destinationDefinitionId(standardDestinationDefinition.getDestinationDefinitionId())
267+
.workspaceId(destinationConnection.getWorkspaceId())
268+
.destinationId(destinationConnection.getDestinationId())
269+
.connectionConfiguration(destinationConnection.getConfiguration())
270+
.destinationName(standardDestinationDefinition.getName());
271+
272+
final DestinationIdRequestBody destinationIdRequestBody = new DestinationIdRequestBody().destinationId(destinationRead.getDestinationId());
273+
274+
when(uuidGenerator.get()).thenReturn(clonedConnection.getDestinationId());
275+
when(configRepository.getDestinationConnectionWithSecrets(destinationConnection.getDestinationId())).thenReturn(destinationConnection);
276+
when(configRepository.getDestinationConnection(clonedConnection.getDestinationId())).thenReturn(clonedConnection);
277+
278+
when(configRepository.getStandardDestinationDefinition(destinationDefinitionSpecificationRead.getDestinationDefinitionId()))
279+
.thenReturn(standardDestinationDefinition);
280+
when(configRepository.getDestinationDefinitionFromDestination(destinationConnection.getDestinationId())).thenReturn(standardDestinationDefinition);
281+
when(secretsProcessor.maskSecrets(destinationConnection.getConfiguration(), destinationDefinitionSpecificationRead.getConnectionSpecification()))
282+
.thenReturn(destinationConnection.getConfiguration());
283+
284+
final DestinationRead actualDestinationRead = destinationHandler.cloneDestination(destinationIdRequestBody);
285+
286+
assertEquals(expectedDestinationRead, actualDestinationRead);
287+
}
288+
254289
}

airbyte-server/src/test/java/io/airbyte/server/handlers/SourceHandlerTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,37 @@ void testGetSource() throws JsonValidationException, ConfigNotFoundException, IO
179179
verify(secretsProcessor).maskSecrets(sourceConnection.getConfiguration(), sourceDefinitionSpecificationRead.getConnectionSpecification());
180180
}
181181

182+
@Test
183+
void testCloneSource() throws JsonValidationException, ConfigNotFoundException, IOException {
184+
final SourceConnection clonedConnection = SourceHelpers.generateSource(standardSourceDefinition.getSourceDefinitionId());
185+
final SourceRead expectedClonedSourceRead = SourceHelpers.getSourceRead(clonedConnection, standardSourceDefinition);
186+
final SourceRead sourceRead = SourceHelpers.getSourceRead(sourceConnection, standardSourceDefinition);
187+
188+
final SourceIdRequestBody sourceIdRequestBody = new SourceIdRequestBody().sourceId(sourceRead.getSourceId());
189+
190+
when(uuidGenerator.get()).thenReturn(clonedConnection.getSourceId());
191+
when(configRepository.getSourceConnectionWithSecrets(sourceConnection.getSourceId())).thenReturn(sourceConnection);
192+
when(configRepository.getSourceConnection(clonedConnection.getSourceId())).thenReturn(clonedConnection);
193+
194+
when(configRepository.getStandardSourceDefinition(sourceDefinitionSpecificationRead.getSourceDefinitionId()))
195+
.thenReturn(standardSourceDefinition);
196+
when(configRepository.getSourceDefinitionFromSource(sourceConnection.getSourceId())).thenReturn(standardSourceDefinition);
197+
when(secretsProcessor.maskSecrets(sourceConnection.getConfiguration(), sourceDefinitionSpecificationRead.getConnectionSpecification()))
198+
.thenReturn(sourceConnection.getConfiguration());
199+
200+
final SourceRead actualSourceRead = sourceHandler.cloneSource(sourceIdRequestBody);
201+
202+
assertEquals(expectedClonedSourceRead, actualSourceRead);
203+
}
204+
182205
@Test
183206
void testListSourcesForWorkspace() throws JsonValidationException, ConfigNotFoundException, IOException {
184207
final SourceRead expectedSourceRead = SourceHelpers.getSourceRead(sourceConnection, standardSourceDefinition);
185208
final WorkspaceIdRequestBody workspaceIdRequestBody = new WorkspaceIdRequestBody().workspaceId(sourceConnection.getWorkspaceId());
186209

187210
when(configRepository.getSourceConnection(sourceConnection.getSourceId())).thenReturn(sourceConnection);
211+
when(configRepository.getSourceConnection(sourceConnection.getSourceId())).thenReturn(sourceConnection);
212+
188213
when(configRepository.listSourceConnection()).thenReturn(Lists.newArrayList(sourceConnection));
189214
when(configRepository.getStandardSourceDefinition(sourceDefinitionSpecificationRead.getSourceDefinitionId()))
190215
.thenReturn(standardSourceDefinition);

0 commit comments

Comments
 (0)