From 34ef22c0a3da1f4ed4d5a4d22851fc1798211519 Mon Sep 17 00:00:00 2001 From: Al James Date: Mon, 1 Sep 2025 10:48:33 +0200 Subject: [PATCH 1/3] feat: adds the ability to pass options to the docker build command --- docs/guides/functions.md | 3 +- docs/guides/serverless.yml.md | 9 +++ lib/plugins/aws/provider.js | 11 +++ test/unit/lib/plugins/aws/provider.test.js | 91 ++++++++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/docs/guides/functions.md b/docs/guides/functions.md index dc1a469b87..230cb0a36b 100644 --- a/docs/guides/functions.md +++ b/docs/guides/functions.md @@ -298,11 +298,12 @@ In service configuration, images can be configured via `provider.ecr.images`. To Additionally, you can define arguments that will be passed to the `docker build` command via the following properties: - `buildArgs`: With the `buildArgs` property, you can define arguments that will be passed to `docker build` command with `--build-arg` flag. They might be later referenced via `ARG` within your `Dockerfile`. (See [Documentation](https://docs.docker.com/engine/reference/builder/#arg)) +- `buildOptions`: With the `buildOptions` property, you can define options that will be passed to the `docker build` command. (See [Documentation](https://docs.docker.com/engine/reference/commandline/image_build/#options)) - `cacheFrom`: The `cacheFrom` property can be used to specify which images to use as a source for layer caching in the `docker build` command with `--cache-from` flag. (See [Documentation](https://docs.docker.com/engine/reference/builder/#usage)) - `platform`: The `platform` property can be used to specify the architecture target in the `docker build` command with the `--platform` flag. If not specified, Docker will build for your computer's architecture by default. AWS Lambda typically uses `x86` architecture unless otherwise specified in the Lambda's runtime settings. In order to avoid runtime errors when building on an ARM-based machine (e.g. Apple M1 Mac), `linux/amd64` must be used here. The options for this flag are `linux/amd64` (`x86`-based Lambdas), `linux/arm64` (`arm`-based Lambdas), or `windows/amd64`. (See [Documentation](https://docs.docker.com/engine/reference/builder/#from)) - `provenance` Use the `provenance` property to disable multi-architecture manifest generated from BuildKit or `docker buildx`, allows the architecture specified in `platform` to be recognized by AWS Lambda during deployment. -When `uri` is defined for an image, `buildArgs`, `cacheFrom`, and `platform` cannot be defined. +When `uri` is defined for an image, `buildArgs`, `buildOptions`, `cacheFrom`, and `platform` cannot be defined. Example configuration diff --git a/docs/guides/serverless.yml.md b/docs/guides/serverless.yml.md index 76605699e8..a5eb5e1adb 100644 --- a/docs/guides/serverless.yml.md +++ b/docs/guides/serverless.yml.md @@ -390,6 +390,15 @@ provider: file: Dockerfile.dev buildArgs: STAGE: ${sls:stage} + buildOptions: + [ + '--tag', + 'v1.0.0', + '--add-host', + 'example.com:0.0.0.0', + '--ssh', + 'default=/path/to/private/key/id_rsa', + ] cacheFrom: - my-image:latest ``` diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index ae94025124..ad41ac854e 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -1144,6 +1144,7 @@ class AwsProvider { path: { type: 'string' }, file: { type: 'string' }, buildArgs: { type: 'object', additionalProperties: { type: 'string' } }, + buildOptions: {type: 'array',items: { type: 'string' } }, cacheFrom: { type: 'array', items: { type: 'string' } }, platform: { type: 'string' }, provenance: { type: 'string' }, @@ -2331,6 +2332,7 @@ Object.defineProperties( imagePath, imageFilename, buildArgs, + buildOptions, cacheFrom, platform, provenance, @@ -2377,6 +2379,7 @@ Object.defineProperties( pathToDockerfile, ...buildArgsArr, ...cacheFromArr, + ...buildOptions, imagePath, ]; @@ -2515,6 +2518,7 @@ Object.defineProperties( const { imageUri, imageName } = resolveImageUriOrName(); const defaultDockerfile = 'Dockerfile'; const defaultBuildArgs = {}; + const defaultBuildOptions = [] const defaultCacheFrom = []; const defaultScanOnPush = false; const defaultPlatform = ''; @@ -2561,6 +2565,12 @@ Object.defineProperties( 'ECR_IMAGE_BOTH_URI_AND_BUILDARGS_DEFINED_ERROR' ); } + if (imageDefinedInProvider.uri && imageDefinedInProvider.buildOptions) { + throw new ServerlessError( + `You can't use the "buildOptions" and the "uri" properties at the same time "${imageName}"`, + 'ECR_IMAGE_URI_AND_BUILDOPTIONS_DEFINED_ERROR', + ) + } if (imageDefinedInProvider.uri && imageDefinedInProvider.cacheFrom) { throw new ServerlessError( `The "cacheFrom" property cannot be used with "uri" property "${imageName}"`, @@ -2585,6 +2595,7 @@ Object.defineProperties( imagePath: imageDefinedInProvider.path, imageFilename: imageDefinedInProvider.file || defaultDockerfile, buildArgs: imageDefinedInProvider.buildArgs || defaultBuildArgs, + buildOptions: imageDefinedInProvider.buildOptions || defaultBuildOptions, cacheFrom: imageDefinedInProvider.cacheFrom || defaultCacheFrom, platform: imageDefinedInProvider.platform || defaultPlatform, provenance: imageDefinedInProvider.provenance || defaultProvenance, diff --git a/test/unit/lib/plugins/aws/provider.test.js b/test/unit/lib/plugins/aws/provider.test.js index 2477cff37f..593a9ee655 100644 --- a/test/unit/lib/plugins/aws/provider.test.js +++ b/test/unit/lib/plugins/aws/provider.test.js @@ -795,6 +795,35 @@ aws_secret_access_key = CUSTOMSECRET }); describe('when resolving images', () => { + it('should fail if `functions[].image` references image with both buildOptions and uri', async () => { + await expect( + runServerless({ + fixture: 'function', + command: 'package', + configExt: { + provider: { + ecr: { + images: { + invalidimage: { + buildOptions: ['--no-cache'], + uri: '000000000000.dkr.ecr.sa-east-1.amazonaws.com/test-lambda-docker@sha256:6bb600b4d6e1d7cf521097177dd0c4e9ea373edb91984a505333be8ac9455d38', + }, + }, + }, + }, + functions: { + fnProviderInvalidImage: { + image: 'invalidimage', + }, + }, + }, + }), + ).to.be.eventually.rejected.and.have.property( + 'code', + 'ECR_IMAGE_URI_AND_BUILDOPTIONS_DEFINED_ERROR', + ) + }) + it('should fail if `functions[].image` references image with both path and uri', async () => { await expect( runServerless({ @@ -1659,6 +1688,68 @@ aws_secret_access_key = CUSTOMSECRET ]); }); + t('should work correctly when image is defined with `buildOptions` set', async () => { + const awsRequestStubMap = { + ...baseAwsRequestStubMap, + ECR: { + ...baseAwsRequestStubMap.ECR, + describeRepositories: describeRepositoriesStub.resolves({ + repositories: [{ repositoryUri }], + }), + createRepository: createRepositoryStub, + }, + } + const { + awsNaming, + cfTemplate, + fixtureData: { servicePath: serviceDir }, + } = await runServerless({ + fixture: 'ecr', + command: 'package', + awsRequestStubMap, + modulesCacheStub, + configExt: { + provider: { + ecr: { + images: { + baseimage: { + path: './', + file: 'Dockerfile.dev', + buildOptions: ['--ssh', 'default=/path/to/file'], + }, + }, + }, + }, + }, + }) + + const functionCfLogicalId = awsNaming.getLambdaLogicalId('foo') + const functionCfConfig = + cfTemplate.Resources[functionCfLogicalId].Properties + const versionCfConfig = Object.values(cfTemplate.Resources).find( + (resource) => + resource.Type === 'AWS::Lambda::Version' && + resource.Properties.FunctionName.Ref === functionCfLogicalId, + ).Properties + + expect(functionCfConfig.Code.ImageUri).to.deep.equal( + `${repositoryUri}@sha256:${imageSha}`, + ) + expect(versionCfConfig.CodeSha256).to.equal(imageSha) + expect(describeRepositoriesStub).to.be.calledOnce + expect(createRepositoryStub.notCalled).to.be.true + expect(spawnExtStub).to.be.calledWith('docker', [ + 'build', + '-t', + `${awsNaming.getEcrRepositoryName()}:baseimage`, + '-f', + path.join(serviceDir, 'Dockerfile.dev'), + '--ssh', + 'default=/path/to/file', + './', + ]) + }); + it('should work correctly when image is defined with `buildArgs` set', async () => { const awsRequestStubMap = { ...baseAwsRequestStubMap, From a3ffc4fa5e4b1d8fe05f479bf52f81311ec3575a Mon Sep 17 00:00:00 2001 From: Al James Date: Mon, 1 Sep 2025 11:59:34 +0200 Subject: [PATCH 2/3] Fix typo --- test/unit/lib/plugins/aws/provider.test.js | 37 ++++++++++------------ 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/test/unit/lib/plugins/aws/provider.test.js b/test/unit/lib/plugins/aws/provider.test.js index 593a9ee655..1dba40617d 100644 --- a/test/unit/lib/plugins/aws/provider.test.js +++ b/test/unit/lib/plugins/aws/provider.test.js @@ -817,12 +817,12 @@ aws_secret_access_key = CUSTOMSECRET }, }, }, - }), + }) ).to.be.eventually.rejected.and.have.property( 'code', - 'ECR_IMAGE_URI_AND_BUILDOPTIONS_DEFINED_ERROR', - ) - }) + 'ECR_IMAGE_URI_AND_BUILDOPTIONS_DEFINED_ERROR' + ); + }); it('should fail if `functions[].image` references image with both path and uri', async () => { await expect( @@ -1688,7 +1688,7 @@ aws_secret_access_key = CUSTOMSECRET ]); }); - t('should work correctly when image is defined with `buildOptions` set', async () => { + it('should work correctly when image is defined with `buildOptions` set', async () => { const awsRequestStubMap = { ...baseAwsRequestStubMap, ECR: { @@ -1698,7 +1698,7 @@ aws_secret_access_key = CUSTOMSECRET }), createRepository: createRepositoryStub, }, - } + }; const { awsNaming, cfTemplate, @@ -1721,23 +1721,20 @@ aws_secret_access_key = CUSTOMSECRET }, }, }, - }) + }); - const functionCfLogicalId = awsNaming.getLambdaLogicalId('foo') - const functionCfConfig = - cfTemplate.Resources[functionCfLogicalId].Properties + const functionCfLogicalId = awsNaming.getLambdaLogicalId('foo'); + const functionCfConfig = cfTemplate.Resources[functionCfLogicalId].Properties; const versionCfConfig = Object.values(cfTemplate.Resources).find( (resource) => resource.Type === 'AWS::Lambda::Version' && - resource.Properties.FunctionName.Ref === functionCfLogicalId, - ).Properties - - expect(functionCfConfig.Code.ImageUri).to.deep.equal( - `${repositoryUri}@sha256:${imageSha}`, - ) - expect(versionCfConfig.CodeSha256).to.equal(imageSha) - expect(describeRepositoriesStub).to.be.calledOnce - expect(createRepositoryStub.notCalled).to.be.true + resource.Properties.FunctionName.Ref === functionCfLogicalId + ).Properties; + + expect(functionCfConfig.Code.ImageUri).to.deep.equal(`${repositoryUri}@sha256:${imageSha}`); + expect(versionCfConfig.CodeSha256).to.equal(imageSha); + expect(describeRepositoriesStub).to.be.calledOnce; + expect(createRepositoryStub.notCalled).to.be.true; expect(spawnExtStub).to.be.calledWith('docker', [ 'build', '-t', @@ -1747,7 +1744,7 @@ aws_secret_access_key = CUSTOMSECRET '--ssh', 'default=/path/to/file', './', - ]) + ]); }); it('should work correctly when image is defined with `buildArgs` set', async () => { From 1119177bcac4b94549876e205670a526c69530c4 Mon Sep 17 00:00:00 2001 From: Al James Date: Mon, 1 Sep 2025 12:08:41 +0200 Subject: [PATCH 3/3] Fix missing default options --- lib/plugins/aws/provider.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index ad41ac854e..e182e28bef 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -1144,7 +1144,7 @@ class AwsProvider { path: { type: 'string' }, file: { type: 'string' }, buildArgs: { type: 'object', additionalProperties: { type: 'string' } }, - buildOptions: {type: 'array',items: { type: 'string' } }, + buildOptions: { type: 'array', items: { type: 'string' } }, cacheFrom: { type: 'array', items: { type: 'string' } }, platform: { type: 'string' }, provenance: { type: 'string' }, @@ -2518,7 +2518,7 @@ Object.defineProperties( const { imageUri, imageName } = resolveImageUriOrName(); const defaultDockerfile = 'Dockerfile'; const defaultBuildArgs = {}; - const defaultBuildOptions = [] + const defaultBuildOptions = []; const defaultCacheFrom = []; const defaultScanOnPush = false; const defaultPlatform = ''; @@ -2568,8 +2568,8 @@ Object.defineProperties( if (imageDefinedInProvider.uri && imageDefinedInProvider.buildOptions) { throw new ServerlessError( `You can't use the "buildOptions" and the "uri" properties at the same time "${imageName}"`, - 'ECR_IMAGE_URI_AND_BUILDOPTIONS_DEFINED_ERROR', - ) + 'ECR_IMAGE_URI_AND_BUILDOPTIONS_DEFINED_ERROR' + ); } if (imageDefinedInProvider.uri && imageDefinedInProvider.cacheFrom) { throw new ServerlessError( @@ -2612,6 +2612,7 @@ Object.defineProperties( imagePath: imageDefinedInProvider, imageFilename: defaultDockerfile, buildArgs: imageDefinedInProvider.buildArgs || defaultBuildArgs, + buildOptions: imageDefinedInProvider.buildOptions || defaultBuildOptions, cacheFrom: imageDefinedInProvider.cacheFrom || defaultCacheFrom, platform: imageDefinedInProvider.platform || defaultPlatform, provenance: imageDefinedInProvider.provenance || defaultProvenance,