-
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 36 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,16 @@ | ||||||
#!/bin/bash | ||||||
|
||||||
# Copy files from the @aws-cdk/asset-awscli-v1 devDependency into | ||||||
# this package, so that they will be included in the released package. | ||||||
|
||||||
# Set bash to exit the script immediately on any error (e) and if any unset (u) | ||||||
# variable is referenced. | ||||||
set -eu | ||||||
|
||||||
dir=$(node -pe 'path.resolve(require.resolve("@aws-cdk/asset-awscli-v1"), "../..")') | ||||||
|
||||||
cp $dir/layer/requirements.txt ./layer | ||||||
cp $dir/lib/layer.zip ./lib | ||||||
|
||||||
target_version=$(node -pe 'require("@aws-cdk/asset-awscli-v1/package.json").version') | ||||||
echo "export const TARGET_VERSION = '${target_version}';" > lib/asset-package-version.ts | ||||||
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.
Suggested change
? |
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 |
---|---|---|
@@ -0,0 +1 @@ | ||
export const TARGET_VERSION = '2.0.0'; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,19 +1,71 @@ | ||||||
/* eslint-disable no-console */ | ||||||
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 { Construct } from 'constructs'; | ||||||
import { TARGET_VERSION } from './asset-package-version'; | ||||||
import { installAndLoadPackage, _downloadPackage, _tryLoadPackage } from './private/package-loading-functions'; | ||||||
|
||||||
/** | ||||||
* An AWS Lambda layer that includes the AWS CLI. | ||||||
*/ | ||||||
export class AwsCliLayer extends lambda.LayerVersion { | ||||||
|
||||||
private static readonly assetPackageName: string = '@aws-cdk/asset-awscli-v1'; | ||||||
private static readonly assetPackageNpmTarPrefix: string = 'aws-cdk-asset-awscli-v1-'; | ||||||
|
||||||
constructor(scope: Construct, id: string) { | ||||||
super(scope, id, { | ||||||
code: lambda.Code.fromAsset(path.join(__dirname, 'layer.zip'), { | ||||||
const logs: string[] = []; | ||||||
let fallback = false; | ||||||
|
||||||
let assetPackage = _tryLoadPackage(AwsCliLayer.assetPackageName, TARGET_VERSION, logs); | ||||||
|
||||||
if (!assetPackage) { | ||||||
const downloadPath = _downloadPackage(AwsCliLayer.assetPackageName, AwsCliLayer.assetPackageNpmTarPrefix, TARGET_VERSION, logs); | ||||||
if (downloadPath) { | ||||||
assetPackage = installAndLoadPackage(AwsCliLayer.assetPackageName, downloadPath, logs); | ||||||
} | ||||||
} | ||||||
|
||||||
let code; | ||||||
if (assetPackage) { | ||||||
// ask for feedback here | ||||||
if (!assetPackage.AwsCliAsset) { | ||||||
logs.push(`ERROR: loaded ${AwsCliLayer.assetPackageName}, but AwsCliAsset is undefined in the module.`); | ||||||
} else { | ||||||
const asset = new assetPackage.AwsCliAsset(scope, `${id}-asset`); | ||||||
code = lambda.Code.fromBucket(asset.bucket, asset.s3ObjectKey); | ||||||
} | ||||||
} | ||||||
|
||||||
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', | ||||||
}); | ||||||
console.log(logs.join('\n')); | ||||||
|
||||||
if (debugModeEnabled()) { | ||||||
Annotations.of(this).addInfo(logs.join('\n')); | ||||||
} | ||||||
|
||||||
if (fallback) { | ||||||
Annotations.of(this).addWarning(`[ACTION REQUIRED] Your CDK application is using ${this.constructor.name}. Add a dependency on ${AwsCliLayer.assetPackageName}, or the equivalent in your language, to remove this warning. See https://github.com/aws/aws-cdk/issues/22470 for more information.`); | ||||||
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.
Suggested change
|
||||||
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 {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import * as childproc from 'child_process'; | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import * as cxapi from '@aws-cdk/cx-api'; | ||
import * as semver from 'semver'; | ||
|
||
export function _tryLoadPackage(packageName: string, targetVersion: string, logs: string[]): any { | ||
let availableVersion; | ||
let assetPackagePath; | ||
try { | ||
assetPackagePath = require.resolve(`${packageName}`); | ||
} catch (e) { | ||
logs.push(`require.resolve('${packageName}') failed`); | ||
const eAsError = e as Error; | ||
if (eAsError?.stack) { | ||
logs.push(eAsError.stack); | ||
} | ||
return; | ||
} | ||
availableVersion = requireWrapper(path.join(assetPackagePath, '../../package.json'), logs).version; | ||
|
||
if (semver.satisfies(availableVersion, targetVersion)) { | ||
logs.push(`${packageName} already installed with correct version: ${availableVersion}.`); | ||
const result = requireWrapper(packageName, logs); | ||
if (result) { | ||
logs.push(`Successfully loaded ${packageName} from pre-installed packages.`); | ||
return result; | ||
} | ||
} else { | ||
logs.push(`${packageName} already installed with incorrect version: ${availableVersion}. Target version was: ${targetVersion}.`); | ||
} | ||
} | ||
|
||
export function _downloadPackage(packageName: string, packageNpmTarPrefix: string, targetVersion: string, logs: string[]): string | undefined { | ||
const cdkHomeDir = cxapi.cdkHomeDir(); | ||
const downloadDir = path.join(cdkHomeDir, 'npm-cache'); | ||
const downloadPath = path.join(downloadDir, `${packageNpmTarPrefix}${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}`); | ||
fs.mkdirSync(downloadDir); | ||
childproc.execSync(`npm pack ${packageName}@${targetVersion} -q`, { | ||
cwd: downloadDir, | ||
}); | ||
if (fs.existsSync(downloadPath)) { | ||
logs.push('Successfully downloaded using npm pack.'); | ||
return downloadPath; | ||
} | ||
|
||
logs.push('Failed to download using npm pack.'); | ||
return undefined; | ||
} | ||
|
||
export function 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); | ||
} | ||
} | ||
} | ||
|
||
export function installAndLoadPackage(packageName: string, from: string, logs: string[]): any { | ||
const installDir = 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 requireWrapper(path.join(installDir, 'node_modules', packageName, 'lib/index.js'), logs); | ||
} | ||
|
||
export function findInstallDir(): string | undefined { | ||
if (!require.main?.paths) { | ||
return undefined; | ||
} | ||
return require.main.paths[0]; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,25 +71,32 @@ | |
"organization": true | ||
}, | ||
"license": "Apache-2.0", | ||
"bundledDependencies": [ | ||
"semver" | ||
Comment on lines
+74
to
+75
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. I'm not sure I did this correctly... Adding the non-JSII package as a dependency forced me to add it here as well. And then having a bundled dependency led me to add it to the "nohoist" list in our top-level 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. Potentially, we could add a utility function in export function versionMatchesRange(version: string, range: string) {
// Use semver here
} |
||
], | ||
"devDependencies": { | ||
"@aws-cdk/assertions": "0.0.0", | ||
"@aws-cdk/custom-resources": "0.0.0", | ||
"@aws-cdk/cdk-build-tools": "0.0.0", | ||
"@aws-cdk/integ-runner": "0.0.0", | ||
"@aws-cdk/integ-tests": "0.0.0", | ||
"@aws-cdk/pkglint": "0.0.0", | ||
"@aws-cdk/asset-awscli-v1": "2.0.0", | ||
"@types/jest": "^27.5.2", | ||
"jest": "^27.5.1" | ||
}, | ||
"dependencies": { | ||
"@aws-cdk/aws-lambda": "0.0.0", | ||
"@aws-cdk/core": "0.0.0", | ||
"constructs": "^10.0.0" | ||
"@aws-cdk/cx-api": "0.0.0", | ||
"constructs": "^10.0.0", | ||
"semver": "^7.3.8" | ||
}, | ||
"homepage": "https://github.com/aws/aws-cdk", | ||
"peerDependencies": { | ||
"@aws-cdk/aws-lambda": "0.0.0", | ||
"@aws-cdk/core": "0.0.0", | ||
"@aws-cdk/cx-api": "0.0.0", | ||
"constructs": "^10.0.0" | ||
}, | ||
"engines": { | ||
|
@@ -102,7 +109,7 @@ | |
}, | ||
"cdk-build": { | ||
"pre": [ | ||
"layer/build.sh" | ||
"layer/pre-build.sh" | ||
], | ||
"env": { | ||
"AWSLINT_BASE_CONSTRUCT": true | ||
|
@@ -120,5 +127,10 @@ | |
"publishConfig": { | ||
"tag": "latest" | ||
}, | ||
"awslint": { | ||
"exclude": [ | ||
"props-physical-name:@aws-cdk/lambda-layer-awscli.AwsCliLayerProps" | ||
] | ||
}, | ||
"private": true | ||
} |
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!