Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RSE-734 Fix: Asynchronous Project Import #535

Merged
merged 4 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading