Skip to content
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
114 changes: 108 additions & 6 deletions e2e/src/test/java/octopus/teamcity/e2e/dsl/OctopusProvisioning.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.Map;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

Expand Down Expand Up @@ -115,15 +116,103 @@ private static String createLifecycleWithPhases(
return JsonParser.parseString(resp.body()).getAsJsonObject().get("Id").getAsString();
}

/**
* Adds a runbook with the same single run-on-server script step to an existing project, and
* publishes a snapshot so it can be run without naming one. Returns the runbook's id.
*/
public static String createRunbookWithServerScriptStep(
final OctopusClient client,
final SpaceHome spaceHome,
final String octopusBaseUrl,
final String apiKey,
final String projectName,
final String runbookName)
throws Exception {
final String projectId =
ProjectApi.create(client, spaceHome)
.getByName(projectName)
.orElseThrow(() -> new IllegalStateException("No project named " + projectName))
.getProperties()
.getId();

final JsonObject retention = new JsonObject();
retention.addProperty("QuantityToKeep", 10);
retention.addProperty("ShouldKeepForever", false);
final JsonObject runbook = new JsonObject();
runbook.addProperty("ProjectId", projectId);
runbook.addProperty("Name", runbookName);
runbook.addProperty("EnvironmentScope", "All");
runbook.add("RunRetentionPolicy", retention);

final JsonObject created =
readJson(
octopusRequest(
"POST",
octopusBaseUrl + stripUriTemplate(spaceHome.getRunbooksLink()),
apiKey,
runbook.toString()),
"create runbook");
final String runbookId = created.get("Id").getAsString();
final String processUrl =
octopusBaseUrl
+ stripUriTemplate(spaceHome.getRunbookProcessesLink())
+ "/"
+ created.get("RunbookProcessId").getAsString();

final JsonObject process =
readJson(octopusRequest("GET", processUrl, apiKey, null), "get runbook process");
process.add("Steps", scriptStep());
readJson(octopusRequest("PUT", processUrl, apiKey, process.toString()), "put runbook process");

final JsonObject template =
readJson(
octopusRequest("GET", processUrl + "/runbookSnapshotTemplate", apiKey, null),
"get runbook snapshot template");
final JsonObject snapshot = new JsonObject();
snapshot.addProperty("ProjectId", projectId);
snapshot.addProperty("RunbookId", runbookId);
snapshot.addProperty("Name", template.get("NextNameIncrement").getAsString());
snapshot.add("SelectedPackages", new JsonArray());
readJson(
octopusRequest(
"POST",
octopusBaseUrl
+ stripUriTemplate(spaceHome.getRunbookSnapshotsLink())
+ "?publish=true",
apiKey,
snapshot.toString()),
"publish runbook snapshot");

return runbookId;
}

/** True if the runbook has been run at least once. */
public static boolean hasRunbookRun(
final SpaceHome spaceHome,
final String octopusBaseUrl,
final String apiKey,
final String runbookId)
throws Exception {
final JsonObject runs =
readJson(
octopusRequest(
"GET",
octopusBaseUrl + stripUriTemplate(spaceHome.getRunbookRunsLink()) + "?take=100",
apiKey,
null),
"list runbook runs");
for (final JsonElement run : runs.getAsJsonArray("Items")) {
if (runbookId.equals(run.getAsJsonObject().get("RunbookId").getAsString())) {
return true;
}
}
return false;
}

private static void addInlineScriptStep(
final String octopusBaseUrl, final String apiKey, final String deploymentProcessLink)
throws Exception {
// Links may carry a URI template suffix (e.g. "{?...}"); strip it.
final String path =
deploymentProcessLink.contains("{")
? deploymentProcessLink.substring(0, deploymentProcessLink.indexOf('{'))
: deploymentProcessLink;
final String url = octopusBaseUrl + path;
final String url = octopusBaseUrl + stripUriTemplate(deploymentProcessLink);

final Http.Response get = octopusRequest("GET", url, apiKey, null);
if (get.statusCode() != 200) {
Expand All @@ -141,6 +230,19 @@ private static void addInlineScriptStep(
}
}

/** Links may carry a URI template suffix (e.g. "{?...}"); strip it. */
private static String stripUriTemplate(final String link) {
return link.contains("{") ? link.substring(0, link.indexOf('{')) : link;
}

private static JsonObject readJson(final Http.Response response, final String what) {
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new IllegalStateException(
what + " -> " + response.statusCode() + ": " + response.body());
}
return JsonParser.parseString(response.body()).getAsJsonObject();
}

