-
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 18 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,20 @@ | ||
#!/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 from aws-cdk-asset-awscli-v1-$version.tgz" | ||
tar -zxvf aws-cdk-asset-awscli-v1-$version.tgz package/lib/layer.zip | ||
|
||
echo "Copying layer.zip to ./layer" | ||
mv ./package/lib/layer.zip ./lib/ | ||
|
||
echo "Cleaning up" | ||
rm aws-cdk-asset-awscli-v1-$version.tgz | ||
rm -r ./package | ||
|
||
echo "download-fallback.sh complete" |
This file was deleted.
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 * as cxapi from '@aws-cdk/cx-api'; | ||
import { Construct } from 'constructs'; | ||
|
||
/** | ||
* Options for AwsCliLayer | ||
*/ | ||
export interface AwsCliLayerProps { | ||
/** | ||
* Filter out logging statements. | ||
* | ||
* @default true | ||
*/ | ||
readonly quiet?: boolean; | ||
} | ||
|
||
/** | ||
* An AWS Lambda layer that includes the AWS CLI. | ||
*/ | ||
export class AwsCliLayer extends lambda.LayerVersion { | ||
constructor(scope: Construct, id: string) { | ||
super(scope, id, { | ||
code: lambda.Code.fromAsset(path.join(__dirname, 'layer.zip'), { | ||
/** | ||
* @internal | ||
*/ | ||
public static _tryLoadPackage(targetVersion: string, log: (s: any) => void): any { | ||
let availableVersion; | ||
try { | ||
const assetPackagePath = require.resolve(`${AwsCliLayer.assetPackageName}`); | ||
availableVersion = AwsCliLayer.requireWrapper(path.join(assetPackagePath, '../../package.json'), log).version; | ||
} catch (err) { | ||
log('require.resolve error'); | ||
} | ||
|
||
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. |
||
return AwsCliLayer.requireWrapper(AwsCliLayer.assetPackageName, log); | ||
} | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
public static _downloadPackage(targetVersion: string, log: (s: string) => void): string | undefined { | ||
const cdkHomeDir = cxapi.cdkHomeDir(); | ||
const downloadDir = path.join(cdkHomeDir, 'npm-cache'); | ||
const downloadPath = path.join(downloadDir, `${AwsCliLayer.assetPackageNpmTarPrefix}${targetVersion}.tgz`); | ||
log(downloadPath); | ||
|
||
if (fs.existsSync(downloadPath)) { | ||
return downloadPath; | ||
} | ||
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)) { | ||
return downloadPath; | ||
} | ||
|
||
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, log: (s: any) => void): any { | ||
try { | ||
// eslint-disable-next-line @typescript-eslint/no-require-imports | ||
return require(id); | ||
} catch (err) { | ||
log(`require('${id}') failed`); | ||
log(err); | ||
if (err instanceof Error) { | ||
console.error(err.name, err.message.split('\n')[0]); | ||
} | ||
} | ||
} | ||
|
||
private static installAndLoadPackage(from: string, log: (s: any) => void): any { | ||
const installDir = AwsCliLayer.findInstallDir(log); | ||
if (!installDir) { | ||
return; | ||
} | ||
log(`install dir: ${installDir}`); | ||
childproc.execSync(`npm install ${from} --no-save --prefix ${installDir} -q`); | ||
return AwsCliLayer.requireWrapper(path.join(installDir, 'node_modules', AwsCliLayer.assetPackageName, 'lib/index.js'), log); | ||
} | ||
|
||
private static findInstallDir(log: (s: any) => void): string | undefined { | ||
if (!require.main?.paths) { | ||
return undefined; | ||
} | ||
for (let p of require.main.paths) { | ||
log(`p: ${p}`); | ||
if (fs.existsSync(p)) { | ||
return p; | ||
} | ||
} | ||
return undefined; | ||
} | ||
|
||
constructor(scope: Construct, id: string, props?: AwsCliLayerProps) { | ||
const quiet = props?.quiet ?? true; | ||
const log = (s: any) => { | ||
if (!quiet) { | ||
console.log(s); | ||
} | ||
}; | ||
|
||
const targetVersion = AwsCliLayer.requireWrapper(path.join(__dirname, '../package.json'), log).devDependencies[AwsCliLayer.assetPackageName]; | ||
|
||
let assetPackage; | ||
|
||
let downloadStyle = 'require'; | ||
log('trying regular require'); | ||
assetPackage = AwsCliLayer._tryLoadPackage(targetVersion, log); | ||
|
||
if (!assetPackage) { | ||
downloadStyle = 'dynamic'; | ||
log('trying to download package'); | ||
const downloadPath = AwsCliLayer._downloadPackage(targetVersion, log); | ||
if (downloadPath) { | ||
log('trying to load from install location'); | ||
assetPackage = AwsCliLayer.installAndLoadPackage(downloadPath, log); | ||
} | ||
} | ||
|
||
let code; | ||
if (assetPackage) { | ||
const asset = new assetPackage.AwsCliAsset(scope, `${id}-asset`); | ||
code = lambda.Code.fromBucket(asset.bucket, asset.s3ObjectKey); | ||
} else { | ||
downloadStyle = 'fallback'; | ||
log('using fallback to original version'); | ||
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 (downloadStyle === 'fallback') { | ||
log('we used the fallback when creating this construct, so a marker construct should be added to the tree for CLI notices'); | ||
} | ||
} | ||
} |
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!