From 00cf697254713f7c12416b9c43c8bde39e437bd5 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Mon, 18 Nov 2024 09:48:23 -0800 Subject: [PATCH 1/5] fix: access level --- .../org/rundeck/client/tool/commands/projects/Archives.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 9d1a7d0b..c159a7f6 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 @@ -70,7 +70,7 @@ String validate(ProjectInput opts) throws InputError { } @Getter @Setter - static class ArchiveImportOpts extends BaseOptions{ + public static class ArchiveImportOpts extends BaseOptions{ @CommandLine.Option(names = {"-r"}, description = "Remove Job UUIDs in imported jobs. Default: preserve job UUIDs.") boolean remove; @@ -216,7 +216,7 @@ public int importArchive(@CommandLine.Mixin ArchiveImportOpts opts) throws Input @Getter @Setter - static class ArchiveExportOpts extends BaseOptions { + public static class ArchiveExportOpts extends BaseOptions { @CommandLine.Option( names = {"--execids", "-e"}, From 6227b3f51d56d7602f79a72d112161c03a254b3f Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Mon, 18 Nov 2024 09:55:04 -0800 Subject: [PATCH 2/5] add include Flags option to match export CLI --- .../tool/commands/projects/Archives.java | 69 +++++++-- .../commands/projects/ArchivesSpec.groovy | 145 ++++++++++++++++++ 2 files changed, 204 insertions(+), 10 deletions(-) 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 c159a7f6..43d7aca5 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 @@ -122,6 +122,54 @@ public static class ArchiveImportOpts extends BaseOptions{ description = "Set options for enabled components, in the form name.key=value") Map componentOptions; + @CommandLine.Option( + names = {"--include", "-i"}, + description = + "List of archive contents to import. [executions,config,acl,scm,webhooks,nodeSources]. Default: " + + "executions. (webhooks: requires API v34. nodeSources: requires API v38).") + Set includeFlags; + + boolean isIncludeFlags() { + return includeFlags != null && !includeFlags.isEmpty(); + } + + Set calculateFlags(){ + if(isIncludeFlags()){ + return includeFlags; + } + Set importFlags = new HashSet<>(); + //determine via boolean options + if(!isNoExecutions()){ + importFlags.add(ImportFlags.executions); + } + if(isIncludeConfig()){ + importFlags.add(ImportFlags.config); + } + if(isIncludeAcl()){ + importFlags.add(ImportFlags.acl); + } + if(isIncludeScm()){ + importFlags.add(ImportFlags.scm); + } + if(isIncludeWebhooks()){ + importFlags.add(ImportFlags.webhooks); + } + if(isIncludeNodeSources()){ + importFlags.add(ImportFlags.nodeSources); + } + + return importFlags; + } + + } + + enum ImportFlags { + executions, + config, + acl, + scm, + webhooks, + nodeSources, } @CommandLine.Command(description = "Get the status of an ongoing asynchronous import process.", name = "async-import-status") @@ -142,13 +190,14 @@ public int importArchive(@CommandLine.Mixin ArchiveImportOpts opts) throws Input if (!input.canRead() || !input.isFile()) { throw new InputError(String.format("File is not readable or does not exist: %s", input)); } - if ((opts.isIncludeWebhooks() || opts.isWhkRegenAuthTokens()) && getRdTool().getClient().getApiVersion() < 34) { + Set importFlags = opts.calculateFlags(); + if ((importFlags.contains(ImportFlags.webhooks) || opts.isWhkRegenAuthTokens()) && getRdTool().getClient().getApiVersion() < 34) { throw new InputError(String.format("Cannot use --include-webhooks or --regenerate-tokens with API < 34 (currently: %s)", getRdTool().getClient().getApiVersion())); } - if ((opts.isIncludeWebhooks() || opts.isWhkRegenUuid()) && getRdTool().getClient().getApiVersion() < 47) { - throw new InputError(String.format("Cannot use --include-webhooks or --remove-webhooks-uuids with API < 47 (currently: %s)", getRdTool().getClient().getApiVersion())); + if ((importFlags.contains(ImportFlags.webhooks) && opts.isWhkRegenUuid()) && getRdTool().getClient().getApiVersion() < 47) { + throw new InputError(String.format("Cannot use --include-webhooks with --remove-webhooks-uuids with API < 47 (currently: %s)", getRdTool().getClient().getApiVersion())); } - if ((opts.isIncludeNodeSources()) && getRdTool().getClient().getApiVersion() < 38) { + if ((importFlags.contains(ImportFlags.nodeSources)) && getRdTool().getClient().getApiVersion() < 38) { throw new InputError(String.format("Cannot use --include-node-sources with API < 38 (currently: %s)", getRdTool().getClient().getApiVersion())); } RequestBody body = RequestBody.create(input, Client.MEDIA_TYPE_ZIP); @@ -168,14 +217,14 @@ public int importArchive(@CommandLine.Mixin ArchiveImportOpts opts) throws Input ProjectImportStatus status = apiCall(api -> api.importProjectArchive( project, opts.isRemove() ? "remove" : "preserve", - !opts.isNoExecutions(), - opts.isIncludeConfig(), - opts.isIncludeAcl(), - opts.isIncludeScm(), - opts.isIncludeWebhooks(), + importFlags.contains(ImportFlags.executions), + importFlags.contains(ImportFlags.config), + importFlags.contains(ImportFlags.acl), + importFlags.contains(ImportFlags.scm), + importFlags.contains(ImportFlags.webhooks), opts.isWhkRegenAuthTokens(), opts.isWhkRegenUuid(), - opts.isIncludeNodeSources(), + importFlags.contains(ImportFlags.nodeSources), opts.isAsyncImportEnabled(), extraCompOpts, body 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 bf027dab..ea17ae32 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 @@ -82,6 +82,151 @@ class ArchivesSpec extends Specification { 0 * api._(*_) result == 0 } + def "import include via direct flags"() { + + 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, 38, 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.noExecutions = !execs + opts.includeConfig = configs + opts.includeAcl = acls + opts.includeScm = scm + opts.includeWebhooks = webhooks + opts.includeNodeSources = nodes + + + when: + def result = sut.importArchive(opts) + + then: + 1 * api.importProjectArchive( + 'Aproj', + _, + execs, + configs, + acls, + scm, + webhooks, + _, + _, + nodes, + _, + _, + _ + ) >> Calls.response(new ProjectImportStatus(successful: true)) + 0 * api._(*_) + result == 0 + where: + execs | configs | acls | scm | webhooks | nodes + false | false | false | false | false | false + false | false | false | false | false | true + false | false | false | false | true | false + false | false | false | true | false | false + false | false | true | false | false | false + false | true | false | false | false | false + true | false | false | false | false | false + true | true | true | true | true | true + } + + def "import include via include includeFlags option"() { + + 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, 38, 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' + + Set importFlags = new HashSet<>() + if (execs) { + importFlags.add(Archives.ImportFlags.executions) + } + if (configs) { + importFlags.add(Archives.ImportFlags.config) + } + if (acls) { + importFlags.add(Archives.ImportFlags.acl) + } + if (scm) { + importFlags.add(Archives.ImportFlags.scm) + } + if (webhooks) { + importFlags.add(Archives.ImportFlags.webhooks) + } + if (nodes) { + importFlags.add(Archives.ImportFlags.nodeSources) + } + opts.setIncludeFlags(importFlags) + + + when: + def result = sut.importArchive(opts) + + then: + 1 * api.importProjectArchive( + 'Aproj', + _, + expectExecs, + configs, + acls, + scm, + webhooks, + _, + _, + nodes, + _, + _, + _ + ) >> Calls.response(new ProjectImportStatus(successful: true)) + 0 * api._(*_) + result == 0 + where: "include flags determine request params, lack of any params still includes executions" + execs | expectExecs | configs | acls | scm | webhooks | nodes + false | true | false | false | false | false | false + false | false | false | false | false | false | true + false | false | false | false | false | true | false + false | false | false | false | true | false | false + false | false | false | true | false | false | false + false | false | true | false | false | false | false + true | true | false | false | false | false | false + true | true | true | true | true | true | true + } def "successful with async import enabled"() { From 2346f7bee833a535829b93e9fe4ca4e508c94531 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Mon, 18 Nov 2024 09:56:35 -0800 Subject: [PATCH 3/5] add tests for api version requirements, fix issue with webhook the webhook flag incorrectly was failing if api version <47 was used --- .../commands/projects/ArchivesSpec.groovy | 310 +++++++++++++++++- 1 file changed, 306 insertions(+), 4 deletions(-) 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 ea17ae32..198ef151 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 @@ -1,18 +1,16 @@ 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.InputError 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 -import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.jackson.JacksonConverterFactory import retrofit2.mock.Calls @@ -82,6 +80,310 @@ class ArchivesSpec extends Specification { 0 * api._(*_) result == 0 } + + def "api version < 34 fails for webhooks or webhook regen auth flag"() { + + 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, apiversion, 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' + if (useIncludeFlags) { + opts.setIncludeFlags(new HashSet([Archives.ImportFlags.webhooks])) + } else { + opts.includeWebhooks = true + } + opts.whkRegenAuthTokens = true + + + when: + def result = sut.importArchive(opts) + + then: + InputError exc = thrown() + exc.message.contains('Cannot use --include-webhooks or --regenerate-tokens with API < 34') + + where: + apiversion = 33 + useIncludeFlags << [true, false] + } + + def "api version 34 ok for webhooks or webhook regen auth flag"() { + + 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, 34, 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' + + if (useIncludeFlags) { + opts.setIncludeFlags(new HashSet([Archives.ImportFlags.webhooks])) + } else { + opts.includeWebhooks = true + } + opts.whkRegenAuthTokens = true + + + when: + def result = sut.importArchive(opts) + + then: + 1 * api.importProjectArchive( + 'Aproj', + _, + _, + _, + _, + _, + true, + true, + _, + _, + _, + _, + _ + ) >> Calls.response(new ProjectImportStatus(successful: true)) + 0 * api._(*_) + result == 0 + where: + useIncludeFlags << [true, false] + } + + def "api version < 38 fails for node sources flag"() { + + 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, apiversion, 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' + + if (useIncludeFlags) { + opts.setIncludeFlags(new HashSet([Archives.ImportFlags.nodeSources])) + } else { + opts.includeNodeSources = true + } + + + when: + def result = sut.importArchive(opts) + + then: + InputError exc = thrown() + exc.message.contains('Cannot use --include-node-sources with API < 38') + + where: + apiversion = 37 + useIncludeFlags << [true, false] + } + + def "api version 38 ok for node sources flag"() { + + 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, 38, 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' + + if (useIncludeFlags) { + opts.setIncludeFlags(new HashSet([Archives.ImportFlags.nodeSources])) + } else { + opts.includeNodeSources = true + } + + when: + def result = sut.importArchive(opts) + + then: + 1 * api.importProjectArchive( + 'Aproj', + _, + _, + _, + _, + _, + false, + false, + _, + true, + _, + _, + _ + ) >> Calls.response(new ProjectImportStatus(successful: true)) + 0 * api._(*_) + result == 0 + where: + useIncludeFlags << [true, false] + } + def "api version < 47 fails for webhook regen uuids"() { + + 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, apiversion, 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' + + if (useIncludeFlags) { + opts.setIncludeFlags(new HashSet([Archives.ImportFlags.webhooks])) + } else { + opts.includeWebhooks = true + } + opts.whkRegenUuid=true + + + when: + def result = sut.importArchive(opts) + + then: + InputError exc = thrown() + exc.message.contains('Cannot use --include-webhooks with --remove-webhooks-uuids with API < 47') + + where: + apiversion = 46 + useIncludeFlags << [true, false] + } + + def "api version 47 ok for webhook regen uuids flag"() { + + 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, 47, 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' + + if (useIncludeFlags) { + opts.setIncludeFlags(new HashSet([Archives.ImportFlags.webhooks])) + } else { + opts.includeWebhooks = true + } + opts.whkRegenUuid=true + + when: + def result = sut.importArchive(opts) + + then: + 1 * api.importProjectArchive( + 'Aproj', + _, + _, + _, + _, + _, + true, + _, + true, + _, + _, + _, + _ + ) >> Calls.response(new ProjectImportStatus(successful: true)) + 0 * api._(*_) + result == 0 + where: + useIncludeFlags << [true, false] + } + def "import include via direct flags"() { def api = Mock(RundeckApi) From ad75066dab42eab4bca183990e0407452862f7fb Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Mon, 18 Nov 2024 09:56:47 -0800 Subject: [PATCH 4/5] cleanup --- .../org/rundeck/client/tool/commands/projects/Archives.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 43d7aca5..b5394bb7 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 @@ -203,12 +203,12 @@ public int importArchive(@CommandLine.Mixin ArchiveImportOpts opts) throws Input RequestBody body = RequestBody.create(input, Client.MEDIA_TYPE_ZIP); Map extraCompOpts = new HashMap<>(); - if (opts.components != null && opts.components.size() > 0) { + if (opts.components != null && !opts.components.isEmpty()) { for (String component : opts.components) { extraCompOpts.put("importComponents." + component, "true"); } } - if (opts.componentOptions != null && opts.componentOptions.size() > 0) { + if (opts.componentOptions != null && !opts.componentOptions.isEmpty()) { for (Map.Entry stringStringEntry : opts.componentOptions.entrySet()) { extraCompOpts.put("importOpts." + stringStringEntry.getKey(), stringStringEntry.getValue()); } From c8654fb18019d8c078bb6cb261374c9f8da9bf6e Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Mon, 18 Nov 2024 10:09:33 -0800 Subject: [PATCH 5/5] de conflict the -i flag --- .../org/rundeck/client/tool/commands/projects/Archives.java | 2 +- 1 file changed, 1 insertion(+), 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 b5394bb7..0469368a 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 @@ -123,7 +123,7 @@ public static class ArchiveImportOpts extends BaseOptions{ Map componentOptions; @CommandLine.Option( - names = {"--include", "-i"}, + names = {"--include"/*, "-i"*/}, //note: -i would conflict with async import description = "List of archive contents to import. [executions,config,acl,scm,webhooks,nodeSources]. Default: " + "executions. (webhooks: requires API v34. nodeSources: requires API v38).")