mirror of
https://github.com/serverless/serverless.git
synced 2026-01-18 14:58:43 +00:00
feat(AWS Lambda): Add support for function URLs
This commit is contained in:
parent
26f79aaf19
commit
d215517cf0
@ -200,6 +200,91 @@ provider:
|
||||
|
||||
See the documentation about [IAM](./iam.md) for function level IAM roles.
|
||||
|
||||
## Lambda Function URLs
|
||||
|
||||
A [Lambda Function URL](https://docs.aws.amazon.com/lambda/latest/dg/configuration-function-urls.html) is a simple solution to create HTTP endpoints with AWS Lambda. Function URLs are ideal for getting started with AWS Lambda, or for single-function applications like webhooks or APIs built with web frameworks.
|
||||
|
||||
You can create a function URL via the `url` property in the function configuration in `serverless.yml`. By setting `url` to `true`, as shown below, the URL will be public without CORS configuration.
|
||||
|
||||
```yaml
|
||||
functions:
|
||||
func:
|
||||
handler: index.handler
|
||||
url: true
|
||||
```
|
||||
|
||||
Alternatively, you can configure it as an object with the `authorizer` and/or `cors` properties. The `authorizer` property can be set to `aws_iam` to enable AWS IAM authorization on your function URL.
|
||||
|
||||
```yaml
|
||||
functions:
|
||||
func:
|
||||
handler: index.handler
|
||||
url:
|
||||
authorizer: aws_iam
|
||||
```
|
||||
|
||||
When using IAM authorization, the URL will only accept HTTP requests with AWS credentials allowing `lambda:InvokeFunctionUrl` (similar to [API Gateway IAM authentication](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-access-control-iam.html)).
|
||||
|
||||
You can also configure [CORS headers](https://developer.mozilla.org/docs/Web/HTTP/CORS) so that your function URL can be called from other domains in browsers. Setting `cors` to `true` will allow all domains via the following CORS headers:
|
||||
|
||||
```yaml
|
||||
functions:
|
||||
func:
|
||||
handler: index.handler
|
||||
url:
|
||||
cors: true
|
||||
```
|
||||
|
||||
| Header | Value |
|
||||
| :--------------------------- | :----------------------------------------------------------------------- |
|
||||
| Access-Control-Allow-Origin | \* |
|
||||
| Access-Control-Allow-Headers | Content-Type, X-Amz-Date, Authorization, X-Api-Key, X-Amz-Security-Token |
|
||||
| Access-Control-Allow-Methods | \* |
|
||||
|
||||
You can also additionally adjust your CORS configuration by setting `allowedOrigins`, `allowedHeaders`, `allowedMethods`, `allowCredentials`, `exposedResponseHeaders`, and `maxAge` properties as shown in example below.
|
||||
|
||||
```yaml
|
||||
functions:
|
||||
func:
|
||||
handler: index.handler
|
||||
url:
|
||||
cors:
|
||||
allowedOrigins:
|
||||
- https://url1.com
|
||||
- https://url2.com
|
||||
allowedHeaders:
|
||||
- Content-Type
|
||||
- Authorization
|
||||
allowedMethods:
|
||||
- GET
|
||||
allowCredentials: true
|
||||
exposedResponseHeaders:
|
||||
- Special-Response-Header
|
||||
maxAge: 6000 # In seconds
|
||||
```
|
||||
|
||||
In the table below you can find how the `cors` properties map to CORS headers
|
||||
|
||||
| Configuration property | CORS Header |
|
||||
| :--------------------- | :------------------------------- |
|
||||
| allowedOrigins | Access-Control-Allow-Origin |
|
||||
| allowedHeaders | Access-Control-Allow-Headers |
|
||||
| allowedMethods | Access-Control-Allow-Methods |
|
||||
| allowCredentials | Access-Control-Allow-Credentials |
|
||||
| exposedResponseHeaders | Access-Control-Expose-Headers |
|
||||
| maxAge | Access-Control-Max-Age |
|
||||
|
||||
It is also possible to remove the values in CORS configuration that are set by default by setting them to `null` instead.
|
||||
|
||||
```yaml
|
||||
functions:
|
||||
func:
|
||||
handler: index.handler
|
||||
url:
|
||||
cors:
|
||||
allowedHeaders: null
|
||||
```
|
||||
|
||||
## Referencing container image as a target
|
||||
|
||||
Alternatively lambda environment can be configured through docker images. Image published to AWS ECR registry can be referenced as lambda source (check [AWS Lambda – Container Image Support](https://aws.amazon.com/blogs/aws/new-for-aws-lambda-container-image-support/)). In addition, you can also define your own images that will be built locally and uploaded to AWS ECR registry.
|
||||
|
||||
@ -645,6 +645,21 @@ functions:
|
||||
subnetIds:
|
||||
- subnetId1
|
||||
- subnetId2
|
||||
# Lambda URL definition for this function, optional
|
||||
# Can be defined as `true` which will create URL without authorizer and cors settings
|
||||
url:
|
||||
authorizer: 'aws_iam' # Authorizer used for calls to Lambda URL
|
||||
cors: # CORS configuration for Lambda URL, can also be defined as `true` with default CORS configuration
|
||||
allowedOrigins:
|
||||
- *
|
||||
allowedHeaders:
|
||||
- Authorization
|
||||
allowedMethods:
|
||||
- GET
|
||||
allowCredentials: true
|
||||
exposedResponseHeaders:
|
||||
- SomeHeader
|
||||
maxAge: 3600
|
||||
# Packaging rules specific to this function
|
||||
package:
|
||||
# Directories and files to include in the deployed package
|
||||
|
||||
@ -88,6 +88,17 @@ module.exports = {
|
||||
outputSectionItems.push(`CloudFront - ${info.cloudFront}`);
|
||||
}
|
||||
|
||||
const functionsWithUrls = this.gatheredData.info.functions.filter((fn) => fn.url);
|
||||
|
||||
for (const functionWithUrl of functionsWithUrls) {
|
||||
if (outputSectionItems.length === 0 && functionsWithUrls.length === 1) {
|
||||
// In this situation we want to skip displaying function name as there is only one URL in whole service
|
||||
outputSectionItems.push(functionWithUrl.url);
|
||||
} else {
|
||||
outputSectionItems.push(`${functionWithUrl.name}: ${functionWithUrl.url}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (outputSectionItems.length > 1) {
|
||||
this.serverless.serviceOutputs.set('endpoints', outputSectionItems);
|
||||
} else if (outputSectionItems.length) {
|
||||
|
||||
@ -75,6 +75,13 @@ module.exports = {
|
||||
functionInfo.name = func;
|
||||
functionInfo.deployedName = functionObj.name;
|
||||
functionInfo.artifactSize = functionObj.artifactSize;
|
||||
const functionUrlOutput = outputs.find(
|
||||
(output) =>
|
||||
output.OutputKey === this.provider.naming.getLambdaFunctionUrlOutputLogicalId(func)
|
||||
);
|
||||
if (functionUrlOutput) {
|
||||
functionInfo.url = functionUrlOutput.OutputValue;
|
||||
}
|
||||
this.gatheredData.info.functions.push(functionInfo);
|
||||
});
|
||||
|
||||
|
||||
@ -150,6 +150,9 @@ module.exports = {
|
||||
getLambdaLogicalId(functionName) {
|
||||
return `${this.getNormalizedFunctionName(functionName)}LambdaFunction`;
|
||||
},
|
||||
getLambdaFunctionUrlLogicalId(functionName) {
|
||||
return `${this.getNormalizedFunctionName(functionName)}LambdaFunctionUrl`;
|
||||
},
|
||||
getLambdaEventConfigLogicalId(functionName) {
|
||||
return `${this.getNormalizedFunctionName(functionName)}LambdaEvConf`;
|
||||
},
|
||||
@ -186,6 +189,9 @@ module.exports = {
|
||||
getLambdaProvisionedConcurrencyAliasName() {
|
||||
return 'provisioned';
|
||||
},
|
||||
getLambdaFunctionUrlOutputLogicalId(functionName) {
|
||||
return `${this.getNormalizedFunctionName(functionName)}LambdaFunctionUrl`;
|
||||
},
|
||||
getLambdaVersionOutputLogicalId(functionName) {
|
||||
return `${this.getLambdaLogicalId(functionName)}QualifiedArn`;
|
||||
},
|
||||
@ -516,6 +522,9 @@ module.exports = {
|
||||
getLambdaHttpApiPermissionLogicalId(functionName) {
|
||||
return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionHttpApi`;
|
||||
},
|
||||
getLambdaFnUrlPermissionLogicalId(functionName) {
|
||||
return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionFnUrl`;
|
||||
},
|
||||
getLambdaAuthorizerHttpApiPermissionLogicalId(authorizerName) {
|
||||
return `${this.getNormalizedResourceName(authorizerName)}LambdaAuthorizerPermissionHttpApi`;
|
||||
},
|
||||
|
||||
@ -9,9 +9,22 @@ const path = require('path');
|
||||
const ServerlessError = require('../../../../serverless-error');
|
||||
const deepSortObjectByKey = require('../../../../utils/deep-sort-object-by-key');
|
||||
const getHashForFilePath = require('../lib/get-hash-for-file-path');
|
||||
const resolveLambdaTarget = require('../../utils/resolve-lambda-target');
|
||||
const parseS3URI = require('../../utils/parse-s3-uri');
|
||||
const { log } = require('@serverless/utils/log');
|
||||
|
||||
const defaultCors = {
|
||||
allowedOrigins: ['*'],
|
||||
allowedHeaders: [
|
||||
'Content-Type',
|
||||
'X-Amz-Date',
|
||||
'Authorization',
|
||||
'X-Api-Key',
|
||||
'X-Amz-Security-Token',
|
||||
],
|
||||
allowedMethods: ['*'],
|
||||
};
|
||||
|
||||
class AwsCompileFunctions {
|
||||
constructor(serverless, options) {
|
||||
this.serverless = serverless;
|
||||
@ -593,9 +606,94 @@ class AwsCompileFunctions {
|
||||
}
|
||||
}
|
||||
|
||||
this.compileFunctionUrl(functionName);
|
||||
this.compileFunctionEventInvokeConfig(functionName);
|
||||
}
|
||||
|
||||
compileFunctionUrl(functionName) {
|
||||
const functionObject = this.serverless.service.getFunction(functionName);
|
||||
const cfTemplate = this.serverless.service.provider.compiledCloudFormationTemplate;
|
||||
const { url } = functionObject;
|
||||
|
||||
if (!url) return;
|
||||
|
||||
let auth = 'NONE';
|
||||
let cors = null;
|
||||
if (url.authorizer === 'aws_iam') {
|
||||
auth = 'AWS_IAM';
|
||||
}
|
||||
|
||||
if (url.cors) {
|
||||
cors = Object.assign({}, defaultCors);
|
||||
|
||||
if (url.cors.allowedOrigins) {
|
||||
cors.allowedOrigins = _.uniq(url.cors.allowedOrigins);
|
||||
} else if (url.cors.allowedOrigins === null) {
|
||||
delete cors.allowedOrigins;
|
||||
}
|
||||
|
||||
if (url.cors.allowedHeaders) {
|
||||
cors.allowedHeaders = _.uniq(url.cors.allowedHeaders);
|
||||
} else if (url.cors.allowedHeaders === null) {
|
||||
delete cors.allowedHeaders;
|
||||
}
|
||||
|
||||
if (url.cors.allowedMethods) {
|
||||
cors.allowedMethods = _.uniq(url.cors.allowedMethods);
|
||||
} else if (url.cors.allowedMethods === null) {
|
||||
delete cors.allowedMethods;
|
||||
}
|
||||
|
||||
if (url.cors.allowCredentials) cors.allowCredentials = true;
|
||||
|
||||
if (url.cors.exposedResponseHeaders) {
|
||||
cors.exposedResponseHeaders = _.uniq(url.cors.exposedResponseHeaders);
|
||||
}
|
||||
|
||||
cors.maxAge = url.cors.maxAge;
|
||||
}
|
||||
|
||||
const urlResource = {
|
||||
Type: 'AWS::Lambda::Url',
|
||||
Properties: {
|
||||
AuthType: auth,
|
||||
TargetFunctionArn: resolveLambdaTarget(functionName, functionObject),
|
||||
},
|
||||
};
|
||||
|
||||
if (cors) {
|
||||
urlResource.Properties.Cors = {
|
||||
AllowCredentials: cors.allowCredentials,
|
||||
AllowHeaders: cors.allowedHeaders && Array.from(cors.allowedHeaders),
|
||||
AllowMethods: cors.allowedMethods && Array.from(cors.allowedMethods),
|
||||
AllowOrigins: cors.allowedOrigins && Array.from(cors.allowedOrigins),
|
||||
ExposeHeaders: cors.exposedResponseHeaders && Array.from(cors.exposedResponseHeaders),
|
||||
MaxAge: cors.maxAge,
|
||||
};
|
||||
}
|
||||
|
||||
const logicalId = this.provider.naming.getLambdaFunctionUrlLogicalId(functionName);
|
||||
cfTemplate.Resources[logicalId] = urlResource;
|
||||
cfTemplate.Outputs[this.provider.naming.getLambdaFunctionUrlOutputLogicalId(functionName)] = {
|
||||
Description: 'Lambda Function URL',
|
||||
Value: {
|
||||
'Fn::GetAtt': [logicalId, 'FunctionUrl'],
|
||||
},
|
||||
};
|
||||
|
||||
if (auth === 'NONE') {
|
||||
cfTemplate.Resources[this.provider.naming.getLambdaFnUrlPermissionLogicalId(functionName)] = {
|
||||
Type: 'AWS::Lambda::Permission',
|
||||
Properties: {
|
||||
FunctionName: resolveLambdaTarget(functionName, functionObject),
|
||||
Action: 'lambda:InvokeFunctionUrl',
|
||||
Principal: '*',
|
||||
FunctionUrlAuthType: auth,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
compileFunctionEventInvokeConfig(functionName) {
|
||||
const functionObject = this.serverless.service.getFunction(functionName);
|
||||
const { destinations, maximumEventAge, maximumRetryAttempts } = functionObject;
|
||||
|
||||
@ -1332,6 +1332,55 @@ class AwsProvider {
|
||||
tags: { $ref: '#/definitions/awsResourceTags' },
|
||||
timeout: { $ref: '#/definitions/awsLambdaTimeout' },
|
||||
tracing: { $ref: '#/definitions/awsLambdaTracing' },
|
||||
url: {
|
||||
anyOf: [
|
||||
{ type: 'boolean' },
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
authorizer: { type: 'string', enum: ['aws_iam'] },
|
||||
cors: {
|
||||
anyOf: [
|
||||
{ type: 'boolean' },
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowCredentials: { type: 'boolean' },
|
||||
allowedHeaders: {
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
maxItems: 100,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
allowedMethods: {
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
maxItems: 6,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
allowedOrigins: {
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
maxItems: 100,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
exposedResponseHeaders: {
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
maxItems: 100,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
maxAge: { type: 'integer', minimum: 0 },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
versionFunction: { $ref: '#/definitions/awsLambdaVersioning' },
|
||||
vpc: { $ref: '#/definitions/awsLambdaVpcConfig' },
|
||||
httpApi: {
|
||||
|
||||
@ -99,6 +99,7 @@ const getServiceConfig = ({ configuration, options, variableSources }) => {
|
||||
})();
|
||||
|
||||
return {
|
||||
url: Boolean(functionConfig.url),
|
||||
runtime: functionRuntime,
|
||||
events: functionEvents.map((eventConfig) => ({
|
||||
type: isObject(eventConfig) ? Object.keys(eventConfig)[0] || null : null,
|
||||
|
||||
@ -81,6 +81,7 @@
|
||||
"@serverless/eslint-config": "^4.0.0",
|
||||
"@serverless/test": "^10.0.2",
|
||||
"adm-zip": "^0.5.9",
|
||||
"aws4": "^1.11.0",
|
||||
"chai": "^4.3.6",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"cos-nodejs-sdk-v5": "^2.11.6",
|
||||
|
||||
113
test/integration/aws/function-url.test.js
Normal file
113
test/integration/aws/function-url.test.js
Normal file
@ -0,0 +1,113 @@
|
||||
'use strict';
|
||||
|
||||
const { expect } = require('chai');
|
||||
const awsRequest = require('@serverless/test/aws-request');
|
||||
const CloudFormationService = require('aws-sdk').CloudFormation;
|
||||
const fixtures = require('../../fixtures/programmatic');
|
||||
const aws4 = require('aws4');
|
||||
const url = require('url');
|
||||
|
||||
const { deployService, removeService, fetch } = require('../../utils/integration');
|
||||
|
||||
describe('test/integration/aws/function-url.test.js', function () {
|
||||
this.timeout(1000 * 60 * 10); // Involves time-taking deploys
|
||||
let stackName;
|
||||
let serviceDir;
|
||||
let basicEndpoint;
|
||||
let otherEndpoint;
|
||||
let authedEndpoint;
|
||||
const stage = 'dev';
|
||||
|
||||
before(async () => {
|
||||
const serviceData = await fixtures.setup('function', {
|
||||
configExt: {
|
||||
functions: {
|
||||
basic: {
|
||||
url: true,
|
||||
},
|
||||
other: {
|
||||
url: {
|
||||
cors: {
|
||||
exposedResponseHeaders: ['x-foo'],
|
||||
allowCredentials: true,
|
||||
allowedMethods: ['GET'],
|
||||
},
|
||||
},
|
||||
},
|
||||
authed: {
|
||||
handler: 'basic.handler',
|
||||
url: {
|
||||
authorizer: 'aws_iam',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
({ servicePath: serviceDir } = serviceData);
|
||||
const serviceName = serviceData.serviceConfig.service;
|
||||
stackName = `${serviceName}-${stage}`;
|
||||
await deployService(serviceDir);
|
||||
const describeStacksResponse = await awsRequest(CloudFormationService, 'describeStacks', {
|
||||
StackName: stackName,
|
||||
});
|
||||
basicEndpoint = describeStacksResponse.Stacks[0].Outputs.find(
|
||||
(output) => output.OutputKey === 'BasicLambdaFunctionUrl'
|
||||
).OutputValue;
|
||||
otherEndpoint = describeStacksResponse.Stacks[0].Outputs.find(
|
||||
(output) => output.OutputKey === 'OtherLambdaFunctionUrl'
|
||||
).OutputValue;
|
||||
authedEndpoint = describeStacksResponse.Stacks[0].Outputs.find(
|
||||
(output) => output.OutputKey === 'AuthedLambdaFunctionUrl'
|
||||
).OutputValue;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
if (!serviceDir) return;
|
||||
await removeService(serviceDir);
|
||||
});
|
||||
|
||||
it('should return valid response from Lambda URL', async () => {
|
||||
const expectedMessage = 'Basic';
|
||||
|
||||
const response = await fetch(basicEndpoint, { method: 'GET' });
|
||||
const jsonResponse = await response.json();
|
||||
expect(jsonResponse.message).to.equal(expectedMessage);
|
||||
});
|
||||
|
||||
it('should return valid response from Lambda URL with authorizer with valid signature', async () => {
|
||||
const expectedMessage = 'Basic';
|
||||
const signedParams = aws4.sign(
|
||||
{
|
||||
service: 'lambda',
|
||||
region: 'us-east-1',
|
||||
method: 'GET',
|
||||
host: url.parse(authedEndpoint).hostname,
|
||||
},
|
||||
{
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||
}
|
||||
);
|
||||
|
||||
const response = await fetch(authedEndpoint, signedParams);
|
||||
const jsonResponse = await response.json();
|
||||
expect(jsonResponse.message).to.equal(expectedMessage);
|
||||
});
|
||||
|
||||
it('should return invalid response from Lambda URL with authorizer without passed signature', async () => {
|
||||
const expectedMessage = 'Forbidden';
|
||||
const response = await fetch(authedEndpoint, { method: 'GET' });
|
||||
const jsonResponse = await response.json();
|
||||
expect(jsonResponse.Message).to.equal(expectedMessage);
|
||||
});
|
||||
|
||||
it('should return expected CORS headers from Lambda URL', async () => {
|
||||
const response = await fetch(otherEndpoint, {
|
||||
method: 'GET',
|
||||
headers: { Origin: 'https://serverless.com' },
|
||||
});
|
||||
const headers = response.headers;
|
||||
expect(headers.get('access-control-expose-headers')).to.equal('x-foo');
|
||||
expect(headers.get('access-control-allow-credentials')).to.equal('true');
|
||||
});
|
||||
});
|
||||
@ -43,6 +43,10 @@ describe('test/unit/lib/plugins/aws/info/display.test.js', () => {
|
||||
OutputKey: 'LayerLambdaLayerQualifiedArn',
|
||||
OutputValue: 'arn:aws:lambda:us-east-1:00000000:layer:layer:1',
|
||||
},
|
||||
{
|
||||
OutputKey: 'WithUrlLambdaFunctionUrl',
|
||||
OutputValue: 'https://sub.lambda.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@ -68,6 +72,12 @@ describe('test/unit/lib/plugins/aws/info/display.test.js', () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
functions: {
|
||||
withUrl: {
|
||||
handler: 'index.handler',
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
layers: {
|
||||
layer: {
|
||||
path: 'layer',
|
||||
@ -90,6 +100,7 @@ describe('test/unit/lib/plugins/aws/info/display.test.js', () => {
|
||||
'GET - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/foo',
|
||||
'POST - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/some-post',
|
||||
'GET - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/bar/{marko}',
|
||||
'withUrl: https://sub.lambda.com',
|
||||
]);
|
||||
});
|
||||
it('should register functions section', () => {
|
||||
@ -97,6 +108,7 @@ describe('test/unit/lib/plugins/aws/info/display.test.js', () => {
|
||||
`minimal: ${serviceName}-dev-minimal`,
|
||||
`foo: ${serviceName}-dev-foo`,
|
||||
`other: ${serviceName}-dev-other`,
|
||||
`withUrl: ${serviceName}-dev-withUrl`,
|
||||
]);
|
||||
});
|
||||
it('should register layers section', () => {
|
||||
|
||||
@ -1044,6 +1044,14 @@ describe('#naming()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getLambdaFnUrlPermissionLogicalId()', () => {
|
||||
it('should normalize the name and append correct suffix', () => {
|
||||
expect(sdk.naming.getLambdaFnUrlPermissionLogicalId('fnName')).to.equal(
|
||||
'FnNameLambdaPermissionFnUrl'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getHttpApiName()', () => {
|
||||
it('should return the composition of service & stage name if custom name not provided and shouldStartNameWithService is true', () => {
|
||||
serverless.service.service = 'myService';
|
||||
|
||||
@ -1577,6 +1577,7 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => {
|
||||
|
||||
describe('Function properties', () => {
|
||||
let cfResources;
|
||||
let cfOutputs;
|
||||
let naming;
|
||||
let serverless;
|
||||
let serviceConfig;
|
||||
@ -1689,6 +1690,28 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => {
|
||||
handler: 'index.handler',
|
||||
ephemeralStorageSize: 1024,
|
||||
},
|
||||
fnUrl: {
|
||||
handler: 'target.handler',
|
||||
url: true,
|
||||
},
|
||||
fnUrlWithAuthAndCors: {
|
||||
handler: 'target.handler',
|
||||
url: {
|
||||
authorizer: 'aws_iam',
|
||||
cors: {
|
||||
maxAge: 3600,
|
||||
},
|
||||
},
|
||||
},
|
||||
fnUrlNullifyDefaultCorsValue: {
|
||||
handler: 'target.handler',
|
||||
url: {
|
||||
authorizer: 'aws_iam',
|
||||
cors: {
|
||||
allowedHeaders: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resources: {
|
||||
Resources: {
|
||||
@ -1708,6 +1731,7 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => {
|
||||
},
|
||||
});
|
||||
cfResources = cfTemplate.Resources;
|
||||
cfOutputs = cfTemplate.Outputs;
|
||||
naming = awsNaming;
|
||||
serverless = serverlessInstance;
|
||||
serviceConfig = fixtureData.serviceConfig;
|
||||
@ -1888,6 +1912,89 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => {
|
||||
// https://github.com/serverless/serverless/blob/d8527d8b57e7e5f0b94ba704d9f53adb34298d99/lib/plugins/aws/package/compile/functions/index.test.js#L2381-L2397
|
||||
});
|
||||
|
||||
it('should support `functions[].url` set to `true`', () => {
|
||||
expect(cfResources[naming.getLambdaFunctionUrlLogicalId('fnUrl')].Properties).to.deep.equal({
|
||||
AuthType: 'NONE',
|
||||
TargetFunctionArn: {
|
||||
'Fn::GetAtt': [naming.getLambdaLogicalId('fnUrl'), 'Arn'],
|
||||
},
|
||||
});
|
||||
expect(cfOutputs[naming.getLambdaFunctionUrlLogicalId('fnUrl')].Value).to.deep.equal({
|
||||
'Fn::GetAtt': [naming.getLambdaFunctionUrlOutputLogicalId('fnUrl'), 'FunctionUrl'],
|
||||
});
|
||||
|
||||
expect(
|
||||
cfResources[naming.getLambdaFnUrlPermissionLogicalId('fnUrl')].Properties
|
||||
).to.deep.equal({
|
||||
Action: 'lambda:InvokeFunctionUrl',
|
||||
FunctionName: {
|
||||
'Fn::GetAtt': ['FnUrlLambdaFunction', 'Arn'],
|
||||
},
|
||||
FunctionUrlAuthType: 'NONE',
|
||||
Principal: '*',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support `functions[].url` set to an object with authorizer and cors', () => {
|
||||
expect(
|
||||
cfResources[naming.getLambdaFunctionUrlLogicalId('fnUrlWithAuthAndCors')].Properties
|
||||
).to.deep.equal({
|
||||
AuthType: 'AWS_IAM',
|
||||
TargetFunctionArn: {
|
||||
'Fn::GetAtt': [naming.getLambdaLogicalId('fnUrlWithAuthAndCors'), 'Arn'],
|
||||
},
|
||||
Cors: {
|
||||
AllowMethods: ['*'],
|
||||
AllowOrigins: ['*'],
|
||||
AllowHeaders: [
|
||||
'Content-Type',
|
||||
'X-Amz-Date',
|
||||
'Authorization',
|
||||
'X-Api-Key',
|
||||
'X-Amz-Security-Token',
|
||||
],
|
||||
MaxAge: 3600,
|
||||
AllowCredentials: undefined,
|
||||
ExposeHeaders: undefined,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
cfOutputs[naming.getLambdaFunctionUrlLogicalId('fnUrlWithAuthAndCors')].Value
|
||||
).to.deep.equal({
|
||||
'Fn::GetAtt': [
|
||||
naming.getLambdaFunctionUrlOutputLogicalId('fnUrlWithAuthAndCors'),
|
||||
'FunctionUrl',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should support nullifying default cors value with `null` for `functions[].url`', () => {
|
||||
expect(
|
||||
cfResources[naming.getLambdaFunctionUrlLogicalId('fnUrlNullifyDefaultCorsValue')].Properties
|
||||
).to.deep.equal({
|
||||
AuthType: 'AWS_IAM',
|
||||
TargetFunctionArn: {
|
||||
'Fn::GetAtt': [naming.getLambdaLogicalId('fnUrlNullifyDefaultCorsValue'), 'Arn'],
|
||||
},
|
||||
Cors: {
|
||||
AllowMethods: ['*'],
|
||||
AllowOrigins: ['*'],
|
||||
AllowHeaders: undefined,
|
||||
MaxAge: undefined,
|
||||
AllowCredentials: undefined,
|
||||
ExposeHeaders: undefined,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
cfOutputs[naming.getLambdaFunctionUrlLogicalId('fnUrlNullifyDefaultCorsValue')].Value
|
||||
).to.deep.equal({
|
||||
'Fn::GetAtt': [
|
||||
naming.getLambdaFunctionUrlOutputLogicalId('fnUrlNullifyDefaultCorsValue'),
|
||||
'FunctionUrl',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should support `functions[].architecture`', () => {
|
||||
expect(
|
||||
cfResources[naming.getLambdaLogicalId('fnArch')].Properties.Architectures
|
||||
|
||||
@ -53,6 +53,10 @@ describe('test/unit/lib/utils/telemetry/generatePayload.test.js', () => {
|
||||
image:
|
||||
'000000000000.dkr.ecr.sa-east-1.amazonaws.com/test-lambda-docker@sha256:6bb600b4d6e1d7cf521097177dd0c4e9ea373edb91984a505333be8ac9455d38',
|
||||
},
|
||||
withUrl: {
|
||||
handler: 'index.handler',
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
resources: {
|
||||
Resources: {
|
||||
@ -148,11 +152,12 @@ describe('test/unit/lib/utils/telemetry/generatePayload.test.js', () => {
|
||||
},
|
||||
plugins: [],
|
||||
functions: [
|
||||
{ runtime: 'nodejs14.x', events: [{ type: 'httpApi' }, { type: 'httpApi' }] },
|
||||
{ runtime: 'nodejs14.x', events: [{ type: 'httpApi' }] },
|
||||
{ runtime: 'nodejs14.x', events: [] },
|
||||
{ runtime: 'nodejs14.x', events: [] },
|
||||
{ runtime: '$containerimage', events: [] },
|
||||
{ runtime: 'nodejs14.x', events: [{ type: 'httpApi' }, { type: 'httpApi' }], url: false },
|
||||
{ runtime: 'nodejs14.x', events: [{ type: 'httpApi' }], url: false },
|
||||
{ runtime: 'nodejs14.x', events: [], url: false },
|
||||
{ runtime: 'nodejs14.x', events: [], url: false },
|
||||
{ runtime: '$containerimage', events: [], url: false },
|
||||
{ runtime: 'nodejs14.x', events: [], url: true },
|
||||
],
|
||||
resources: {
|
||||
general: ['AWS::Logs::LogGroup', 'AWS::S3::Bucket', 'Custom'],
|
||||
@ -217,8 +222,8 @@ describe('test/unit/lib/utils/telemetry/generatePayload.test.js', () => {
|
||||
},
|
||||
plugins: ['./custom-provider'],
|
||||
functions: [
|
||||
{ runtime: 'foo', events: [{ type: 'someEvent' }] },
|
||||
{ runtime: 'bar', events: [] },
|
||||
{ runtime: 'foo', events: [{ type: 'someEvent' }], url: false },
|
||||
{ runtime: 'bar', events: [], url: false },
|
||||
],
|
||||
resources: undefined,
|
||||
variableSources: [],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user