From 76f140f2b163c872f905dd3ff3d6e8f7a3437e80 Mon Sep 17 00:00:00 2001 From: Darwis Date: Mon, 23 Oct 2023 10:18:19 -0300 Subject: [PATCH 1/3] Changes to support async import --- .../org/rundeck/client/api/RundeckApi.java | 2 + .../tool/commands/projects/Archives.java | 17 +++++- .../commands/projects/ArchivesSpec.groovy | 52 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java b/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java index a44c370d..6608d9bb 100644 --- a/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java +++ b/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java @@ -390,6 +390,7 @@ Call importProjectArchive( @Query("importWebhooks") Boolean importWebhooks, @Query("whkRegenAuthTokens") Boolean whkRegenAuthTokens, @Query("importNodesSources") Boolean importNodesSources, + @Query("asyncImport") Boolean asyncImport, @Body RequestBody body ); @@ -412,6 +413,7 @@ Call importProjectArchive( @Query("importWebhooks") Boolean importWebhooks, @Query("whkRegenAuthTokens") Boolean whkRegenAuthTokens, @Query("importNodesSources") Boolean importNodesSources, + @Query("asyncImport") Boolean asyncImport, @QueryMap Map params, @Body RequestBody body ); diff --git a/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java b/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java index 2fcfd3a7..4044f336 100644 --- a/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java +++ b/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java @@ -25,6 +25,7 @@ import org.rundeck.client.api.model.ProjectImportStatus; import org.rundeck.client.tool.CommandOutput; import org.rundeck.client.tool.InputError; +import org.rundeck.client.tool.Main; import org.rundeck.client.tool.ProjectInput; import org.rundeck.client.tool.extension.BaseCommand; import org.rundeck.client.tool.options.ProjectRequiredNameOptions; @@ -92,6 +93,9 @@ static class ArchiveImportOpts extends BaseOptions{ @CommandLine.Option(names = {"-n", "--include-node-sources"}, description = "Include node resources in import, default: false (api v38 required)") boolean includeNodeSources; + @CommandLine.Option(names = {"-i", "--async-import-enabled"}, description = "Enables asynchronous import process for the uploaded project file.") + boolean asyncImportEnabled; + @CommandLine.Option( names = {"--strict"}, description = "Return non-zero exit status if any imported item had an error. Default: only job " + @@ -149,12 +153,23 @@ public int importArchive(@CommandLine.Mixin ArchiveImportOpts opts) throws Input opts.isIncludeWebhooks(), opts.isWhkRegenAuthTokens(), opts.isIncludeNodeSources(), + opts.isAsyncImportEnabled(), extraCompOpts, body )); boolean anyerror = false; if (status.getResultSuccess()) { - getRdOutput().info("Jobs imported successfully"); + if( opts.isAsyncImportEnabled() ){ + + String RD_URL = ""; + String projectPlaceholder = ""; + String apiVersionPlaceholder = ""; + + getRdOutput().info("Asynchronous import operation started, please check status endpoint for more info."); + getRdOutput().info("Users could check import status through endpoint: " + RD_URL + "/api/" + apiVersionPlaceholder + "/project/" + projectPlaceholder + "/async/import-status"); + }else{ + getRdOutput().info("Jobs imported successfully"); + } } else { anyerror = true; if (null != status.errors && status.errors.size() > 0) { diff --git a/rd-cli-tool/src/test/groovy/org/rundeck/client/tool/commands/projects/ArchivesSpec.groovy b/rd-cli-tool/src/test/groovy/org/rundeck/client/tool/commands/projects/ArchivesSpec.groovy index 74af462d..3a66b8b7 100644 --- a/rd-cli-tool/src/test/groovy/org/rundeck/client/tool/commands/projects/ArchivesSpec.groovy +++ b/rd-cli-tool/src/test/groovy/org/rundeck/client/tool/commands/projects/ArchivesSpec.groovy @@ -79,6 +79,58 @@ class ArchivesSpec extends Specification { result == 0 } + def "successful with async import enabled"() { + + def api = Mock(RundeckApi) + + def retrofit = new Retrofit.Builder() + .addConverterFactory(JacksonConverterFactory.create()) + .baseUrl('http://example.com/fake/').build() + def out = Mock(CommandOutput) + def client = new Client(api, retrofit, null, null, 18, true, null) + + def rdapp = Mock(RdApp) { + getClient() >> client + getAppConfig() >> Mock(RdClientConfig) + } + def rdTool = new RdToolImpl(rdapp) + + def sut = new Archives() + sut.rdOutput = out + sut.rdTool = rdTool + def opts = new Archives.ArchiveImportOpts() + opts.components = ['test-comp'].toSet() + opts.componentOptions = ['test-comp.key': 'value'] + opts.file = tempFile + opts.project = 'Aproj' + opts.asyncImportEnabled = true + + + when: + def result = sut.importArchive(opts) + + then: + 1 * api.importProjectArchive( + 'Aproj', + _, + _, + _, + _, + _, + _, + _, + _, + true, + [ + 'importComponents.test-comp': 'true', + 'importOpts.test-comp.key' : 'value', + ], + _ + ) >> Calls.response(new ProjectImportStatus(successful: true)) + 0 * api._(*_) + result == 0 + } + def "import some failure has correct exit code"() { def api = Mock(RundeckApi) From 24110aa80262523a9cf962074254fbdb40d20f04 Mon Sep 17 00:00:00 2001 From: Darwis Narvaez Date: Mon, 15 Jan 2024 12:20:00 -0300 Subject: [PATCH 2/3] Status endpoint call for cli --- .../org/rundeck/client/api/RundeckApi.java | 6 ++++ .../api/model/AsyncProjectImportStatus.java | 36 +++++++++++++++++++ .../tool/commands/projects/Archives.java | 17 +++++++++ .../commands/projects/ArchivesSpec.groovy | 33 +++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 rd-api-client/src/main/java/org/rundeck/client/api/model/AsyncProjectImportStatus.java diff --git a/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java b/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java index 6608d9bb..4c05d317 100644 --- a/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java +++ b/rd-api-client/src/main/java/org/rundeck/client/api/RundeckApi.java @@ -418,6 +418,12 @@ Call importProjectArchive( @Body RequestBody body ); + @Headers("Accept: application/json") + @GET("project/{project}/import/status") + Call asyncImportProjectArchiveStatus( + @Path("project") String project + ); + @Headers("Accept: application/json") @GET("project/{project}/executions/running") Call runningExecutions( diff --git a/rd-api-client/src/main/java/org/rundeck/client/api/model/AsyncProjectImportStatus.java b/rd-api-client/src/main/java/org/rundeck/client/api/model/AsyncProjectImportStatus.java new file mode 100644 index 00000000..eec2bf08 --- /dev/null +++ b/rd-api-client/src/main/java/org/rundeck/client/api/model/AsyncProjectImportStatus.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017 Rundeck, Inc. (http://rundeck.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.rundeck.client.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * @author Darwis + * @since 1/15/24 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AsyncProjectImportStatus { + @JsonProperty("lastUpdate") + public String lastUpdate; + @JsonProperty("lastUpdated") + public String lastUpdated; + @JsonProperty("errors") + public String errors; +} diff --git a/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java b/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java index 4044f336..ce18a92e 100644 --- a/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java +++ b/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java @@ -16,11 +16,15 @@ package org.rundeck.client.tool.commands.projects; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.util.JSONPObject; import lombok.Setter; import lombok.Getter; import okhttp3.RequestBody; import okhttp3.ResponseBody; import org.rundeck.client.api.RundeckApi; +import org.rundeck.client.api.model.AsyncProjectImportStatus; import org.rundeck.client.api.model.ProjectExportStatus; import org.rundeck.client.api.model.ProjectImportStatus; import org.rundeck.client.tool.CommandOutput; @@ -28,6 +32,7 @@ import org.rundeck.client.tool.Main; import org.rundeck.client.tool.ProjectInput; import org.rundeck.client.tool.extension.BaseCommand; +import org.rundeck.client.tool.options.ProjectNameOptions; import org.rundeck.client.tool.options.ProjectRequiredNameOptions; import org.rundeck.client.util.Client; import org.rundeck.client.util.ServiceClient; @@ -117,6 +122,18 @@ static class ArchiveImportOpts extends BaseOptions{ } + @CommandLine.Command(description = "Get the status of an ongoing asynchronous import process.", name = "async-import-status") + public void asyncImportStatus(@CommandLine.Mixin ProjectNameOptions projectNameOptions) throws InputError, IOException { + String project = getRdTool().projectOrEnv(projectNameOptions); + AsyncProjectImportStatus status = apiCall(api -> api.asyncImportProjectArchiveStatus(project)); + if( status != null ){ + final ObjectMapper mapper = new ObjectMapper(); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + String outputString = mapper.writeValueAsString(status); + getRdOutput().info(outputString); + } + } + @CommandLine.Command(description = "Import a project archive", name = "import") public int importArchive(@CommandLine.Mixin ArchiveImportOpts opts) throws InputError, IOException { File input = opts.getFile(); diff --git a/rd-cli-tool/src/test/groovy/org/rundeck/client/tool/commands/projects/ArchivesSpec.groovy b/rd-cli-tool/src/test/groovy/org/rundeck/client/tool/commands/projects/ArchivesSpec.groovy index 3a66b8b7..0f190300 100644 --- a/rd-cli-tool/src/test/groovy/org/rundeck/client/tool/commands/projects/ArchivesSpec.groovy +++ b/rd-cli-tool/src/test/groovy/org/rundeck/client/tool/commands/projects/ArchivesSpec.groovy @@ -3,10 +3,12 @@ package org.rundeck.client.tool.commands.projects import groovy.transform.CompileStatic import okhttp3.ResponseBody import org.rundeck.client.api.RundeckApi +import org.rundeck.client.api.model.AsyncProjectImportStatus import org.rundeck.client.api.model.ProjectImportStatus import org.rundeck.client.tool.CommandOutput import org.rundeck.client.tool.RdApp import org.rundeck.client.tool.commands.RdToolImpl +import org.rundeck.client.tool.options.ProjectNameOptions import org.rundeck.client.tool.options.ProjectRequiredNameOptions import org.rundeck.client.util.Client import org.rundeck.client.util.RdClientConfig @@ -69,6 +71,7 @@ class ArchivesSpec extends Specification { _, _, _, + _, [ 'importComponents.test-comp': 'true', 'importOpts.test-comp.key' : 'value', @@ -131,6 +134,35 @@ class ArchivesSpec extends Specification { result == 0 } + def "status endpoint for async import"(){ + given: + def api = Mock(RundeckApi) + def retrofit = new Retrofit.Builder() + .addConverterFactory(JacksonConverterFactory.create()) + .baseUrl('http://example.com/fake/').build() + def out = Mock(CommandOutput) + def client = new Client(api, retrofit, null, null, 40, true, null) + def rdapp = Mock(RdApp) { + getClient() >> client + getAppConfig() >> Mock(RdClientConfig) + } + def rdTool = new RdToolImpl(rdapp) + def sut = new Archives() + sut.rdOutput = out + sut.rdTool = rdTool + def project = new ProjectNameOptions().with { + project = "test" + return it + } + + when: + sut.asyncImportStatus(project) + + then: + 1 * api.asyncImportProjectArchiveStatus(project.project) >> Calls.response(new AsyncProjectImportStatus()) + 0 * api._(*_) + } + def "import some failure has correct exit code"() { def api = Mock(RundeckApi) @@ -170,6 +202,7 @@ class ArchivesSpec extends Specification { _, _, _, + _, [:], _ ) >> Calls.response(new ProjectImportStatus(resultsmap)) From f1402c25073657151c9be45cc4f43d44f9bd16bf Mon Sep 17 00:00:00 2001 From: Darwis Narvaez Date: Mon, 15 Jan 2024 12:23:01 -0300 Subject: [PATCH 3/3] Removed unused import --- .../java/org/rundeck/client/tool/commands/projects/Archives.java | 1 - 1 file changed, 1 deletion(-) diff --git a/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java b/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java index ce18a92e..27300272 100644 --- a/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java +++ b/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/projects/Archives.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.util.JSONPObject; import lombok.Setter; import lombok.Getter; import okhttp3.RequestBody;