diff --git a/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/Jobs.java b/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/Jobs.java index 9d1dcacb..268a254f 100644 --- a/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/Jobs.java +++ b/rd-cli-tool/src/main/java/org/rundeck/client/tool/commands/Jobs.java @@ -18,7 +18,7 @@ import lombok.Getter; import lombok.Setter; -import org.rundeck.client.tool.CommandOutput; +import okhttp3.MediaType; import org.rundeck.client.tool.extension.BaseCommand; import picocli.CommandLine; import org.rundeck.client.api.model.scheduler.ForecastJobItem; @@ -41,7 +41,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.function.BiFunction; @@ -157,7 +156,7 @@ public int purge(@CommandLine.Mixin Purge options, return 0; } - @CommandLine.Command(description = "Load Job definitions from a file in XML or YAML format.") + @CommandLine.Command(description = "Load Job definitions from a file in XML, YAML or JSON format.") public int load( @CommandLine.Mixin JobLoadOptions options, @CommandLine.Mixin JobFileOptions fileOptions, @@ -171,10 +170,15 @@ public int load( if (!input.canRead() || !input.isFile()) { throw new InputError(String.format("File is not readable or does not exist: %s", input)); } - + MediaType mediaType = Client.MEDIA_TYPE_XML; + if (fileOptions.getFormat() == JobFileOptions.Format.yaml) { + mediaType = Client.MEDIA_TYPE_YAML; + } else if (fileOptions.getFormat() == JobFileOptions.Format.json) { + mediaType = Client.MEDIA_TYPE_JSON; + } RequestBody requestBody = RequestBody.create( input, - fileOptions.getFormat() == JobFileOptions.Format.xml ? Client.MEDIA_TYPE_XML : Client.MEDIA_TYPE_YAML + mediaType ); String project = getRdTool().projectOrEnv(projectNameOptions); @@ -188,9 +192,9 @@ public int load( List failed = importResult.getFailed(); - printLoadResult(importResult.getSucceeded(), "Succeeded", getRdOutput(), verboseOption.isVerbose()); - printLoadResult(importResult.getSkipped(), "Skipped", getRdOutput(), verboseOption.isVerbose()); - printLoadResult(failed, "Failed", getRdOutput(), verboseOption.isVerbose()); + printLoadResult(importResult.getSucceeded(), "Succeeded", verboseOption.isVerbose()); + printLoadResult(importResult.getSkipped(), "Skipped", verboseOption.isVerbose()); + printLoadResult(failed, "Failed", verboseOption.isVerbose()); return (failed == null || failed.isEmpty()) ? 0 : 1; } @@ -198,7 +202,7 @@ public int load( private void printLoadResult( final List list, final String title, - CommandOutput output, final boolean isVerbose + final boolean isVerbose ) { if (null != list && !list.isEmpty()) { getRdOutput().info(String.format("%d Jobs %s:%n", list.size(), title)); @@ -240,9 +244,27 @@ public void list( )); } try (ResponseBody body = body1) { - if ((jobFileOptions.getFormat() != JobFileOptions.Format.yaml || - !ServiceClient.hasAnyMediaType(body.contentType(), Client.MEDIA_TYPE_YAML, Client.MEDIA_TYPE_TEXT_YAML)) && - !ServiceClient.hasAnyMediaType(body.contentType(), Client.MEDIA_TYPE_XML, Client.MEDIA_TYPE_TEXT_XML)) { + if (( + jobFileOptions.getFormat() == JobFileOptions.Format.yaml + && !ServiceClient.hasAnyMediaType( + body.contentType(), + Client.MEDIA_TYPE_YAML, + Client.MEDIA_TYPE_TEXT_YAML + ) + ) || ( + jobFileOptions.getFormat() == JobFileOptions.Format.json + && !ServiceClient.hasAnyMediaType( + body.contentType(), + Client.MEDIA_TYPE_JSON + ) + ) || ( + jobFileOptions.getFormat() == JobFileOptions.Format.xml + && !ServiceClient.hasAnyMediaType( + body.contentType(), + Client.MEDIA_TYPE_XML, + Client.MEDIA_TYPE_TEXT_XML + ) + )) { throw new IllegalStateException("Unexpected response format: " + body.contentType()); } @@ -386,7 +408,7 @@ public static String[] splitJobNameParts(final String job) { int i = job.lastIndexOf('/'); String group = job.substring(0, i); String name = job.substring(i + 1); - if ("".equals(group.trim())) { + if (group.trim().isEmpty()) { group = null; } return new String[]{group, name}; diff --git a/rd-cli-tool/src/main/java/org/rundeck/client/tool/options/JobFileOptions.java b/rd-cli-tool/src/main/java/org/rundeck/client/tool/options/JobFileOptions.java index 109fc725..03ba6ec7 100644 --- a/rd-cli-tool/src/main/java/org/rundeck/client/tool/options/JobFileOptions.java +++ b/rd-cli-tool/src/main/java/org/rundeck/client/tool/options/JobFileOptions.java @@ -41,7 +41,8 @@ public boolean isFile() { public enum Format { xml, - yaml + yaml, + json } } diff --git a/rd-cli-tool/src/test/groovy/org/rundeck/client/tool/commands/JobsSpec.groovy b/rd-cli-tool/src/test/groovy/org/rundeck/client/tool/commands/JobsSpec.groovy index cae81019..64212ab0 100644 --- a/rd-cli-tool/src/test/groovy/org/rundeck/client/tool/commands/JobsSpec.groovy +++ b/rd-cli-tool/src/test/groovy/org/rundeck/client/tool/commands/JobsSpec.groovy @@ -16,6 +16,7 @@ package org.rundeck.client.tool.commands +import okhttp3.RequestBody import org.rundeck.client.api.model.BulkToggleJobExecutionResponse import org.rundeck.client.api.model.BulkToggleJobScheduleResponse import org.rundeck.client.api.model.DeleteJob @@ -148,6 +149,84 @@ class JobsSpec extends Specification { null | null | 'a' | 'b/c' } + def "job list write to file with format #format"() { + given: + def api = Mock(RundeckApi) + RdTool rdTool = setupMock(api) + def out = Mock(CommandOutput) + Jobs command = new Jobs() + command.rdTool = rdTool + command.rdOutput = out + + def opts = new JobListOptions() + opts.project = 'ProjectName' + opts.setJob(job) + opts.setGroup(group) + + + + def fileOptions = new JobFileOptions( + format: format, + file: tempFile + ) + when: + command.list(new JobOutputFormatOption(), fileOptions, opts) + + then: + 1 * api.exportJobs('ProjectName', job, group, null, null, format.toString()) >> + Calls.response(ResponseBody.create( 'abc',MediaType.parse(contentType))) + 0 * api._(*_) + tempFile.exists() + tempFile.text == 'abc' + + where: + job = 'a' + group = 'b/c' + format | contentType + JobFileOptions.Format.xml | 'application/xml' + JobFileOptions.Format.xml | 'text/xml' + JobFileOptions.Format.json | 'application/json' + JobFileOptions.Format.yaml | 'application/yaml' + JobFileOptions.Format.yaml | 'text/yaml' + } + def "job list write to file with incorrect response format causes error #format"() { + given: + def api = Mock(RundeckApi) + RdTool rdTool = setupMock(api) + def out = Mock(CommandOutput) + Jobs command = new Jobs() + command.rdTool = rdTool + command.rdOutput = out + + def opts = new JobListOptions() + opts.project = 'ProjectName' + opts.setJob(job) + opts.setGroup(group) + + + + def fileOptions = new JobFileOptions( + format: format, + file: tempFile + ) + when: + command.list(new JobOutputFormatOption(), fileOptions, opts) + + then: + 1 * api.exportJobs('ProjectName', job, group, null, null, format.toString()) >> + Calls.response(ResponseBody.create( 'abc',MediaType.parse(contentType))) + 0 * api._(*_) + IllegalStateException e = thrown() + + where: + job = 'a' + group = 'b/c' + format | contentType + JobFileOptions.Format.xml | 'application/yaml' + JobFileOptions.Format.json | 'application/xml' + JobFileOptions.Format.yaml | 'text/json' + } + @Unroll def "jobs #action behavior"() { given: @@ -400,7 +479,7 @@ class JobsSpec extends Specification { def "job load success"() { given: def opts = new JobFileOptions() - opts.format= JobFileOptions.Format.yaml + opts.format= format opts.file=tempFile @@ -415,7 +494,9 @@ class JobsSpec extends Specification { def result = command.load(new JobLoadOptions(), opts,new ProjectNameOptions(project:'ProjectName'),new VerboseOption()) then: - 1 * api.loadJobs('ProjectName', _, 'yaml', _, _) >> + 1 * api.loadJobs('ProjectName', { RequestBody body-> + body.contentType()==expectedType + }, format.toString(), _, _) >> Calls.response(new ImportResult(succeeded: [new JobLoadItem( id:'jobid',name: 'Job Name')], skipped: [], failed: [])) 0 * api._(*_) 1 * out.info('1 Jobs Succeeded:\n') @@ -423,6 +504,12 @@ class JobsSpec extends Specification { 0 * out._(*_) result == 0 + where: + format | expectedType + JobFileOptions.Format.yaml | Client.MEDIA_TYPE_YAML + JobFileOptions.Format.xml | Client.MEDIA_TYPE_XML + JobFileOptions.Format.json | Client.MEDIA_TYPE_JSON + } def "job load with errors verbose output"() {