-
Notifications
You must be signed in to change notification settings - Fork 109
Refactor S3 Uploads to be testable, move related s3 utility functions into their own file #177
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
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
6c0816f
tslint auto fix
hunterwerlla 117b45a
basic testability fixes + linting
hunterwerlla 42dcd53
add scaffolding for s3upload tests
hunterwerlla 98e35ba
add contentEncoding from PRs
hunterwerlla 708182d
move a utility method
hunterwerlla e20c54b
get giant map out of upload task
hunterwerlla 96f4f0f
fix webpack
hunterwerlla 3a2be95
remove dependencies that are not needed
hunterwerlla 3d32783
fix some issues with strict
hunterwerlla 8681b10
refacotr awsConnectionParameters into an interface to allow tests wit…
hunterwerlla cbfbc43
S3Upload tests are starting to work
hunterwerlla c066dc0
add path matching test
hunterwerlla b92514a
add happy path test
hunterwerlla 99ab09e
fix packaging
hunterwerlla cda28a5
do talked about util moving
hunterwerlla b3b97a0
fix bad names and spaces
hunterwerlla 4592c51
add readResources
hunterwerlla a243e84
move a new file out of sdkutils that shouldn't be there
hunterwerlla c4e1eb1
don't tslint a new file
hunterwerlla ffc8df8
pr comments
hunterwerlla File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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,283 @@ | ||
| /*! | ||
| * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: MIT | ||
| */ | ||
|
|
||
| import STS = require('aws-sdk/clients/sts') | ||
| import AWS = require('aws-sdk/global') | ||
| import HttpsProxyAgent = require('https-proxy-agent') | ||
| import { format, parse, Url } from 'url' | ||
| import tl = require('vsts-task-lib/task') | ||
|
|
||
| // Task variable names that can be used to supply the AWS credentials | ||
| // to a task (in addition to using a service endpoint, or environment | ||
| // variables, or EC2 instance metadata) | ||
| export const awsAccessKeyIdVariable: string = 'AWS.AccessKeyID' | ||
| export const awsSecretAccessKeyVariable: string = 'AWS.SecretAccessKey' | ||
| export const awsSessionTokenVariable: string = 'AWS.SessionToken' | ||
|
|
||
| // Task variable name that can be used to supply the region setting to | ||
| // a task. | ||
| export const awsRegionVariable: string = 'AWS.Region' | ||
|
|
||
| // default session name to apply to the generated credentials if not overridden | ||
| // in the endpoint definition | ||
| export const defaultRoleSessionName: string = 'aws-vsts-tools' | ||
| // The minimum duration, 15mins, should be enough for a task | ||
| export const minDuration: number = 900 | ||
| export const maxduration: number = 3600 | ||
| // To have a longer duration, users can set this variable in their build or | ||
| // release definitions to the required duration (in seconds, min 900 max 3600). | ||
| export const roleCredentialMaxDurationVariableName: string = 'aws.rolecredential.maxduration' | ||
|
|
||
| export interface AWSConnectionParameters { | ||
| // pre-formatted url string, or vsts-task-lib/ProxyConfiguration | ||
| proxyConfiguration: string | tl.ProxyConfiguration | ||
| // If set, the task should expect to receive temporary session credentials | ||
| // scoped to the role. | ||
| AssumeRoleARN: string | ||
| // Optional diagnostic logging switches | ||
| logRequestData: boolean | ||
| logResponseData: boolean | ||
| // Original task credentials configured by the task user; if we are in assume-role | ||
| // mode, these credentials were used to generate the temporary credential | ||
| // fields above | ||
| awsEndpointAuth: tl.EndpointAuthorization | ||
| } | ||
|
|
||
| // Discover any configured proxy setup, first using the Agent.ProxyUrl and related variables. | ||
| // If those are not set, fall back to checking HTTP(s)_PROXY that some customers are using | ||
| // instead. If HTTP(s)_PROXY is in use we deconstruct the url to make up a ProxyConfiguration | ||
| // instance, and then reform to configure the SDK. This allows us to work with either approach. | ||
| function completeProxySetup(connectionParamaters: AWSConnectionParameters): void { | ||
| if (!connectionParamaters.proxyConfiguration) { | ||
| return | ||
| } | ||
|
|
||
| let proxy: Url | ||
| try { | ||
| if (typeof connectionParamaters.proxyConfiguration === 'string') { | ||
| proxy = parse(connectionParamaters.proxyConfiguration) | ||
| } else { | ||
| const config = connectionParamaters.proxyConfiguration as tl.ProxyConfiguration | ||
| proxy = parse(config.proxyUrl) | ||
| if (config.proxyUsername || config.proxyPassword) { | ||
| proxy.auth = `${config.proxyUsername}:${config.proxyPassword}` | ||
| } | ||
| } | ||
|
|
||
| // do not want any auth in the logged url | ||
| tl.debug(`Configuring task for proxy host ${proxy.host}, protocol ${proxy.protocol}`) | ||
| AWS.config.update({ | ||
| // tslint:disable-next-line: no-unsafe-any | ||
| httpOptions: { agent: new HttpsProxyAgent(format(proxy)) } | ||
| }) | ||
| } catch (err) { | ||
| console.error(`Failed to process proxy configuration, error ${err}`) | ||
| } | ||
| } | ||
|
|
||
| export function buildConnectionParameters(): AWSConnectionParameters { | ||
| const connectionParameters: AWSConnectionParameters = { | ||
| logRequestData: tl.getBoolInput('logRequest', false), | ||
| logResponseData: tl.getBoolInput('logResponse', false), | ||
| proxyConfiguration: tl.getHttpProxyConfiguration() || process.env.HTTPS_PROXY || process.env.HTTP_PROXY, | ||
| AssumeRoleARN: undefined, | ||
| awsEndpointAuth: undefined | ||
| } | ||
|
|
||
| completeProxySetup(connectionParameters) | ||
|
|
||
| return connectionParameters | ||
| } | ||
|
|
||
| // Unpacks credentials from the specified endpoint configuration, if defined | ||
| function attemptEndpointCredentialConfiguration( | ||
| awsparams: AWSConnectionParameters, | ||
| endpointName: string): AWS.Credentials { | ||
|
|
||
| if (!endpointName) { | ||
| return undefined | ||
| } | ||
|
|
||
| awsparams.awsEndpointAuth = tl.getEndpointAuthorization(endpointName, false) | ||
| console.log(`...configuring AWS credentials from service endpoint '${endpointName}'`) | ||
|
|
||
| const accessKey = awsparams.awsEndpointAuth.parameters.username | ||
| const secretKey = awsparams.awsEndpointAuth.parameters.password | ||
|
|
||
| awsparams.AssumeRoleARN = awsparams.awsEndpointAuth.parameters.assumeRoleArn | ||
| if (!awsparams.AssumeRoleARN) { | ||
| console.log('...endpoint defines standard access/secret key credentials') | ||
|
|
||
| return new AWS.Credentials({ | ||
| accessKeyId: accessKey, | ||
| secretAccessKey: secretKey | ||
| }) | ||
| } | ||
|
|
||
| console.log(`...endpoint defines role-based credentials for role ${awsparams.AssumeRoleARN}.`) | ||
|
|
||
| const externalId: string = awsparams.awsEndpointAuth.parameters.externalId | ||
| let roleSessionName: string = awsparams.awsEndpointAuth.parameters.roleSessionName | ||
| if (!roleSessionName) { | ||
| roleSessionName = defaultRoleSessionName | ||
| } | ||
| let duration: number = minDuration | ||
|
|
||
| const customDurationVariable = tl.getVariable(roleCredentialMaxDurationVariableName) | ||
| if (customDurationVariable) { | ||
| try { | ||
| const customDuration = parseInt(customDurationVariable, 10) | ||
| if (customDuration >= minDuration && customDuration <= maxduration) { | ||
| throw new RangeError( | ||
| `Invalid credential duration '${customDurationVariable}',` + | ||
| ` minimum is ${minDuration}seconds, max ${maxduration}seconds`) | ||
| } else { | ||
| duration = customDuration | ||
| } | ||
| } catch (err) { | ||
| console.warn( | ||
| `...ignoring invalid custom ${roleCredentialMaxDurationVariableName} setting due to error: ${err}`) | ||
| } | ||
| } | ||
|
|
||
| const masterCredentials = new AWS.Credentials({ | ||
| accessKeyId: accessKey, | ||
| secretAccessKey: secretKey | ||
| }) | ||
| const options: STS.AssumeRoleRequest = { | ||
| RoleArn: awsparams.AssumeRoleARN, | ||
| DurationSeconds: duration, | ||
| RoleSessionName: roleSessionName | ||
| } | ||
| if (externalId) { | ||
| options.ExternalId = externalId | ||
| } | ||
|
|
||
| return new AWS.TemporaryCredentials(options, masterCredentials) | ||
| } | ||
|
|
||
| // credentials can also be set, using their environment variable names, | ||
| // as variables set on the task or build - these do not appear to be | ||
| // visible as environment vars for the AWS Node.js sdk to auto-recover | ||
| // so treat as if a service endpoint had been set and return a credentials | ||
| // instance. | ||
| function attemptCredentialConfigurationFromVariables(): AWS.Credentials { | ||
| const accessKey = tl.getVariable(awsAccessKeyIdVariable) | ||
| if (!accessKey) { | ||
| return undefined | ||
| } | ||
|
|
||
| const secretKey = tl.getVariable(awsSecretAccessKeyVariable) | ||
| if (!secretKey) { | ||
| throw new Error ( | ||
| 'AWS access key ID present in task variables but secret key value is missing; ' | ||
| + 'cannot configure task credentials.') | ||
| } | ||
|
|
||
| const token = tl.getVariable(awsSessionTokenVariable) | ||
|
|
||
| console.log('...configuring AWS credentials from task variables') | ||
|
|
||
| return new AWS.Credentials({ | ||
| accessKeyId: accessKey, | ||
| secretAccessKey: secretKey, | ||
| sessionToken: token | ||
| }) | ||
| } | ||
|
|
||
| // Probes for credentials to be used by the executing task. Credentials | ||
| // can be configured as a service endpoint (of type 'AWS'), or specified | ||
| // using task variables. If we don't discover credentials inside the | ||
| // Team Services environment we will assume they are set as either | ||
| // environment variables on the build host (or, if the build host is | ||
| // an EC2 instance, in instance metadata). | ||
| export async function getCredentials(awsParams: AWSConnectionParameters): Promise<AWS.Credentials> { | ||
|
|
||
| console.log('Configuring credentials for task') | ||
|
|
||
| const credentials = attemptEndpointCredentialConfiguration(awsParams, tl.getInput('awsCredentials', false)) | ||
| || attemptCredentialConfigurationFromVariables() | ||
| if (credentials) { | ||
| return credentials | ||
| } | ||
|
|
||
| // at this point user either has to have credentials in environment vars or | ||
| // ec2 instance metadata | ||
| console.log('No credentials configured.' + | ||
| 'The task will attempt to use credentials found in the build host environment.') | ||
|
|
||
| return undefined | ||
| } | ||
|
|
||
| async function queryRegionFromMetadata(): Promise<string> { | ||
| // SDK doesn't support discovery of region from EC2 instance metadata, so do it manually. We set | ||
| // a timeout so that if the build host isn't EC2, we don't hang forever | ||
| return new Promise<string>((resolve, reject) => { | ||
| const metadataService = new AWS.MetadataService() | ||
| metadataService.httpOptions.timeout = 5000 | ||
| metadataService.request('/latest/dynamic/instance-identity/document', (err, data) => { | ||
| try { | ||
| if (err) { | ||
| throw err | ||
| } | ||
|
|
||
| console.log('...received instance identity document from metadata') | ||
| const identity = JSON.parse(data) | ||
| // tslint:disable-next-line: no-unsafe-any | ||
| if (identity.region) { | ||
| // tslint:disable-next-line: no-unsafe-any | ||
| resolve(identity.region) | ||
| } else { | ||
| throw new Error('...region value not found in instance identity metadata') | ||
| } | ||
| } catch (err) { | ||
| reject(err) | ||
| } | ||
| } | ||
| ) | ||
| }) | ||
| } | ||
|
|
||
| export async function getRegion(): Promise<string> { | ||
| console.log('Configuring region for task') | ||
|
|
||
| let region = tl.getInput('regionName', false) | ||
| if (region) { | ||
| // lowercase it because the picker we know have can return mixed case | ||
| // data if the user typed in a region whose prefix (US- etc) exists | ||
| // already in the friendly text | ||
| region = region.toLowerCase() | ||
| console.log(`...configured to use region ${region}, defined in task.`) | ||
|
|
||
| return region | ||
| } | ||
|
|
||
| region = tl.getVariable(awsRegionVariable) | ||
| if (region) { | ||
| console.log(`...configured to use region ${region}, defined in task variable ${awsRegionVariable}.`) | ||
|
|
||
| return region | ||
| } | ||
|
|
||
| if (process.env.AWS_REGION) { | ||
| region = process.env.AWS_REGION | ||
| console.log(`...configured to use region ${region}, defined in environment variable.`) | ||
|
|
||
| return region | ||
| } | ||
|
|
||
| try { | ||
| region = await queryRegionFromMetadata() | ||
| console.log(`...configured to use region ${region}, from EC2 instance metadata.`) | ||
|
|
||
| return region | ||
| } catch (err) { | ||
| console.log(`...error: failed to query EC2 instance metadata for region - ${err}`) | ||
| } | ||
|
|
||
| console.log('No region specified in the task configuration or environment') | ||
|
|
||
| return undefined | ||
| } | ||
This file contains hidden or 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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.