diff --git a/buildAndReleaseTask/awsServiceEndpoint.ts b/buildAndReleaseTask/awsServiceEndpoint.ts new file mode 100644 index 0000000..6b289f4 --- /dev/null +++ b/buildAndReleaseTask/awsServiceEndpoint.ts @@ -0,0 +1,53 @@ +// Copyright 2016-2023, Pulumi Corporation. All rights reserved. + +import * as tl from "azure-pipelines-task-lib/task"; + +export interface IAWSServiceEndpoint { + accessKeyId: string; + secretAccessKey: string; + sessionToken: string; + roleArn: string; + region: string; +} + +export function getAWSServiceEndpoint( + connectedServiceName: string +): IAWSServiceEndpoint | undefined { + const endpointAuthorization = tl.getEndpointAuthorization( + connectedServiceName, + true + ); + if (!endpointAuthorization) { + return undefined; + } + + const endpoint = { + accessKeyId: tl.getEndpointAuthorizationParameter( + connectedServiceName, + "username", + false + ), + secretAccessKey: tl.getEndpointAuthorizationParameter( + connectedServiceName, + "password", + false + ), + sessionToken: tl.getEndpointAuthorizationParameter( + connectedServiceName, + "sessionToken", + true + ), + roleArn: tl.getEndpointAuthorizationParameter( + connectedServiceName, + "assumeRoleArn", + true + ), + region: tl.getEndpointAuthorizationParameter( + connectedServiceName, + "region", + true + ), + } as IAWSServiceEndpoint; + + return endpoint; +} diff --git a/buildAndReleaseTask/models.d.ts b/buildAndReleaseTask/models.d.ts index 899a8f1..b18e44d 100644 --- a/buildAndReleaseTask/models.d.ts +++ b/buildAndReleaseTask/models.d.ts @@ -12,6 +12,7 @@ declare module "models" { // and assert the value at the place of // its use. azureSubscription?: string; + awsServiceConnection?: string; command?: string; loginArgs?: string; args?: string; diff --git a/buildAndReleaseTask/pulumi.ts b/buildAndReleaseTask/pulumi.ts index d2cf655..dbd4f82 100644 --- a/buildAndReleaseTask/pulumi.ts +++ b/buildAndReleaseTask/pulumi.ts @@ -6,6 +6,7 @@ import { join as pathJoin } from "path"; import * as tl from "azure-pipelines-task-lib/task"; import * as tr from "azure-pipelines-task-lib/toolrunner"; +import { getAWSServiceEndpoint } from "./awsServiceEndpoint"; import { getServiceEndpoint } from "./serviceEndpoint"; import { INSTALLED_PULUMI_VERSION, PULUMI_ACCESS_TOKEN } from "./vars"; @@ -243,6 +244,36 @@ function tryGetAzureEnvVarsFromServiceEndpoint(): IEnvMap { AZURE_TENANT_ID: serviceEndpoint.tenantId, }; } +/** + * If the `serviceEndpoint` param is not `undefined`, then + * this function returns an env var map with the `AWS_*` + * env vars. + */ +function tryGetAWSEnvVarsFromServiceEndpoint(): IEnvMap { + const connectedServiceName = tl.getInput("awsServiceConnection", false); + if (!connectedServiceName) { + return {}; + } + tl.debug(tl.loc("Debug_AWSServiceEndpointName", connectedServiceName)); + + const awsServiceEndpoint = getAWSServiceEndpoint(connectedServiceName); + if (awsServiceEndpoint) { + tl.debug( + `Service endpoint retrieved with access key ID ${awsServiceEndpoint.accessKeyId}` + ); + } + if (!awsServiceEndpoint) { + return {}; + } + + return { + AWS_ACCESS_KEY_ID: awsServiceEndpoint.accessKeyId, + AWS_SECRET_ACCESS_KEY: awsServiceEndpoint.secretAccessKey, + AWS_SESSION_TOKEN: awsServiceEndpoint.sessionToken, + AWS_ROLE_ARN: awsServiceEndpoint.roleArn, + AWS_REGION: awsServiceEndpoint.region, + }; +} /** * Returns all variables available to this task. @@ -282,7 +313,9 @@ export async function runPulumi(taskConfig: TaskConfig) { const toolPath = tl.which("pulumi"); const agentEnvVars = tryGetEnvVars(); const azureServiceEndpointEnvVars = tryGetAzureEnvVarsFromServiceEndpoint(); + const awsServiceEndpointEnvVars = tryGetAWSEnvVarsFromServiceEndpoint(); const loginEnvVars = { + ...awsServiceEndpointEnvVars, ...azureServiceEndpointEnvVars, ...agentEnvVars, ...processEnv, @@ -329,6 +362,7 @@ export async function runPulumi(taskConfig: TaskConfig) { } const envVars: IEnvMap = { + ...awsServiceEndpointEnvVars, ...azureServiceEndpointEnvVars, ...agentEnvVars, ...processEnv, diff --git a/buildAndReleaseTask/task.json b/buildAndReleaseTask/task.json index 4d3c202..96b9dd7 100644 --- a/buildAndReleaseTask/task.json +++ b/buildAndReleaseTask/task.json @@ -32,6 +32,14 @@ "required": "false", "helpMarkDown": "Select the Azure Resource Manager subscription for the deployment. If you do not provide a service connection, ensure that you have configured your cloud provider by following the setup instructions for your respective [cloud provider](https://www.pulumi.com/docs/intro/cloud-providers/)." }, + { + "name": "awsServiceConnection", + "type": "connectedService:AWS", + "label": "AWS Service Connection", + "defaultValue": "", + "required": "false", + "helpMarkDown": "Select the AWS Service Connection for the deployment. If you do not provide a service connection, ensure that you have configured your cloud provider by following the setup instructions for your respective [cloud provider](https://www.pulumi.com/docs/intro/cloud-providers/)." + }, { "name": "command", "type": "pickList", @@ -147,6 +155,7 @@ "Debug_Login": "Logging in to Pulumi CLI.", "Debug_PrintingVersion": "Printing version.", "Debug_ServiceEndpointName": "Using service endpoint '%s'.", + "Debug_AWSServiceEndpointName": "Using AWS service endpoint '%s'.", "Debug_TempDirectoryNotSet": "agent.tempdirectory not set. Using $HOME/temp.", "Debug_LatestPulumiVersion": "Latest pulumi version is '%s'.", "Debug_CachingPulumiToHome": "Caching pulumi CLI to path '%s'.", diff --git a/buildAndReleaseTask/taskConfig.ts b/buildAndReleaseTask/taskConfig.ts index 757a070..02bf216 100644 --- a/buildAndReleaseTask/taskConfig.ts +++ b/buildAndReleaseTask/taskConfig.ts @@ -8,6 +8,7 @@ import { TaskConfig } from "models"; export function getTaskConfig(): TaskConfig { return { azureSubscription: tl.getInput("azureSubscription"), + awsServiceConnection: tl.getInput("awsServiceConnection"), command: tl.getInput("command"), loginArgs: tl.getInput("loginArgs"), args: tl.getInput("args"), diff --git a/buildAndReleaseTask/tests/envvars.ts b/buildAndReleaseTask/tests/envvars.ts index a3a250e..e5076f7 100644 --- a/buildAndReleaseTask/tests/envvars.ts +++ b/buildAndReleaseTask/tests/envvars.ts @@ -37,6 +37,14 @@ tmr.registerMock("./serviceEndpoint", { }, }); +tmr.registerMock("./awsServiceEndpoint", { + getAWSServiceEndpoint: (_: string) => { + // Returning undefined to test that our task extension isn't requiring + // an AWS Endpoint. + return undefined; + }, +}); + tmr.registerMock("./version", { getLatestPulumiVersion: (): Promise => { return Promise.resolve(latestPulumiVersion); diff --git a/buildAndReleaseTask/tests/envvars_storage.ts b/buildAndReleaseTask/tests/envvars_storage.ts index a4bc294..475a462 100644 --- a/buildAndReleaseTask/tests/envvars_storage.ts +++ b/buildAndReleaseTask/tests/envvars_storage.ts @@ -38,6 +38,14 @@ tmr.registerMock("./serviceEndpoint", { }, }); +tmr.registerMock("./awsServiceEndpoint", { + getAWSServiceEndpoint: (_: string) => { + // Returning undefined to test that our task extension isn't requiring + // an AWS Endpoint. + return undefined; + }, +}); + tmr.registerMock("./version", { getLatestPulumiVersion: (): Promise => { return Promise.resolve(latestPulumiVersion); diff --git a/buildAndReleaseTask/tests/success.ts b/buildAndReleaseTask/tests/success.ts index ce40bba..c8b1f27 100644 --- a/buildAndReleaseTask/tests/success.ts +++ b/buildAndReleaseTask/tests/success.ts @@ -4,6 +4,7 @@ import * as ma from "azure-pipelines-task-lib/mock-answer"; import * as tmrm from "azure-pipelines-task-lib/mock-run"; import * as path from "path"; +import { IAWSServiceEndpoint } from "../awsServiceEndpoint"; import { IServiceEndpoint } from "../serviceEndpoint"; const taskPath = path.join(__dirname, "..", "index.js"); @@ -24,6 +25,7 @@ process.env["HOME"] = "/fake/home"; tmr.setVariableName("PULUMI_ACCESS_TOKEN", "fake-access-token", true); // Set the mock inputs for the task. tmr.setInput("azureSubscription", "fake-subscription-id"); +tmr.setInput("awsServiceConnection", "fake-aws-service-endpoint"); tmr.setInput("command", "preview"); tmr.setInput("cwd", "dir/"); tmr.setInput("stack", "myOrg/project/dev"); @@ -40,6 +42,18 @@ tmr.registerMock("./serviceEndpoint", { }, }); +tmr.registerMock("./awsServiceEndpoint", { + getAWSServiceEndpoint: (_: string): IAWSServiceEndpoint => { + return { + accessKeyId: "fake-access-key-id", + secretAccessKey: "fake-secret-access-key", + sessionToken: "fake-session-token", + roleArn: "fake-role-arn", + region: "fake-region", + } + }, +}); + tmr.registerMock("./version", { getLatestPulumiVersion: (): Promise => { return Promise.resolve(latestPulumiVersion); diff --git a/integrationtest.azure-pipelines.yml b/integrationtest.azure-pipelines.yml index 1f98557..ce8aacd 100644 --- a/integrationtest.azure-pipelines.yml +++ b/integrationtest.azure-pipelines.yml @@ -29,6 +29,7 @@ jobs: - task: Test Pulumi@1 inputs: + awsServiceConnection: "$(Example.AWS.ServiceConnectName)" command: "preview" cwd: "./examples/aws" stack: "$(Example.AWS.StackName)" diff --git a/overview.md b/overview.md index 004978d..aa93481 100644 --- a/overview.md +++ b/overview.md @@ -16,9 +16,9 @@ Read on to learn how you can get started quickly. - You must have an active Azure DevOps account and an organization. Create a new account at https://dev.azure.com. - Install this extension to your organization. To do this, you must be an admin of the organization. - Once installed, you may have to have your admin make the extension available to you or your project. -- You'll need an Azure subscription if you plan on creating resources in Azure. Create a [service connection](https://docs.microsoft.com/en-us/azure/devops/pipelines/library/connect-to-azure?view=azure-devops) for the Azure Subscription in your DevOps project. - - **Note**: At this time, only a `Service Principal Authentication` based service connection can be used with this extension. - - The name of this service connection is what you will use in the Pulumi task for the input `azureSubscription` if you are using the YAML configuration. +- You'll need an Azure subscription if you plan on creating resources in Azure. Create a [service connection](https://docs.microsoft.com/en-us/azure/devops/pipelines/library/connect-to-azure?view=azure-devops) for the Azure Subscription or AWS Authentication in your DevOps project. + - **Note**: For Azure Service Connections, only `Service Principal Authentication` work with this extension. + - The name of the relevant service connection is what you will use in the Pulumi task for the input `azureSubscription` or `awsServiceConnection` if you are using the YAML configuration. ## Quickstart @@ -192,7 +192,7 @@ Try to uninstall the extension, and then re-install it. Most of the times this r Pulumi supports several [cloud providers](https://www.pulumi.com/docs/intro/cloud-providers/), including [Kubernetes](https://www.pulumi.com/docs/intro/cloud-providers/kubernetes/). You can deploy to any cloud provider that Pulumi supports using this task extension, by simply setting the required environment variables as part of each cloud provider's setup, as your Pipeline's build variable. -For example, in order to deploy to [AWS](https://www.pulumi.com/docs/intro/cloud-providers/aws/setup/#environment-variables), simply set the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` env vars either as pipeline variables or in a variable group that is linked to your pipeline. +For example, in order to deploy to [AWS](https://www.pulumi.com/docs/intro/cloud-providers/aws/setup/#environment-variables), you may use the AWS Service Connection or by simply set the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` env vars either as pipeline variables or in a variable group that is linked to your pipeline. ## Troubleshooting