-
Notifications
You must be signed in to change notification settings - Fork 4k
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
feat(lambda-layer-awscli): Dynamically load asset for AwsCliLayer, with bundled fallback #21938
Changes from 22 commits
217a199
3819c26
6d45a8f
7e68272
c244bc8
a829e28
b01414b
dcaf2ce
5a35d5a
da1f1ba
1f856c7
f0376b4
403313a
18fc81c
9312860
3f901f1
42b84ed
f236539
0f0a89e
19b9c8b
a14b01a
7c7083f
a5e37e7
c9b8740
4e84738
6d5dd99
7657203
bda4f88
8c2edba
b2af0c6
01f194e
4b11d81
a5e35f6
7415ae8
729f9cb
9227e93
af77de9
33df916
70d9fb7
b9d5bd8
2ab6282
e97124c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import * as fs from 'fs'; | ||
import * as os from 'os'; | ||
import * as path from 'path'; | ||
|
||
/** | ||
* Return a location that will be used as the CDK home directory. | ||
* Currently the only thing that is placed here is the cache. | ||
* | ||
* First try to use the users home directory (i.e. /home/someuser/), | ||
* but if that directory does not exist for some reason create a tmp directory. | ||
* | ||
* Typically it wouldn't make sense to create a one time use tmp directory for | ||
* the purpose of creating a cache, but since this only applies to users that do | ||
* not have a home directory (some CI systems?) this should be fine. | ||
*/ | ||
export function cdkHomeDir() { | ||
const tmpDir = fs.realpathSync(os.tmpdir()); | ||
let home; | ||
try { | ||
home = path.join((os.userInfo().homedir ?? os.homedir()).trim(), '.cdk'); | ||
} catch {} | ||
return process.env.CDK_HOME | ||
? path.resolve(process.env.CDK_HOME) | ||
: home || fs.mkdtempSync(path.join(tmpDir, '.cdk')).trim(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,9 @@ | ||
const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); | ||
module.exports = baseConfig; | ||
module.exports = { | ||
...baseConfig, | ||
coverageThreshold: { | ||
global: { | ||
branches: 60, | ||
}, | ||
}, | ||
}; |
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
#!/bin/bash | ||
madeline-k marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Get the version to use from the package.json devDependencies | ||
lineWithPackageVersion=$(grep '@aws-cdk/asset-awscli-v1' ./package.json) | ||
version=$(echo $lineWithPackageVersion | cut -d '"' -f 4) | ||
|
||
echo "Downloading @aws-cdk/asset-awscli-v1@$version from npm" | ||
npm pack @aws-cdk/asset-awscli-v1@$version -q | ||
madeline-k marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
echo "Extracting layer.zip and requirements.txt from aws-cdk-asset-awscli-v1-$version.tgz" | ||
tar -zxvf aws-cdk-asset-awscli-v1-$version.tgz package/lib/layer.zip package/layer/requirements.txt | ||
|
||
echo "Moving layer.zip and requirements.txt to desired destinations" | ||
mv ./package/lib/layer.zip ./lib/ | ||
mv ./package/layer/requirements.txt ./layer | ||
|
||
echo "Cleaning up" | ||
rm aws-cdk-asset-awscli-v1-$version.tgz | ||
rm -r ./package | ||
|
||
echo "download-fallback.sh complete" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
awscli==1.25.70 | ||
awscli==1.25.46 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This diff line will go away once this PR is released cdklabs/awscdk-asset-awscli#35 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,150 @@ | ||
/* eslint-disable no-console */ | ||
import * as childproc from 'child_process'; | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import * as lambda from '@aws-cdk/aws-lambda'; | ||
import { FileSystem } from '@aws-cdk/core'; | ||
import { Annotations, FileSystem } from '@aws-cdk/core'; | ||
import { debugModeEnabled } from '@aws-cdk/core/lib/debug'; | ||
import * as cxapi from '@aws-cdk/cx-api'; | ||
import { Construct } from 'constructs'; | ||
|
||
/** | ||
* An AWS Lambda layer that includes the AWS CLI. | ||
*/ | ||
export class AwsCliLayer extends lambda.LayerVersion { | ||
/** | ||
* @internal | ||
*/ | ||
public static _tryLoadPackage(targetVersion: string, logs: string[]): any { | ||
madeline-k marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let availableVersion; | ||
let assetPackagePath; | ||
try { | ||
assetPackagePath = require.resolve(`${AwsCliLayer.assetPackageName}`); | ||
} catch (err) { | ||
logs.push(`require.resolve('${AwsCliLayer.assetPackageName}') failed`); | ||
madeline-k marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return; | ||
} | ||
availableVersion = AwsCliLayer.requireWrapper(path.join(assetPackagePath, '../../package.json'), logs).version; | ||
|
||
if (targetVersion === availableVersion) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we care exactly? Do we need There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needs to log if the version is there but we rejected it btw. That's good info to know. |
||
logs.push(`${AwsCliLayer.assetPackageName} already installed with correct version: ${availableVersion}.`); | ||
const result = AwsCliLayer.requireWrapper(AwsCliLayer.assetPackageName, logs); | ||
if (result) { | ||
logs.push(`Successfully loaded ${AwsCliLayer.assetPackageName} from pre-installed packages.`); | ||
return result; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
public static _downloadPackage(targetVersion: string, logs: string[]): string | undefined { | ||
const cdkHomeDir = cxapi.cdkHomeDir(); | ||
const downloadDir = path.join(cdkHomeDir, 'npm-cache'); | ||
const downloadPath = path.join(downloadDir, `${AwsCliLayer.assetPackageNpmTarPrefix}${targetVersion}.tgz`); | ||
|
||
if (fs.existsSync(downloadPath)) { | ||
logs.push(`Using package archive already available at location: ${downloadPath}`); | ||
return downloadPath; | ||
} | ||
logs.push(`Downloading package using npm pack to: ${downloadDir}`); | ||
childproc.execSync(`mkdir -p ${downloadDir}; cd ${downloadDir}; npm pack ${AwsCliLayer.assetPackageName}@${targetVersion} -q`); | ||
madeline-k marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (fs.existsSync(downloadPath)) { | ||
logs.push('Successfully downloaded using npm pack.'); | ||
return downloadPath; | ||
} | ||
|
||
logs.push('Failed to download using npm pack.'); | ||
return undefined; | ||
} | ||
|
||
private static readonly assetPackageName: string = '@aws-cdk/asset-awscli-v1'; | ||
private static readonly assetPackageNpmTarPrefix: string = 'aws-cdk-asset-awscli-v1-'; | ||
|
||
private static requireWrapper(id: string, logs: string[]): any { | ||
try { | ||
// eslint-disable-next-line @typescript-eslint/no-require-imports | ||
const result = require(id); | ||
if (result) { | ||
logs.push(`require('${id}') succeeded.`); | ||
} | ||
return result; | ||
} catch (e) { | ||
logs.push(`require('${id}') failed.`); | ||
const eAsError = e as Error; | ||
if (eAsError?.stack) { | ||
logs.push(eAsError.stack); | ||
} | ||
} | ||
} | ||
|
||
private static installAndLoadPackage(from: string, logs: string[]): any { | ||
const installDir = AwsCliLayer.findInstallDir(); | ||
if (!installDir) { | ||
logs.push('Unable to find an install directory. require.main.paths[0] is undefined.'); | ||
return; | ||
} | ||
logs.push(`Installing from: ${from} to: ${installDir}`); | ||
childproc.execSync(`npm install ${from} --no-save --prefix ${installDir} -q`); | ||
return AwsCliLayer.requireWrapper(path.join(installDir, 'node_modules', AwsCliLayer.assetPackageName, 'lib/index.js'), logs); | ||
} | ||
|
||
private static findInstallDir(): string | undefined { | ||
if (!require.main?.paths) { | ||
return undefined; | ||
} | ||
return require.main.paths[0]; | ||
} | ||
|
||
constructor(scope: Construct, id: string) { | ||
super(scope, id, { | ||
code: lambda.Code.fromAsset(path.join(__dirname, 'layer.zip'), { | ||
const logs: string[] = []; | ||
let fallback = false; | ||
|
||
const targetVersion = AwsCliLayer.requireWrapper(path.join(__dirname, '../package.json'), logs).devDependencies[AwsCliLayer.assetPackageName]; | ||
madeline-k marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
let assetPackage = AwsCliLayer._tryLoadPackage(targetVersion, logs); | ||
|
||
if (!assetPackage) { | ||
const downloadPath = AwsCliLayer._downloadPackage(targetVersion, logs); | ||
if (downloadPath) { | ||
assetPackage = AwsCliLayer.installAndLoadPackage(downloadPath, logs); | ||
} | ||
} | ||
|
||
let code; | ||
if (assetPackage) { | ||
// ask for feedback here | ||
const asset = new assetPackage.AwsCliAsset(scope, `${id}-asset`); | ||
code = lambda.Code.fromBucket(asset.bucket, asset.s3ObjectKey); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After successfully loading the package, is there a good way to verify that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It all happens at runtime in JavaScript, which doesn't have types. So: we can check for the presence of the symbol, but never its type. if (!(assetPackage as any).AwsCliAsset) {
throw new Error('Ruh roh');
} This is probably good enough, though we might need to stick an API version somewhere... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you! What do you mean by API version? The version of |
||
|
||
if (!code) { | ||
fallback = true; | ||
logs.push(`Unable to load ${AwsCliLayer.assetPackageName}. Falling back to use layer.zip bundled with aws-cdk-lib`); | ||
code = lambda.Code.fromAsset(path.join(__dirname, 'layer.zip'), { | ||
// we hash the layer directory (it contains the tools versions and Dockerfile) because hashing the zip is non-deterministic | ||
assetHash: FileSystem.fingerprint(path.join(__dirname, '../layer')), | ||
}), | ||
}); | ||
} | ||
|
||
super(scope, id, { | ||
code: code, | ||
description: '/opt/awscli/aws', | ||
}); | ||
|
||
if (debugModeEnabled()) { | ||
Annotations.of(this).addInfo(logs.join('\n')); | ||
} | ||
|
||
if (fallback) { | ||
Annotations.of(this).addWarning(`WARNING! ACTION REQUIRED! Your CDK application is using ${this.constructor.name} and this construct may experience a breaking change for your environment in a future release. See https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.lambda_layer_awscli-readme.html or https://github.com/aws/aws-cdk/issues/999999999 for details and resolution instructions.`); | ||
madeline-k marked this conversation as resolved.
Show resolved
Hide resolved
|
||
new Notice(this, 'cli-notice'); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* An empty construct that can be added to the tree as a marker for the CLI Notices | ||
*/ | ||
class Notice extends Construct {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Relocating this file from the CLI package (
aws-cdk
), so that it can be re-used in the CLI and the Framework. Let me know if there is a better location for this!