diff --git a/airbyte-api/src/main/openapi/config.yaml b/airbyte-api/src/main/openapi/config.yaml index 9e1438edc081..247d7de72e0d 100644 --- a/airbyte-api/src/main/openapi/config.yaml +++ b/airbyte-api/src/main/openapi/config.yaml @@ -668,7 +668,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/SourceIdRequestBody" + $ref: "#/components/schemas/SourceCloneRequestBody" required: true responses: "200": @@ -1253,7 +1253,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/DestinationIdRequestBody" + $ref: "#/components/schemas/DestinationCloneRequestBody" required: true responses: "200": @@ -2715,6 +2715,23 @@ components: properties: sourceId: $ref: "#/components/schemas/SourceId" + SourceCloneRequestBody: + description: The values required to configure the source. The schema for this should have an id of the existing source along with the configuration you want to change in case. + type: object + required: + - sourceCloneId + properties: + sourceCloneId: + $ref: "#/components/schemas/SourceId" + sourceConfiguration: + $ref: "#/components/schemas/SourceCloneConfiguration" + SourceCloneConfiguration: + type: object + properties: + connectionConfiguration: + $ref: "#/components/schemas/SourceConfiguration" + name: + type: string SourceConfiguration: description: The values required to configure the source. The schema for this must match the schema return by source_definition_specifications/get for the source. example: { user: "charles" } @@ -3043,6 +3060,23 @@ components: $ref: "#/components/schemas/DestinationConfiguration" name: type: string + DestinationCloneRequestBody: + description: The values required to configure the destination. The schema for this should have an id of the existing destination along with the configuration you want to change in case. + type: object + required: + - destinationCloneId + properties: + destinationCloneId: + $ref: "#/components/schemas/DestinationId" + destinationConfiguration: + $ref: "#/components/schemas/DestinationCloneConfiguration" + DestinationCloneConfiguration: + type: object + properties: + connectionConfiguration: + $ref: "#/components/schemas/DestinationConfiguration" + name: + type: string DestinationRead: type: object required: diff --git a/airbyte-server/src/main/java/io/airbyte/server/apis/ConfigurationApi.java b/airbyte-server/src/main/java/io/airbyte/server/apis/ConfigurationApi.java index 017c171429e5..cf6d33b3e58e 100644 --- a/airbyte-server/src/main/java/io/airbyte/server/apis/ConfigurationApi.java +++ b/airbyte-server/src/main/java/io/airbyte/server/apis/ConfigurationApi.java @@ -24,6 +24,7 @@ import io.airbyte.api.model.generated.DbMigrationExecutionRead; import io.airbyte.api.model.generated.DbMigrationReadList; import io.airbyte.api.model.generated.DbMigrationRequestBody; +import io.airbyte.api.model.generated.DestinationCloneRequestBody; import io.airbyte.api.model.generated.DestinationCoreConfig; import io.airbyte.api.model.generated.DestinationCreate; import io.airbyte.api.model.generated.DestinationDefinitionCreate; @@ -64,6 +65,7 @@ import io.airbyte.api.model.generated.SetInstancewideDestinationOauthParamsRequestBody; import io.airbyte.api.model.generated.SetInstancewideSourceOauthParamsRequestBody; import io.airbyte.api.model.generated.SlugRequestBody; +import io.airbyte.api.model.generated.SourceCloneRequestBody; import io.airbyte.api.model.generated.SourceCoreConfig; import io.airbyte.api.model.generated.SourceCreate; import io.airbyte.api.model.generated.SourceDefinitionCreate; @@ -469,8 +471,8 @@ public void deleteSource(final SourceIdRequestBody sourceIdRequestBody) { } @Override - public SourceRead cloneSource(final SourceIdRequestBody sourceIdRequestBody) { - return execute(() -> sourceHandler.cloneSource(sourceIdRequestBody)); + public SourceRead cloneSource(final SourceCloneRequestBody sourceCloneRequestBody) { + return execute(() -> sourceHandler.cloneSource(sourceCloneRequestBody)); } @Override @@ -629,8 +631,8 @@ public DestinationRead getDestination(final DestinationIdRequestBody destination } @Override - public DestinationRead cloneDestination(final DestinationIdRequestBody destinationIdRequestBody) { - return execute(() -> destinationHandler.cloneDestination(destinationIdRequestBody)); + public DestinationRead cloneDestination(final DestinationCloneRequestBody destinationCloneRequestBody) { + return execute(() -> destinationHandler.cloneDestination(destinationCloneRequestBody)); } @Override diff --git a/airbyte-server/src/main/java/io/airbyte/server/handlers/DestinationHandler.java b/airbyte-server/src/main/java/io/airbyte/server/handlers/DestinationHandler.java index 3a3f71a094cc..807b4c9d3fe1 100644 --- a/airbyte-server/src/main/java/io/airbyte/server/handlers/DestinationHandler.java +++ b/airbyte-server/src/main/java/io/airbyte/server/handlers/DestinationHandler.java @@ -8,6 +8,8 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import io.airbyte.api.model.generated.ConnectionRead; +import io.airbyte.api.model.generated.DestinationCloneConfiguration; +import io.airbyte.api.model.generated.DestinationCloneRequestBody; import io.airbyte.api.model.generated.DestinationCreate; import io.airbyte.api.model.generated.DestinationDefinitionIdRequestBody; import io.airbyte.api.model.generated.DestinationIdRequestBody; @@ -168,26 +170,32 @@ public DestinationRead getDestination(final DestinationIdRequestBody destination return buildDestinationRead(destinationIdRequestBody.getDestinationId()); } - public DestinationRead cloneDestination(final DestinationIdRequestBody destinationIdRequestBody) + public DestinationRead cloneDestination(final DestinationCloneRequestBody destinationCloneRequestBody) throws JsonValidationException, IOException, ConfigNotFoundException { // read destination configuration from db - final DestinationRead destinationToClone = buildDestinationReadWithSecrets(destinationIdRequestBody.getDestinationId()); - final ConnectorSpecification spec = getSpec(destinationToClone.getDestinationDefinitionId()); + final DestinationRead destinationToClone = buildDestinationReadWithSecrets(destinationCloneRequestBody.getDestinationCloneId()); + final DestinationCloneConfiguration destinationCloneConfiguration = destinationCloneRequestBody.getDestinationConfiguration(); - // persist - final UUID destinationId = uuidGenerator.get(); final String copyText = " (Copy)"; final String destinationName = destinationToClone.getName() + copyText; - persistDestinationConnection( - destinationName, - destinationToClone.getDestinationDefinitionId(), - destinationToClone.getWorkspaceId(), - destinationId, - destinationToClone.getConnectionConfiguration(), - false); - // read configuration from db - return buildDestinationRead(destinationId, spec); + final DestinationCreate destinationCreate = new DestinationCreate() + .name(destinationName) + .destinationDefinitionId(destinationToClone.getDestinationDefinitionId()) + .connectionConfiguration(destinationToClone.getConnectionConfiguration()) + .workspaceId(destinationToClone.getWorkspaceId()); + + if (destinationCloneConfiguration != null) { + if (destinationCloneConfiguration.getName() != null) { + destinationCreate.name(destinationCloneConfiguration.getName()); + } + + if (destinationCloneConfiguration.getConnectionConfiguration() != null) { + destinationCreate.connectionConfiguration(destinationCloneConfiguration.getConnectionConfiguration()); + } + } + + return createDestination(destinationCreate); } public DestinationReadList listDestinationsForWorkspace(final WorkspaceIdRequestBody workspaceIdRequestBody) diff --git a/airbyte-server/src/main/java/io/airbyte/server/handlers/SourceHandler.java b/airbyte-server/src/main/java/io/airbyte/server/handlers/SourceHandler.java index 8440043b91ba..d140909000d4 100644 --- a/airbyte-server/src/main/java/io/airbyte/server/handlers/SourceHandler.java +++ b/airbyte-server/src/main/java/io/airbyte/server/handlers/SourceHandler.java @@ -7,6 +7,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.Lists; import io.airbyte.api.model.generated.ConnectionRead; +import io.airbyte.api.model.generated.SourceCloneConfiguration; +import io.airbyte.api.model.generated.SourceCloneRequestBody; import io.airbyte.api.model.generated.SourceCreate; import io.airbyte.api.model.generated.SourceDefinitionIdRequestBody; import io.airbyte.api.model.generated.SourceIdRequestBody; @@ -130,28 +132,32 @@ public SourceRead getSource(final SourceIdRequestBody sourceIdRequestBody) return buildSourceRead(sourceIdRequestBody.getSourceId()); } - public SourceRead cloneSource(final SourceIdRequestBody sourceIdRequestBody) + public SourceRead cloneSource(final SourceCloneRequestBody sourceCloneRequestBody) throws JsonValidationException, IOException, ConfigNotFoundException { // read source configuration from db - final SourceRead sourceToClone = buildSourceReadWithSecrets(sourceIdRequestBody.getSourceId()); + final SourceRead sourceToClone = buildSourceReadWithSecrets(sourceCloneRequestBody.getSourceCloneId()); + final SourceCloneConfiguration sourceCloneConfiguration = sourceCloneRequestBody.getSourceConfiguration(); - // persist - final UUID sourceId = uuidGenerator.get(); - final StandardSourceDefinition sourceDef = configRepository.getSourceDefinitionFromSource(sourceIdRequestBody.getSourceId()); - final ConnectorSpecification spec = sourceDef.getSpec(); final String copyText = " (Copy)"; final String sourceName = sourceToClone.getName() + copyText; - persistSourceConnection( - sourceName, - sourceToClone.getSourceDefinitionId(), - sourceToClone.getWorkspaceId(), - sourceId, - false, - sourceToClone.getConnectionConfiguration(), - spec); - // read configuration from db - return buildSourceRead(sourceId, spec); + final SourceCreate sourceCreate = new SourceCreate() + .name(sourceName) + .sourceDefinitionId(sourceToClone.getSourceDefinitionId()) + .connectionConfiguration(sourceToClone.getConnectionConfiguration()) + .workspaceId(sourceToClone.getWorkspaceId()); + + if (sourceCloneConfiguration != null) { + if (sourceCloneConfiguration.getName() != null) { + sourceCreate.name(sourceCloneConfiguration.getName()); + } + + if (sourceCloneConfiguration.getConnectionConfiguration() != null) { + sourceCreate.connectionConfiguration(sourceCloneConfiguration.getConnectionConfiguration()); + } + } + + return createSource(sourceCreate); } public SourceReadList listSourcesForWorkspace(final WorkspaceIdRequestBody workspaceIdRequestBody) diff --git a/airbyte-server/src/test/java/io/airbyte/server/handlers/DestinationHandlerTest.java b/airbyte-server/src/test/java/io/airbyte/server/handlers/DestinationHandlerTest.java index 824b72863191..d161f4e7a9ce 100644 --- a/airbyte-server/src/test/java/io/airbyte/server/handlers/DestinationHandlerTest.java +++ b/airbyte-server/src/test/java/io/airbyte/server/handlers/DestinationHandlerTest.java @@ -12,6 +12,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.Lists; +import io.airbyte.api.model.generated.DestinationCloneConfiguration; +import io.airbyte.api.model.generated.DestinationCloneRequestBody; import io.airbyte.api.model.generated.DestinationCreate; import io.airbyte.api.model.generated.DestinationDefinitionIdRequestBody; import io.airbyte.api.model.generated.DestinationDefinitionSpecificationRead; @@ -269,7 +271,7 @@ void testSearchDestinations() throws JsonValidationException, ConfigNotFoundExce } @Test - void testCloneDestination() throws JsonValidationException, ConfigNotFoundException, IOException { + void testCloneDestinationWithConfiguration() throws JsonValidationException, ConfigNotFoundException, IOException { final DestinationConnection clonedConnection = DestinationHelpers.generateDestination(standardDestinationDefinition.getDestinationDefinitionId()); final DestinationRead expectedDestinationRead = new DestinationRead() .name(clonedConnection.getName()) @@ -286,7 +288,9 @@ void testCloneDestination() throws JsonValidationException, ConfigNotFoundExcept .connectionConfiguration(destinationConnection.getConfiguration()) .destinationName(standardDestinationDefinition.getName()); - final DestinationIdRequestBody destinationIdRequestBody = new DestinationIdRequestBody().destinationId(destinationRead.getDestinationId()); + final DestinationCloneConfiguration destinationCloneConfiguration = new DestinationCloneConfiguration().name("Copy Name"); + final DestinationCloneRequestBody destinationCloneRequestBody = new DestinationCloneRequestBody() + .destinationCloneId(destinationRead.getDestinationId()).destinationConfiguration(destinationCloneConfiguration); when(uuidGenerator.get()).thenReturn(clonedConnection.getDestinationId()); when(secretsRepositoryReader.getDestinationConnectionWithSecrets(destinationConnection.getDestinationId())).thenReturn(destinationConnection); @@ -300,7 +304,45 @@ void testCloneDestination() throws JsonValidationException, ConfigNotFoundExcept destinationDefinitionSpecificationRead.getConnectionSpecification())) .thenReturn(destinationConnection.getConfiguration()); - final DestinationRead actualDestinationRead = destinationHandler.cloneDestination(destinationIdRequestBody); + final DestinationRead actualDestinationRead = destinationHandler.cloneDestination(destinationCloneRequestBody); + + assertEquals(expectedDestinationRead, actualDestinationRead); + } + + @Test + void testCloneDestinationWithoutConfiguration() throws JsonValidationException, ConfigNotFoundException, IOException { + final DestinationConnection clonedConnection = DestinationHelpers.generateDestination(standardDestinationDefinition.getDestinationDefinitionId()); + final DestinationRead expectedDestinationRead = new DestinationRead() + .name(clonedConnection.getName()) + .destinationDefinitionId(standardDestinationDefinition.getDestinationDefinitionId()) + .workspaceId(clonedConnection.getWorkspaceId()) + .destinationId(clonedConnection.getDestinationId()) + .connectionConfiguration(clonedConnection.getConfiguration()) + .destinationName(standardDestinationDefinition.getName()); + final DestinationRead destinationRead = new DestinationRead() + .name(destinationConnection.getName()) + .destinationDefinitionId(standardDestinationDefinition.getDestinationDefinitionId()) + .workspaceId(destinationConnection.getWorkspaceId()) + .destinationId(destinationConnection.getDestinationId()) + .connectionConfiguration(destinationConnection.getConfiguration()) + .destinationName(standardDestinationDefinition.getName()); + + final DestinationCloneRequestBody destinationCloneRequestBody = + new DestinationCloneRequestBody().destinationCloneId(destinationRead.getDestinationId()); + + when(uuidGenerator.get()).thenReturn(clonedConnection.getDestinationId()); + when(secretsRepositoryReader.getDestinationConnectionWithSecrets(destinationConnection.getDestinationId())).thenReturn(destinationConnection); + when(configRepository.getDestinationConnection(clonedConnection.getDestinationId())).thenReturn(clonedConnection); + + when(configRepository.getStandardDestinationDefinition(destinationDefinitionSpecificationRead.getDestinationDefinitionId())) + .thenReturn(standardDestinationDefinition); + when(configRepository.getDestinationDefinitionFromDestination(destinationConnection.getDestinationId())) + .thenReturn(standardDestinationDefinition); + when(secretsProcessor.prepareSecretsForOutput(destinationConnection.getConfiguration(), + destinationDefinitionSpecificationRead.getConnectionSpecification())) + .thenReturn(destinationConnection.getConfiguration()); + + final DestinationRead actualDestinationRead = destinationHandler.cloneDestination(destinationCloneRequestBody); assertEquals(expectedDestinationRead, actualDestinationRead); } diff --git a/airbyte-server/src/test/java/io/airbyte/server/handlers/SourceHandlerTest.java b/airbyte-server/src/test/java/io/airbyte/server/handlers/SourceHandlerTest.java index b50670823a8c..4b5ac5752c1c 100644 --- a/airbyte-server/src/test/java/io/airbyte/server/handlers/SourceHandlerTest.java +++ b/airbyte-server/src/test/java/io/airbyte/server/handlers/SourceHandlerTest.java @@ -15,6 +15,8 @@ import io.airbyte.api.model.generated.ConnectionIdRequestBody; import io.airbyte.api.model.generated.ConnectionRead; import io.airbyte.api.model.generated.ConnectionReadList; +import io.airbyte.api.model.generated.SourceCloneConfiguration; +import io.airbyte.api.model.generated.SourceCloneRequestBody; import io.airbyte.api.model.generated.SourceCreate; import io.airbyte.api.model.generated.SourceDefinitionIdRequestBody; import io.airbyte.api.model.generated.SourceDefinitionSpecificationRead; @@ -198,12 +200,12 @@ void testGetSource() throws JsonValidationException, ConfigNotFoundException, IO } @Test - void testCloneSource() throws JsonValidationException, ConfigNotFoundException, IOException { + void testCloneSourceWithoutConfigChange() throws JsonValidationException, ConfigNotFoundException, IOException { final SourceConnection clonedConnection = SourceHelpers.generateSource(standardSourceDefinition.getSourceDefinitionId()); final SourceRead expectedClonedSourceRead = SourceHelpers.getSourceRead(clonedConnection, standardSourceDefinition); final SourceRead sourceRead = SourceHelpers.getSourceRead(sourceConnection, standardSourceDefinition); - final SourceIdRequestBody sourceIdRequestBody = new SourceIdRequestBody().sourceId(sourceRead.getSourceId()); + final SourceCloneRequestBody sourceCloneRequestBody = new SourceCloneRequestBody().sourceCloneId(sourceRead.getSourceId()); when(uuidGenerator.get()).thenReturn(clonedConnection.getSourceId()); when(secretsRepositoryReader.getSourceConnectionWithSecrets(sourceConnection.getSourceId())).thenReturn(sourceConnection); @@ -216,7 +218,33 @@ void testCloneSource() throws JsonValidationException, ConfigNotFoundException, secretsProcessor.prepareSecretsForOutput(sourceConnection.getConfiguration(), sourceDefinitionSpecificationRead.getConnectionSpecification())) .thenReturn(sourceConnection.getConfiguration()); - final SourceRead actualSourceRead = sourceHandler.cloneSource(sourceIdRequestBody); + final SourceRead actualSourceRead = sourceHandler.cloneSource(sourceCloneRequestBody); + + assertEquals(expectedClonedSourceRead, actualSourceRead); + } + + @Test + void testCloneSourceWithConfigChange() throws JsonValidationException, ConfigNotFoundException, IOException { + final SourceConnection clonedConnection = SourceHelpers.generateSource(standardSourceDefinition.getSourceDefinitionId()); + final SourceRead expectedClonedSourceRead = SourceHelpers.getSourceRead(clonedConnection, standardSourceDefinition); + final SourceRead sourceRead = SourceHelpers.getSourceRead(sourceConnection, standardSourceDefinition); + + final SourceCloneConfiguration sourceCloneConfiguration = new SourceCloneConfiguration().name("Copy Name"); + final SourceCloneRequestBody sourceCloneRequestBody = + new SourceCloneRequestBody().sourceCloneId(sourceRead.getSourceId()).sourceConfiguration(sourceCloneConfiguration); + + when(uuidGenerator.get()).thenReturn(clonedConnection.getSourceId()); + when(secretsRepositoryReader.getSourceConnectionWithSecrets(sourceConnection.getSourceId())).thenReturn(sourceConnection); + when(configRepository.getSourceConnection(clonedConnection.getSourceId())).thenReturn(clonedConnection); + + when(configRepository.getStandardSourceDefinition(sourceDefinitionSpecificationRead.getSourceDefinitionId())) + .thenReturn(standardSourceDefinition); + when(configRepository.getSourceDefinitionFromSource(sourceConnection.getSourceId())).thenReturn(standardSourceDefinition); + when( + secretsProcessor.prepareSecretsForOutput(sourceConnection.getConfiguration(), sourceDefinitionSpecificationRead.getConnectionSpecification())) + .thenReturn(sourceConnection.getConfiguration()); + + final SourceRead actualSourceRead = sourceHandler.cloneSource(sourceCloneRequestBody); assertEquals(expectedClonedSourceRead, actualSourceRead); } diff --git a/docs/reference/api/generated-api-html/index.html b/docs/reference/api/generated-api-html/index.html index e3ed692477eb..ae575f1d2a05 100644 --- a/docs/reference/api/generated-api-html/index.html +++ b/docs/reference/api/generated-api-html/index.html @@ -2267,7 +2267,7 @@