private static Http.Response octopusRequest(
final String method, final String url, final String apiKey, final String body)
throws IOException {
Expand Down
27 changes: 27 additions & 0 deletions e2e/src/test/java/octopus/teamcity/e2e/dsl/TeamCityRest.java
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,33 @@ public void addPromoteReleaseStepUsingConnection(
json);
}

/**
* Adds a Run runbook (octopus.run.runbook) step referencing a connection. Waits for the runbook
* run so the build fails if the run fails.
*/
public void addRunRunbookStepUsingConnection(
final String buildTypeId,
final String connectionId,
final String projectName,
final String runbookName,
final String runIn)
throws Exception {
final String json =
createStepFeatureJson(
"Run runbook",
"octopus.run.runbook",
createProp("octopus_connection_id", connectionId),
createProp("octopus_project_name", projectName),
createProp("octopus_runbook_name", runbookName),
createProp("octopus_deployto", runIn),
createProp("octopus_waitfordeployments", "true"));
send(
"POST",
"/httpAuth/app/rest/buildTypes/" + buildTypeId + "/steps",
"application/json",
json);
}

/** Lists the names of a finished build's artifacts (top-level children). */
public String listBuildArtifacts(final String buildId) throws Exception {
return send(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ void connectionDropdownRendersOnEveryConnectionAwareForm() throws Exception {
"Push package", tc.addEmptyStep("FormIT_Steps", "octopus.push.package", "Push"));
runnerIdsByForm.put(
"Build information", tc.addEmptyStep("FormIT_Steps", "octopus.metadata", "Build info"));
runnerIdsByForm.put(
"Run runbook", tc.addEmptyStep("FormIT_Steps", "octopus.run.runbook", "Runbook"));

PlaywrightUi.withLoggedInPage(
stack,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package octopus.teamcity.e2e.test;

import static org.assertj.core.api.Assertions.assertThat;

import com.octopus.sdk.http.OctopusClient;
import com.octopus.sdk.model.space.SpaceHome;

import java.time.Duration;
import java.util.Collections;

import octopus.teamcity.e2e.dsl.OctopusProvisioning;
import octopus.teamcity.e2e.dsl.OctopusTeamCityStack;
import octopus.teamcity.e2e.dsl.SharedStack;
import octopus.teamcity.e2e.dsl.TeamCityRest;
import org.junit.jupiter.api.Test;

/** Runs a published runbook through the Run runbook step and asserts Octopus recorded the run. */
class OctopusRunRunbookE2ETest {

private static final String OCTOPUS_PROJECT = "RunRunbookIT";
private static final String RUNBOOK = "RunRunbookIT-Say hello";
// Unique to this test — environment names are space-global on the shared Octopus.
private static final String ENVIRONMENT = "RunRunbookIT-Development";

@Test
void runRunbookStepUsingConnectionRunsTheRunbook() throws Exception {
try (final OctopusTeamCityStack stack = SharedStack.full()) {
final OctopusClient client = stack.octopusClient();
final SpaceHome spaceHome = stack.spaceHome(client);

final String environmentId =
OctopusProvisioning.createEnvironment(client, spaceHome, ENVIRONMENT);
OctopusProvisioning.createProjectWithServerScriptStep(
client,
spaceHome,
stack.octopusUrlForHost(),
stack.octopusApiKey(),
OCTOPUS_PROJECT,
Collections.singletonList(environmentId));
final String runbookId =
OctopusProvisioning.createRunbookWithServerScriptStep(
client,
spaceHome,
stack.octopusUrlForHost(),
stack.octopusApiKey(),
OCTOPUS_PROJECT,
RUNBOOK);

final TeamCityRest tc = stack.rest();
tc.createProject("RunbookIT", "Runbook IT");
final String connectionId =
tc.createOctopusConnection(
"RunbookIT",
"IT Octopus",
stack.octopusUrlForContainers(),
stack.octopusApiKey(),
"");
tc.createBuildType("RunbookIT_Run", "Run runbook", "RunbookIT");
tc.addRunRunbookStepUsingConnection(
"RunbookIT_Run", connectionId, OCTOPUS_PROJECT, RUNBOOK, ENVIRONMENT);

final String buildId = tc.triggerBuild("RunbookIT_Run");
final String status = tc.waitForBuildFinished(buildId, Duration.ofMinutes(6));
final String log = tc.downloadBuildLog(buildId);

assertThat(status)
.withFailMessage("Build did not succeed. Log:\n%s", log)
.isEqualTo("SUCCESS");

assertThat(
OctopusProvisioning.hasRunbookRun(
spaceHome, stack.octopusUrlForHost(), stack.octopusApiKey(), runbookId))
.withFailMessage("No run of runbook %s found. Log:\n%s", RUNBOOK, log)
.isTrue();

assertThat(log).doesNotContain(stack.octopusApiKey());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright 2000-2012 Octopus Deploy Pty. Ltd.
*
* 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 octopus.teamcity.agent;

import java.util.ArrayList;
import java.util.Map;

import jetbrains.buildServer.agent.AgentRunningBuild;
import jetbrains.buildServer.agent.BuildRunnerContext;
import octopus.teamcity.common.OctopusConstants;
import org.jetbrains.annotations.NotNull;

public class OctopusRunRunbookBuildProcess extends OctopusBuildProcess {
public OctopusRunRunbookBuildProcess(
@NotNull AgentRunningBuild runningBuild, @NotNull BuildRunnerContext context) {
super(runningBuild, context);
}

@Override
protected String getLogMessage() {
return "Running Octopus Deploy runbook";
}

@Override
protected OctopusCommandBuilder createCommand() {
final Map<String, String> parameters = getContext().getRunnerParameters();
final OctopusConstants constants = OctopusConstants.Instance;

return new OctopusCommandBuilder() {
@Override
protected String[] buildCommand(boolean masked) {
final ArrayList<String> commands = new ArrayList<String>();
final String serverUrl = parameters.get(constants.getServerKey());
final String apiKey = parameters.get(constants.getApiKey());
final String spaceName = parameters.get(constants.getSpaceName());
final String commandLineArguments = parameters.get(constants.getCommandLineArgumentsKey());
final String projectName = parameters.get(constants.getProjectNameKey());
final String runbookName = parameters.get(constants.getRunbookNameKey());
final String snapshot = parameters.get(constants.getRunbookSnapshotKey());
final String runIn = parameters.get(constants.getDeployToKey());
final String tenants = parameters.get(constants.getTenantsKey());
final String tenanttags = parameters.get(constants.getTenantTagsKey());
final boolean wait =
Boolean.parseBoolean(parameters.get(constants.getWaitForDeployments()));
final String runTimeout = parameters.get(constants.getDeploymentTimeout());
final boolean cancelOnTimeout =
Boolean.parseBoolean(parameters.get(constants.getCancelDeploymentOnTimeout()));

commands.add("run-runbook");
commands.add("--server");
commands.add(serverUrl);
commands.add("--apikey");
commands.add(masked ? "SECRET" : apiKey);

if (spaceName != null && !spaceName.isEmpty()) {
commands.add("--space");
commands.add(spaceName);
}
commands.add("--project");
commands.add(projectName);
commands.add("--runbook");
commands.add(runbookName);
commands.add("--enableservicemessages");

if (snapshot != null && !snapshot.isEmpty()) {
commands.add("--snapshot");
commands.add(snapshot);
}

for (String env : splitCommaSeparatedValues(runIn)) {
commands.add("--environment");
commands.add(env);
}

for (String tenant : splitCommaSeparatedValues(tenants)) {
commands.add("--tenant");
commands.add(tenant);
}

for (String tenanttag : splitCommaSeparatedValues(tenanttags)) {
commands.add("--tenantTag");
commands.add(tenanttag);
}

if (wait) {
commands.add("--progress");
commands.add("--waitForRun");

if (runTimeout != null && !runTimeout.isEmpty()) {
commands.add("--runTimeout");
commands.add(runTimeout);
}

if (cancelOnTimeout) {
commands.add("--cancelOnTimeout");
}
}

if (commandLineArguments != null && !commandLineArguments.isEmpty()) {
commands.addAll(splitSpaceSeparatedValues(commandLineArguments));
}

return commands.toArray(new String[commands.size()]);
}
};
}
}
Loading
Loading