Skip to content

Commit

Permalink
Merge pull request #535 from rundeck/RSE-734
Browse files Browse the repository at this point in the history
RSE-734 Fix: Asynchronous Project Import
  • Loading branch information
DarwisNarvaezDev committed Jan 19, 2024
2 parents d0037a6 + 382c234 commit 904af23
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ Call<ProjectImportStatus> importProjectArchive(
@Query("importWebhooks") Boolean importWebhooks,
@Query("whkRegenAuthTokens") Boolean whkRegenAuthTokens,
@Query("importNodesSources") Boolean importNodesSources,
@Query("asyncImport") Boolean asyncImport,
@Body RequestBody body
);

Expand All @@ -411,10 +412,17 @@ Call<ProjectImportStatus> importProjectArchive(
@Query("importWebhooks") Boolean importWebhooks,
@Query("whkRegenAuthTokens") Boolean whkRegenAuthTokens,
@Query("importNodesSources") Boolean importNodesSources,
@Query("asyncImport") Boolean asyncImport,
@QueryMap Map<String,String> params,
@Body RequestBody body
);

@Headers("Accept: application/json")
@GET("project/{project}/import/status")
Call<AsyncProjectImportStatus> asyncImportProjectArchiveStatus(
@Path("project") String project
);

@Headers("Accept: application/json")
@GET("project/{project}/executions/running")
Call<ExecutionList> runningExecutions(
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,22 @@

package org.rundeck.client.tool.commands.projects;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
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;
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.ProjectNameOptions;
import org.rundeck.client.tool.options.ProjectRequiredNameOptions;
import org.rundeck.client.util.Client;
import org.rundeck.client.util.ServiceClient;
Expand Down Expand Up @@ -92,6 +97,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 " +
Expand All @@ -113,6 +121,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();
Expand Down Expand Up @@ -149,12 +169,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 = "<RD_URL>";
String projectPlaceholder = "<project>";
String apiVersionPlaceholder = "<api_version>";

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -69,6 +71,59 @@ class ArchivesSpec extends Specification {
_,
_,
_,
_,
[
'importComponents.test-comp': 'true',
'importOpts.test-comp.key' : 'value',
],
_
) >> Calls.response(new ProjectImportStatus(successful: true))
0 * api._(*_)
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',
Expand All @@ -79,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)
Expand Down Expand Up @@ -118,6 +202,7 @@ class ArchivesSpec extends Specification {
_,
_,
_,
_,
[:],
_
) >> Calls.response(new ProjectImportStatus(resultsmap))
Expand Down

0 comments on commit 904af23

Please sign in to comment.