Consumes

Request body

-
DestinationIdRequestBody DestinationIdRequestBody (required)
+
DestinationCloneRequestBody DestinationCloneRequestBody (required)
Body Parameter
@@ -6136,7 +6136,7 @@

Consumes

Request body

-
SourceIdRequestBody SourceIdRequestBody (required)
+
SourceCloneRequestBody SourceCloneRequestBody (required)
Body Parameter
@@ -10315,6 +10315,8 @@

Table of Contents

  • DbMigrationReadList -
  • DbMigrationRequestBody -
  • DbMigrationState -
  • +
  • DestinationCloneConfiguration -
  • +
  • DestinationCloneRequestBody -
  • DestinationCoreConfig -
  • DestinationCreate -
  • DestinationDefinitionCreate -
  • @@ -10386,6 +10388,8 @@

    Table of Contents

  • SetInstancewideSourceOauthParamsRequestBody -
  • SlackNotificationConfiguration -
  • SlugRequestBody -
  • +
  • SourceCloneConfiguration -
  • +
  • SourceCloneRequestBody -
  • SourceCoreConfig -
  • SourceCreate -
  • SourceDefinitionCreate -
  • @@ -10826,6 +10830,22 @@

    DbMigrationState -

    +
    +

    DestinationCloneConfiguration - Up

    +
    +
    +
    connectionConfiguration (optional)
    +
    name (optional)
    +
    +
    +
    +

    DestinationCloneRequestBody - Up

    +
    The values required to configure the destination. The schema for this should have an id of the existing destination along with the configuration you want to change in case.
    +
    +
    destinationCloneId
    UUID format: uuid
    +
    destinationConfiguration (optional)
    +
    +

    DestinationCoreConfig - Up

    @@ -11437,6 +11457,22 @@

    SlugRequestBody - slug

    String
    +
    +

    SourceCloneConfiguration - Up

    +
    +
    +
    connectionConfiguration (optional)
    +
    name (optional)
    +
    +
    +
    +

    SourceCloneRequestBody - Up

    +
    The values required to configure the source. The schema for this should have an id of the existing source along with the configuration you want to change in case.
    +
    +
    sourceCloneId
    UUID format: uuid
    +
    sourceConfiguration (optional)
    +
    +

    SourceCoreConfig - Up