-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: deploy tenant release v6 (#281)
- Loading branch information
1 parent
2802390
commit a2c2130
Showing
11 changed files
with
482 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
source/tasks/DeployTenant/TenantedDeployV6/createDeployment.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { Client, CreateDeploymentTenantedCommandV1, DeploymentRepository, Logger, TenantRepository } from "@octopusdeploy/api-client"; | ||
import { InputParameters } from "./input-parameters"; | ||
import os from "os"; | ||
import { TaskWrapper } from "tasks/Utils/taskInput"; | ||
|
||
export interface DeploymentResult { | ||
serverTaskId: string; | ||
tenantName: string; | ||
} | ||
|
||
export async function createDeploymentFromInputs(client: Client, parameters: InputParameters, task: TaskWrapper, logger: Logger): Promise<DeploymentResult[]> { | ||
logger.info?.("🐙 Deploying a release in Octopus Deploy..."); | ||
const command: CreateDeploymentTenantedCommandV1 = { | ||
spaceName: parameters.space, | ||
ProjectName: parameters.project, | ||
ReleaseVersion: parameters.releaseNumber, | ||
EnvironmentName: parameters.environment, | ||
Tenants: parameters.tenants, | ||
TenantTags: parameters.tenantTags, | ||
UseGuidedFailure: parameters.useGuidedFailure, | ||
Variables: parameters.variables, | ||
}; | ||
|
||
try { | ||
const deploymentRepository = new DeploymentRepository(client, parameters.space); | ||
const response = await deploymentRepository.createTenanted(command); | ||
|
||
client.info(`🎉 ${response.DeploymentServerTasks.length} Deployment${response.DeploymentServerTasks.length > 1 ? "s" : ""} queued successfully!`); | ||
|
||
if (response.DeploymentServerTasks.length === 0) { | ||
throw new Error("Expected at least one deployment to be queued."); | ||
} | ||
if (response.DeploymentServerTasks[0].ServerTaskId === null || response.DeploymentServerTasks[0].ServerTaskId === undefined) { | ||
throw new Error("Server task id was not deserialized correctly."); | ||
} | ||
|
||
const deploymentIds = response.DeploymentServerTasks.map((x) => x.DeploymentId); | ||
|
||
const deployments = await deploymentRepository.list({ ids: deploymentIds, take: deploymentIds.length }); | ||
|
||
const tenantIds = deployments.Items.map((d) => d.TenantId || ""); | ||
const tenantRepository = new TenantRepository(client, parameters.space); | ||
const tenants = await tenantRepository.list({ ids: tenantIds, take: tenantIds.length }); | ||
|
||
const results = response.DeploymentServerTasks.map((x) => { | ||
return { | ||
serverTaskId: x.ServerTaskId, | ||
tenantName: tenants.Items.filter((e) => e.Id === deployments.Items.filter((d) => d.TaskId === x.ServerTaskId)[0].TenantId)[0].Name, | ||
}; | ||
}); | ||
|
||
task.setOutputVariable("server_tasks", JSON.stringify(results)); | ||
|
||
return results; | ||
} catch (error: unknown) { | ||
if (error instanceof Error) { | ||
task.setFailure(`"Failed to execute command. ${error.message}${os.EOL}${error.stack}`, true); | ||
} else { | ||
task.setFailure(`"Failed to execute command. ${error}`, true); | ||
} | ||
throw error; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { Client, ClientConfiguration, Logger } from "@octopusdeploy/api-client"; | ||
import { OctoServerConnectionDetails } from "../../Utils/connection"; | ||
import { createDeploymentFromInputs } from "./createDeployment"; | ||
import { getInputParameters } from "./input-parameters"; | ||
import os from "os"; | ||
import { TaskWrapper } from "tasks/Utils/taskInput"; | ||
|
||
export class Deploy { | ||
constructor(readonly connection: OctoServerConnectionDetails, readonly task: TaskWrapper, readonly logger: Logger) {} | ||
|
||
public async run() { | ||
try { | ||
const inputParameters = getInputParameters(this.logger, this.task); | ||
|
||
const config: ClientConfiguration = { | ||
userAgentApp: "AzureDevOps deploy-release-tenanted", | ||
instanceURL: this.connection.url, | ||
apiKey: this.connection.apiKey, | ||
logging: this.logger, | ||
}; | ||
const client = await Client.create(config); | ||
|
||
createDeploymentFromInputs(client, inputParameters, this.task, this.logger); | ||
|
||
this.task.setSuccess("Deployment succeeded."); | ||
} catch (error: unknown) { | ||
if (error instanceof Error) { | ||
this.task.setFailure(`"Failed to successfully deploy release. ${error.message}${os.EOL}${error.stack}`, true); | ||
} else { | ||
this.task.setFailure(`"Failed to successfully deploy release. ${error}`, true); | ||
} | ||
throw error; | ||
} | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { getDefaultOctopusConnectionDetailsOrThrow } from "../../Utils/connection"; | ||
import { Deploy } from "./deploy"; | ||
import { ConcreteTaskWrapper, TaskWrapper } from "tasks/Utils/taskInput"; | ||
import { Logger } from "@octopusdeploy/api-client"; | ||
import * as tasks from "azure-pipelines-task-lib/task"; | ||
|
||
const connection = getDefaultOctopusConnectionDetailsOrThrow(); | ||
|
||
const logger: Logger = { | ||
debug: (message) => { | ||
tasks.debug(message); | ||
}, | ||
info: (message) => console.log(message), | ||
warn: (message) => tasks.warning(message), | ||
error: (message, err) => { | ||
if (err !== undefined) { | ||
tasks.error(err.message); | ||
} else { | ||
tasks.error(message); | ||
} | ||
}, | ||
}; | ||
|
||
const task: TaskWrapper = new ConcreteTaskWrapper(); | ||
|
||
new Deploy(connection, task, logger).run(); |
113 changes: 113 additions & 0 deletions
113
source/tasks/DeployTenant/TenantedDeployV6/input-parameters.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { Logger } from "@octopusdeploy/api-client"; | ||
import { TaskWrapper } from "tasks/Utils/taskInput"; | ||
import { getInputParameters } from "./input-parameters"; | ||
import * as tasks from "azure-pipelines-task-lib/task"; | ||
|
||
export class MockTaskWrapper implements TaskWrapper { | ||
lastResult?: tasks.TaskResult | undefined = undefined; | ||
lastResultMessage: string | undefined = undefined; | ||
lastResultDone: boolean | undefined = undefined; | ||
|
||
stringValues: Map<string, string> = new Map<string, string>(); | ||
boolValues: Map<string, boolean> = new Map<string, boolean>(); | ||
outputVariables: Map<string, string> = new Map<string, string>(); | ||
|
||
addVariableString(name: string, value: string) { | ||
this.stringValues.set(name, value); | ||
} | ||
|
||
addVariableBoolean(name: string, value: boolean) { | ||
this.boolValues.set(name, value); | ||
} | ||
|
||
getInput(name: string, _required?: boolean | undefined): string | undefined { | ||
return this.stringValues.get(name); | ||
} | ||
|
||
getBoolean(name: string, _required?: boolean | undefined): boolean | undefined { | ||
return this.boolValues.get(name); | ||
} | ||
|
||
setSuccess(message: string, done?: boolean | undefined): void { | ||
this.lastResult = tasks.TaskResult.Succeeded; | ||
this.lastResultMessage = message; | ||
this.lastResultDone = done; | ||
} | ||
setFailure(message: string, done?: boolean | undefined): void { | ||
this.lastResult = tasks.TaskResult.Failed; | ||
this.lastResultMessage = message; | ||
this.lastResultDone = done; | ||
} | ||
|
||
setOutputVariable(name: string, value: string): void { | ||
this.outputVariables.set(name, value); | ||
} | ||
} | ||
|
||
describe("getInputParameters", () => { | ||
let logger: Logger; | ||
let task: MockTaskWrapper; | ||
beforeEach(() => { | ||
logger = {}; | ||
task = new MockTaskWrapper(); | ||
}); | ||
|
||
test("all regular fields supplied", () => { | ||
task.addVariableString("Space", "Default"); | ||
task.addVariableString("Variables", "var1: value1\nvar2: value2"); | ||
task.addVariableString("Environment", "dev"); | ||
task.addVariableString("Project", "Awesome project"); | ||
task.addVariableString("ReleaseNumber", "1.0.0"); | ||
task.addVariableString("DeployForTenants", "Tenant 1\nTenant 2"); | ||
task.addVariableString("DeployForTenantTags", "tag set 1/tag 1\ntag set 1/tag 2"); | ||
|
||
const inputParameters = getInputParameters(logger, task); | ||
expect(inputParameters.environment).toBe("dev"); | ||
expect(inputParameters.project).toBe("Awesome project"); | ||
expect(inputParameters.releaseNumber).toBe("1.0.0"); | ||
expect(inputParameters.space).toBe("Default"); | ||
expect(inputParameters.variables).toStrictEqual({ var1: "value1", var2: "value2" }); | ||
expect(inputParameters.tenants).toStrictEqual(["Tenant 1", "Tenant 2"]); | ||
expect(inputParameters.tenantTags).toStrictEqual(["tag set 1/tag 1", "tag set 1/tag 2"]); | ||
|
||
expect(task.lastResult).toBeUndefined(); | ||
expect(task.lastResultMessage).toBeUndefined(); | ||
expect(task.lastResultDone).toBeUndefined(); | ||
}); | ||
|
||
test("variables in additional fields", () => { | ||
task.addVariableString("Space", "Default"); | ||
task.addVariableString("Variables", "var1: value1\nvar2: value2"); | ||
task.addVariableString("AdditionalArguments", "-v var3=value3 --variable var4=value4"); | ||
task.addVariableString("DeployForTenants", "Tenant 1"); | ||
|
||
const inputParameters = getInputParameters(logger, task); | ||
expect(inputParameters.variables).toStrictEqual({ var1: "value1", var2: "value2", var3: "value3", var4: "value4" }); | ||
}); | ||
|
||
test("missing space", () => { | ||
const t = () => { | ||
getInputParameters(logger, task); | ||
}; | ||
expect(t).toThrowError("Failed to successfully build parameters: space name is required."); | ||
}); | ||
|
||
test("duplicate variable name, variables field takes precedence", () => { | ||
task.addVariableString("Space", "Default"); | ||
task.addVariableString("Variables", "var1: value1\nvar2: value2"); | ||
task.addVariableString("AdditionalArguments", "-v var1=value3"); | ||
task.addVariableString("DeployForTenants", "Tenant 1"); | ||
const inputParameters = getInputParameters(logger, task); | ||
expect(inputParameters.variables).toStrictEqual({ var1: "value1", var2: "value2" }); | ||
}); | ||
|
||
test("validate tenants and tags", () => { | ||
task.addVariableString("Space", "Default"); | ||
|
||
const t = () => { | ||
getInputParameters(logger, task); | ||
}; | ||
|
||
expect(t).toThrowError("Failed to successfully build parameters.\nMust provide at least one tenant or tenant tag."); | ||
}); | ||
}); |
88 changes: 88 additions & 0 deletions
88
source/tasks/DeployTenant/TenantedDeployV6/input-parameters.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import commandLineArgs from "command-line-args"; | ||
import shlex from "shlex"; | ||
import { getLineSeparatedItems } from "../../Utils/inputs"; | ||
import { Logger, PromptedVariableValues } from "@octopusdeploy/api-client"; | ||
import { TaskWrapper } from "tasks/Utils/taskInput"; | ||
|
||
export interface InputParameters { | ||
// Optional: You should prefer the OCTOPUS_SPACE environment variable | ||
space: string; | ||
// Required | ||
project: string; | ||
releaseNumber: string; | ||
environment: string; | ||
tenants: string[]; | ||
tenantTags: string[]; | ||
|
||
// Optional | ||
useGuidedFailure?: boolean; | ||
variables?: PromptedVariableValues; | ||
} | ||
|
||
export function getInputParameters(logger: Logger, task: TaskWrapper): InputParameters { | ||
const space = task.getInput("Space"); | ||
if (!space) { | ||
throw new Error("Failed to successfully build parameters: space name is required."); | ||
} | ||
|
||
const variablesMap: PromptedVariableValues | undefined = {}; | ||
|
||
const additionalArguments = task.getInput("AdditionalArguments"); | ||
logger.debug?.("AdditionalArguments:" + additionalArguments); | ||
if (additionalArguments) { | ||
const optionDefs = [{ name: "variable", alias: "v", type: String, multiple: true }]; | ||
const splitArgs = shlex.split(additionalArguments); | ||
const options = commandLineArgs(optionDefs, { argv: splitArgs }); | ||
logger.debug?.(JSON.stringify(options)); | ||
for (const variable of options.variable) { | ||
const variableMap = variable.split("=").map((x: string) => x.trim()); | ||
variablesMap[variableMap[0]] = variableMap[1]; | ||
} | ||
} | ||
|
||
const variablesField = task.getInput("Variables"); | ||
logger.debug?.("Variables: " + variablesField); | ||
if (variablesField) { | ||
const variables = getLineSeparatedItems(variablesField).map((p) => p.trim()) || undefined; | ||
if (variables) { | ||
for (const variable of variables) { | ||
const variableMap = variable.split(":").map((x) => x.trim()); | ||
variablesMap[variableMap[0]] = variableMap[1]; | ||
} | ||
} | ||
} | ||
|
||
const tenantsField = task.getInput("DeployForTenants"); | ||
logger.debug?.("Tenants: " + tenantsField); | ||
const tagsField = task.getInput("DeployForTenantTags"); | ||
logger.debug?.("Tenant Tags: " + tagsField); | ||
const tags = getLineSeparatedItems(tagsField || "")?.map((t: string) => t.trim()) || []; | ||
|
||
const parameters: InputParameters = { | ||
space: task.getInput("Space") || "", | ||
project: task.getInput("Project", true) || "", | ||
releaseNumber: task.getInput("ReleaseNumber", true) || "", | ||
environment: task.getInput("Environment", true) || "", | ||
useGuidedFailure: task.getBoolean("UseGuidedFailure") || undefined, | ||
variables: variablesMap || undefined, | ||
tenants: getLineSeparatedItems(tenantsField || "")?.map((t: string) => t.trim()) || [], | ||
tenantTags: tags, | ||
}; | ||
|
||
const errors: string[] = []; | ||
if (parameters.space === "") { | ||
errors.push("The Octopus space name is required."); | ||
} | ||
|
||
if (parameters.tenantTags.length === 0 && parameters.tenants.length === 0) { | ||
errors.push("Must provide at least one tenant or tenant tag."); | ||
} | ||
|
||
if (errors.length > 0) { | ||
throw new Error("Failed to successfully build parameters.\n" + errors.join("\n")); | ||
} | ||
|
||
logger.debug?.(JSON.stringify(parameters)); | ||
|
||
return parameters; | ||
} |
Oops, something went wrong.