From a0ed0f8f69d31e65af36c9b81d53bb7499432b69 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 3 Aug 2018 14:21:52 +0200 Subject: [PATCH] Assets: upload to and grant permissions on prefix We need to give asset consumers permissions on all versions of an asset, not just the latest version. Otherwise, we will never be able to do rolling updates. Also add caching on AWS client instances, so with multiple asset uploads we don't have to construct a new S3 client for every asset (incurring credential lookups for each one). This fixes #484. --- packages/@aws-cdk/assets/lib/asset.ts | 30 ++- .../integ.assets.directory.lit.expected.json | 18 +- .../test/integ.assets.file.lit.expected.json | 18 +- ...integ.assets.permissions.lit.expected.json | 18 +- .../test/integ.assets.refs.lit.expected.json | 22 +- packages/@aws-cdk/assets/test/test.asset.ts | 244 +++--------------- packages/@aws-cdk/cx-api/lib/cxapi.ts | 25 ++ packages/aws-cdk/lib/api/util/sdk.ts | 152 +++++++---- packages/aws-cdk/lib/assets.ts | 7 +- 9 files changed, 246 insertions(+), 288 deletions(-) diff --git a/packages/@aws-cdk/assets/lib/asset.ts b/packages/@aws-cdk/assets/lib/asset.ts index a3fc81ed107e3..d22cc3444d008 100644 --- a/packages/@aws-cdk/assets/lib/asset.ts +++ b/packages/@aws-cdk/assets/lib/asset.ts @@ -67,6 +67,8 @@ export class Asset extends cdk.Construct { private readonly bucket: s3.BucketRef; + private readonly s3PrefixParam: cdk.Parameter; + constructor(parent: cdk.Construct, id: string, props: GenericAssetProps) { super(parent, id); @@ -84,16 +86,19 @@ export class Asset extends cdk.Construct { description: `S3 bucket for asset "${this.path}"`, }); - const keyParam = new cdk.Parameter(this, 'S3ObjectKey', { + this.s3PrefixParam = new cdk.Parameter(this, 'S3Prefix', { + type: 'String', + description: `S3 prefix for asset "${this.path}"` + }); + + const keyParam = new cdk.Parameter(this, 'S3VersionKey', { type: 'String', - description: `S3 object for asset "${this.path}"` + description: `S3 key for asset version "${this.path}"` }); this.s3BucketName = bucketParam.value; this.s3ObjectKey = keyParam.value; - // grant the lambda's role read permissions on the code s3 object - this.bucket = s3.BucketRef.import(parent, 'AssetBucket', { bucketName: this.s3BucketName }); @@ -101,16 +106,25 @@ export class Asset extends cdk.Construct { // form the s3 URL of the object key this.s3Url = this.bucket.urlForObject(this.s3ObjectKey); + // Get a unique identifier for this asset, which will be used + // as a folder to group different versions of the same asset together. + // + // Even though this code looks horrible, this is actually not a terrible thing + // to do. The generated ID will contain the *stack name* as well, which is a + // perfectly nice thing to do to disambiguate similarly named assets in different stacks. + const uniqueId = new cdk.HashedAddressingScheme().allocateAddress(this.path.split('/')); + // attach metadata to the lambda function which includes information // for tooling to be able to package and upload a directory to the // s3 bucket and plug in the bucket name and key in the correct // parameters. - const asset: cxapi.AssetMetadataEntry = { path: this.assetPath, + id: uniqueId, packaging: props.packaging, s3BucketParameter: bucketParam.logicalId, s3KeyParameter: keyParam.logicalId, + s3PrefixParameter: this.s3PrefixParam.logicalId }; this.addMetadata(cxapi.ASSET_METADATA, asset); @@ -124,7 +138,11 @@ export class Asset extends cdk.Construct { * Grants read permissions to the principal on the asset's S3 object. */ public grantRead(principal?: iam.IPrincipal) { - this.bucket.grantRead(principal, this.s3ObjectKey); + // We give permissions on all files with the same prefix. Presumably + // different versions of the same file will have the same prefix + // and we don't want to accidentally revoke permission on old versions + // when deploying a new version. + this.bucket.grantRead(principal, new cdk.FnConcat(this.s3PrefixParam.value, "*")); } } diff --git a/packages/@aws-cdk/assets/test/integ.assets.directory.lit.expected.json b/packages/@aws-cdk/assets/test/integ.assets.directory.lit.expected.json index e3ca3797ddd0c..937a3fe810b53 100644 --- a/packages/@aws-cdk/assets/test/integ.assets.directory.lit.expected.json +++ b/packages/@aws-cdk/assets/test/integ.assets.directory.lit.expected.json @@ -4,9 +4,13 @@ "Type": "String", "Description": "S3 bucket for asset \"aws-cdk-asset-test/SampleAsset\"" }, - "SampleAssetS3ObjectKey6F5D200B": { + "SampleAssetS3Prefix6E89595F": { "Type": "String", - "Description": "S3 object for asset \"aws-cdk-asset-test/SampleAsset\"" + "Description": "S3 prefix for asset \"aws-cdk-asset-test/SampleAsset\"" + }, + "SampleAssetS3VersionKey3E106D34": { + "Type": "String", + "Description": "S3 key for asset version \"aws-cdk-asset-test/SampleAsset\"" } }, "Resources": { @@ -76,7 +80,15 @@ }, "/", { - "Ref": "SampleAssetS3ObjectKey6F5D200B" + "Fn::Join": [ + "", + [ + { + "Ref": "SampleAssetS3Prefix6E89595F" + }, + "*" + ] + ] } ] ] diff --git a/packages/@aws-cdk/assets/test/integ.assets.file.lit.expected.json b/packages/@aws-cdk/assets/test/integ.assets.file.lit.expected.json index 99d5cee84363e..013773a690b8a 100644 --- a/packages/@aws-cdk/assets/test/integ.assets.file.lit.expected.json +++ b/packages/@aws-cdk/assets/test/integ.assets.file.lit.expected.json @@ -4,9 +4,13 @@ "Type": "String", "Description": "S3 bucket for asset \"aws-cdk-asset-file-test/SampleAsset\"" }, - "SampleAssetS3ObjectKey6F5D200B": { + "SampleAssetS3Prefix6E89595F": { "Type": "String", - "Description": "S3 object for asset \"aws-cdk-asset-file-test/SampleAsset\"" + "Description": "S3 prefix for asset \"aws-cdk-asset-file-test/SampleAsset\"" + }, + "SampleAssetS3VersionKey3E106D34": { + "Type": "String", + "Description": "S3 key for asset version \"aws-cdk-asset-file-test/SampleAsset\"" } }, "Resources": { @@ -76,7 +80,15 @@ }, "/", { - "Ref": "SampleAssetS3ObjectKey6F5D200B" + "Fn::Join": [ + "", + [ + { + "Ref": "SampleAssetS3Prefix6E89595F" + }, + "*" + ] + ] } ] ] diff --git a/packages/@aws-cdk/assets/test/integ.assets.permissions.lit.expected.json b/packages/@aws-cdk/assets/test/integ.assets.permissions.lit.expected.json index e7540be27bb73..a98abf17c408e 100644 --- a/packages/@aws-cdk/assets/test/integ.assets.permissions.lit.expected.json +++ b/packages/@aws-cdk/assets/test/integ.assets.permissions.lit.expected.json @@ -4,9 +4,13 @@ "Type": "String", "Description": "S3 bucket for asset \"aws-cdk-asset-refs/MyFile\"" }, - "MyFileS3ObjectKey4641930D": { + "MyFileS3PrefixF078A585": { "Type": "String", - "Description": "S3 object for asset \"aws-cdk-asset-refs/MyFile\"" + "Description": "S3 prefix for asset \"aws-cdk-asset-refs/MyFile\"" + }, + "MyFileS3VersionKey568C3C9F": { + "Type": "String", + "Description": "S3 key for asset version \"aws-cdk-asset-refs/MyFile\"" } }, "Resources": { @@ -76,7 +80,15 @@ }, "/", { - "Ref": "MyFileS3ObjectKey4641930D" + "Fn::Join": [ + "", + [ + { + "Ref": "MyFileS3PrefixF078A585" + }, + "*" + ] + ] } ] ] diff --git a/packages/@aws-cdk/assets/test/integ.assets.refs.lit.expected.json b/packages/@aws-cdk/assets/test/integ.assets.refs.lit.expected.json index 659b218855cc3..dc8769adb0fdc 100644 --- a/packages/@aws-cdk/assets/test/integ.assets.refs.lit.expected.json +++ b/packages/@aws-cdk/assets/test/integ.assets.refs.lit.expected.json @@ -4,9 +4,13 @@ "Type": "String", "Description": "S3 bucket for asset \"aws-cdk-asset-refs/SampleAsset\"" }, - "SampleAssetS3ObjectKey6F5D200B": { + "SampleAssetS3Prefix6E89595F": { "Type": "String", - "Description": "S3 object for asset \"aws-cdk-asset-refs/SampleAsset\"" + "Description": "S3 prefix for asset \"aws-cdk-asset-refs/SampleAsset\"" + }, + "SampleAssetS3VersionKey3E106D34": { + "Type": "String", + "Description": "S3 key for asset version \"aws-cdk-asset-refs/SampleAsset\"" } }, "Outputs": { @@ -20,7 +24,7 @@ }, "S3ObjectKey": { "Value": { - "Ref": "SampleAssetS3ObjectKey6F5D200B" + "Ref": "SampleAssetS3VersionKey3E106D34" }, "Export": { "Name": "aws-cdk-asset-refs:S3ObjectKey" @@ -46,7 +50,7 @@ }, "/", { - "Ref": "SampleAssetS3ObjectKey6F5D200B" + "Ref": "SampleAssetS3VersionKey3E106D34" } ] ] @@ -123,7 +127,15 @@ }, "/", { - "Ref": "SampleAssetS3ObjectKey6F5D200B" + "Fn::Join": [ + "", + [ + { + "Ref": "SampleAssetS3Prefix6E89595F" + }, + "*" + ] + ] } ] ] diff --git a/packages/@aws-cdk/assets/test/test.asset.ts b/packages/@aws-cdk/assets/test/test.asset.ts index eee3edd0c7686..6e8319287ad7c 100644 --- a/packages/@aws-cdk/assets/test/test.asset.ts +++ b/packages/@aws-cdk/assets/test/test.asset.ts @@ -1,4 +1,4 @@ -import { expect } from '@aws-cdk/assert'; +import { expect, haveResource } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; @@ -19,24 +19,18 @@ export = { test.ok(entry, 'found metadata entry'); test.deepEqual(entry!.data, { path: dirPath, + id: 'MyAsset', packaging: 'zip', s3BucketParameter: 'MyAssetS3Bucket68C9B344', - s3KeyParameter: 'MyAssetS3ObjectKeyC07605E4' + s3KeyParameter: 'MyAssetS3VersionKey68E1A45D', + s3PrefixParameter: 'MyAssetS3Prefix5E79A15D', }); - // verify that now the template contains two parameters for this asset - expect(stack).toMatch({ - Parameters: { - MyAssetS3Bucket68C9B344: { - Type: "String", - Description: 'S3 bucket for asset "MyAsset"' - }, - MyAssetS3ObjectKeyC07605E4: { - Type: "String", - Description: 'S3 object for asset "MyAsset"' - } - } - }); + // verify that now the template contains parameters for this asset + const template = stack.toCloudFormation(); + test.equal(template.Parameters.MyAssetS3Bucket68C9B344.Type, 'String'); + test.equal(template.Parameters.MyAssetS3VersionKey68E1A45D.Type, 'String'); + test.equal(template.Parameters.MyAssetS3Prefix5E79A15D.Type, 'String'); test.done(); }, @@ -50,22 +44,17 @@ export = { test.deepEqual(entry!.data, { path: filePath, packaging: 'file', + id: 'MyAsset', s3BucketParameter: 'MyAssetS3Bucket68C9B344', - s3KeyParameter: 'MyAssetS3ObjectKeyC07605E4' + s3KeyParameter: 'MyAssetS3VersionKey68E1A45D', + s3PrefixParameter: 'MyAssetS3Prefix5E79A15D', }); - expect(stack).toMatch({ - Parameters: { - MyAssetS3Bucket68C9B344: { - Type: "String", - Description: 'S3 bucket for asset "MyAsset"' - }, - MyAssetS3ObjectKeyC07605E4: { - Type: "String", - Description: 'S3 object for asset "MyAsset"' - } - } - }); + // verify that now the template contains parameters for this asset + const template = stack.toCloudFormation(); + test.equal(template.Parameters.MyAssetS3Bucket68C9B344.Type, 'String'); + test.equal(template.Parameters.MyAssetS3VersionKey68E1A45D.Type, 'String'); + test.equal(template.Parameters.MyAssetS3Prefix5E79A15D.Type, 'String'); test.done(); }, @@ -82,188 +71,25 @@ export = { asset.grantRead(group); - expect(stack).toMatch({ - Resources: { - MyUserDC45028B: { - Type: "AWS::IAM::User" - }, - MyUserDefaultPolicy7B897426: { - Type: "AWS::IAM::Policy", - Properties: { - PolicyDocument: { - Statement: [ - { - Action: [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*" - ], - Effect: "Allow", - Resource: [ - { - "Fn::Join": [ - "", - [ - "arn", - ":", - { - Ref: "AWS::Partition" - }, - ":", - "s3", - ":", - "", - ":", - "", - ":", - { - Ref: "MyAssetS3Bucket68C9B344" - } - ] - ] - }, - { - "Fn::Join": [ - "", - [ - { - "Fn::Join": [ - "", - [ - "arn", - ":", - { - Ref: "AWS::Partition" - }, - ":", - "s3", - ":", - "", - ":", - "", - ":", - { - Ref: "MyAssetS3Bucket68C9B344" - } - ] - ] - }, - "/", - { - Ref: "MyAssetS3ObjectKeyC07605E4" - } - ] - ] - } - ] - } - ], - Version: "2012-10-17" - }, - PolicyName: "MyUserDefaultPolicy7B897426", - Users: [ - { - Ref: "MyUserDC45028B" - } + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: ["s3:GetObject*", "s3:GetBucket*", "s3:List*"], + Resource: [ + {"Fn::Join": ["", ["arn", ":", {Ref: "AWS::Partition"}, ":", "s3", ":", "", ":", "", ":", {Ref: "MyAssetS3Bucket68C9B344"}]]}, + {"Fn::Join": [ "", [ + {"Fn::Join": ["", [ "arn", ":", {Ref: "AWS::Partition"}, ":", "s3", ":", "", ":", "", ":", {Ref: "MyAssetS3Bucket68C9B344"}]]}, + "/", + {"Fn::Join": ["", [ + {Ref: "MyAssetS3Prefix5E79A15D"}, + "*" + ]]} + ]]} ] - } - }, - MyGroupCBA54B1B: { - Type: "AWS::IAM::Group" - }, - MyGroupDefaultPolicy72C41231: { - Type: "AWS::IAM::Policy", - Properties: { - Groups: [ - { - Ref: "MyGroupCBA54B1B" - } - ], - PolicyDocument: { - Statement: [ - { - Action: [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*" - ], - Effect: "Allow", - Resource: [ - { - "Fn::Join": [ - "", - [ - "arn", - ":", - { - Ref: "AWS::Partition" - }, - ":", - "s3", - ":", - "", - ":", - "", - ":", - { - Ref: "MyAssetS3Bucket68C9B344" - } - ] - ] - }, - { - "Fn::Join": [ - "", - [ - { - "Fn::Join": [ - "", - [ - "arn", - ":", - { - Ref: "AWS::Partition" - }, - ":", - "s3", - ":", - "", - ":", - "", - ":", - { - Ref: "MyAssetS3Bucket68C9B344" - } - ] - ] - }, - "/", - { - Ref: "MyAssetS3ObjectKeyC07605E4" - } - ] - ] - } - ] - } - ], - Version: "2012-10-17" - }, - PolicyName: "MyGroupDefaultPolicy72C41231" - } - } - }, - Parameters: { - MyAssetS3Bucket68C9B344: { - Type: "String", - Description: "S3 bucket for asset \"MyAsset\"" - }, - MyAssetS3ObjectKeyC07605E4: { - Type: "String", - Description: "S3 object for asset \"MyAsset\"" } - } - }); + ] + }})); test.done(); }, @@ -274,4 +100,4 @@ export = { })); test.done(); } -}; +}; \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/lib/cxapi.ts b/packages/@aws-cdk/cx-api/lib/cxapi.ts index 06b5e453caa4a..5d0df2d3168e6 100644 --- a/packages/@aws-cdk/cx-api/lib/cxapi.ts +++ b/packages/@aws-cdk/cx-api/lib/cxapi.ts @@ -109,10 +109,35 @@ export const DEFAULT_REGION_CONTEXT_KEY = 'aws:cdk:toolkit:default-region'; export const ASSET_METADATA = 'aws:cdk:asset'; export interface AssetMetadataEntry { + /** + * Path on disk to the asset + */ path: string; + + /** + * Logical identifier for the asset + */ + id: string; + + /** + * Requested packaging style + */ packaging: 'zip' | 'file'; + + /** + * Name of parameter where S3 bucket should be passed in + */ s3BucketParameter: string; + + /** + * Name of parameter where S3 key should be passed in + */ s3KeyParameter: string; + + /** + * Name of parameter where S3 folder should be passed in + */ + s3PrefixParameter: string; } /** diff --git a/packages/aws-cdk/lib/api/util/sdk.ts b/packages/aws-cdk/lib/api/util/sdk.ts index 2f12c5db467dc..8db8f270778a9 100644 --- a/packages/aws-cdk/lib/api/util/sdk.ts +++ b/packages/aws-cdk/lib/api/util/sdk.ts @@ -15,54 +15,113 @@ import { AccountAccessKeyCache } from './account-cache'; * to the requested account. */ export class SDK { - private defaultAccountFetched = false; - private defaultAccountId?: string = undefined; private readonly userAgent: string; - private readonly accountCache = new AccountAccessKeyCache(); + private readonly s3ClientCache: AWSClientCache; + private readonly cfnClientCache: AWSClientCache; + private readonly ec2ClientCache: AWSClientCache; + private readonly ssmClientCache: AWSClientCache; constructor() { // Find the package.json from the main toolkit const pkg = (require.main as any).require('../package.json'); this.userAgent = `${pkg.name}/${pkg.version}`; + + this.s3ClientCache = new AWSClientCache(S3, this.userAgent); + this.cfnClientCache = new AWSClientCache(CloudFormation, this.userAgent); + this.ec2ClientCache = new AWSClientCache(EC2, this.userAgent); } - public async cloudFormation(environment: Environment, mode: Mode): Promise { - return new CloudFormation({ - region: environment.region, - credentialProvider: await this.getCredentialProvider(environment.account, mode), - customUserAgent: this.userAgent - }); + public cloudFormation(environment: Environment, mode: Mode): Promise { + return this.cfnClientCache.get(environment.account, environment.region, mode); } - public async ec2(awsAccountId: string | undefined, region: string | undefined, mode: Mode): Promise { - return new EC2({ - region, - credentialProvider: await this.getCredentialProvider(awsAccountId, mode), - customUserAgent: this.userAgent - }); + public ec2(awsAccountId: string | undefined, region: string | undefined, mode: Mode): Promise { + return this.ec2ClientCache.get(awsAccountId, region, mode); } - public async ssm(awsAccountId: string | undefined, region: string | undefined, mode: Mode): Promise { - return new SSM({ - region, - credentialProvider: await this.getCredentialProvider(awsAccountId, mode), - customUserAgent: this.userAgent - }); + public ssm(awsAccountId: string | undefined, region: string | undefined, mode: Mode): Promise { + return this.ssmClientCache.get(awsAccountId, region, mode); } public async s3(environment: Environment, mode: Mode): Promise { - return new S3({ - region: environment.region, - credentialProvider: await this.getCredentialProvider(environment.account, mode), - customUserAgent: this.userAgent - }); + return this.s3ClientCache.get(environment.account, environment.region, mode); } public defaultRegion() { return config.region; } - public async defaultAccount() { + public defaultAccount(): Promise { + return DEFAULT_ACCOUNT.get(); + } +} + +/** + * Factory and cache for AWS clients + */ +class AWSClientCache { + private readonly cache: {[key: string]: T} = {}; + + public constructor(private readonly klass: new (u: any) => T, private readonly userAgent: string) { + } + + public async get(awsAccountId: string | undefined, region: string | undefined, mode: Mode): Promise { + const key = `${awsAccountId}-${region}-${mode}`; + if (!(key in this.cache)) { + this.cache[key] = new this.klass({ + region, + credentialProvider: await this.getCredentialProvider(awsAccountId, mode), + customUserAgent: this.userAgent + }); + } + return this.cache[key]; + } + + private async getCredentialProvider(awsAccountId: string | undefined, mode: Mode): Promise { + // If requested account is undefined or equal to default account, use default credentials provider. + const defaultAccount = await DEFAULT_ACCOUNT.get(); + if (!awsAccountId || awsAccountId === defaultAccount) { + debug(`Using default AWS SDK credentials for account ${awsAccountId}`); + return undefined; + } + + const triedSources: CredentialProviderSource[] = []; + + // Otherwise, inspect the various credential sources we have + for (const source of PluginHost.instance.credentialProviderSources) { + if (!(await source.isAvailable())) { + debug('Credentials source %s is not available, ignoring it.', source.name); + continue; + } + triedSources.push(source); + + if (!(await source.canProvideCredentials(awsAccountId))) { continue; } + debug(`Using ${source.name} credentials for account ${awsAccountId}`); + + return await source.getProvider(awsAccountId, mode); + } + + const sourceNames = ['default credentials'].concat(triedSources.map(s => s.name)).join(', '); + + throw new Error(`Need to perform AWS calls for account ${awsAccountId}, but no credentials found. Tried: ${sourceNames}.`); + } +} + +/** + * Class to retrieve the current default account + * + * This requires making an STS call using the credentials + * currently available to the AWS SDK, and is heavily cached. + */ +class DefaultAccount { + private defaultAccountFetched = false; + private defaultAccountId?: string = undefined; + private readonly accountCache = new AccountAccessKeyCache(); + + /** + * Return the default account + */ + public async get(): Promise { if (!this.defaultAccountFetched) { this.defaultAccountId = await this.lookupDefaultAccount(); this.defaultAccountFetched = true; @@ -70,7 +129,7 @@ export class SDK { return this.defaultAccountId; } - private async lookupDefaultAccount() { + private async lookupDefaultAccount(): Promise { try { debug('Resolving default credentials'); const chain = new CredentialProviderChain(); @@ -99,33 +158,12 @@ export class SDK { return undefined; } } - - private async getCredentialProvider(awsAccountId: string | undefined, mode: Mode): Promise { - // If requested account is undefined or equal to default account, use default credentials provider. - const defaultAccount = await this.defaultAccount(); - if (!awsAccountId || awsAccountId === defaultAccount) { - debug(`Using default AWS SDK credentials for account ${awsAccountId}`); - return undefined; - } - - const triedSources: CredentialProviderSource[] = []; - - // Otherwise, inspect the various credential sources we have - for (const source of PluginHost.instance.credentialProviderSources) { - if (!(await source.isAvailable())) { - debug('Credentials source %s is not available, ignoring it.', source.name); - continue; - } - triedSources.push(source); - - if (!(await source.canProvideCredentials(awsAccountId))) { continue; } - debug(`Using ${source.name} credentials for account ${awsAccountId}`); - - return await source.getProvider(awsAccountId, mode); - } - - const sourceNames = ['default credentials'].concat(triedSources.map(s => s.name)).join(', '); - - throw new Error(`Need to perform AWS calls for account ${awsAccountId}, but no credentials found. Tried: ${sourceNames}.`); - } } + +/** + * Singleton instance of DefaultAccount. + * + * Per execution there will only ever be one default account, so we might as well + * cache it process-wide. + */ +const DEFAULT_ACCOUNT = new DefaultAccount(); \ No newline at end of file diff --git a/packages/aws-cdk/lib/assets.ts b/packages/aws-cdk/lib/assets.ts index fcda72433f870..e3c3dbf9d9495 100644 --- a/packages/aws-cdk/lib/assets.ts +++ b/packages/aws-cdk/lib/assets.ts @@ -69,8 +69,10 @@ async function prepareFileAsset( const data = await fs.readFile(filePath); + const s3KeyPrefix = `assets/${asset.id}/`; + const { key, changed } = await toolkitInfo.uploadIfChanged(data, { - s3KeyPrefix: 'assets/', + s3KeyPrefix, s3KeySuffix: path.extname(filePath), contentType }); @@ -84,7 +86,8 @@ async function prepareFileAsset( return [ { ParameterKey: asset.s3BucketParameter, ParameterValue: toolkitInfo.bucketName }, - { ParameterKey: asset.s3KeyParameter, ParameterValue: key } + { ParameterKey: asset.s3KeyParameter, ParameterValue: key }, + { ParameterKey: asset.s3PrefixParameter, ParameterValue: s3KeyPrefix }, ]; }