Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ osls-compose --help

Available commands: `osls-compose`, `oslsc`.

Check out the [osls compose documentation](https://github.com/oss-serverless/osls/blob/main/docs/guides/compose.md).
Check out the [osls compose documentation](https://github.com/oss-serverless/osls/blob/4.x/docs/guides/compose.md).

<p align="center"><a href="https://github.com/oss-serverless/osls/blob/main/docs/guides/compose.md"><img src="https://assets.website-files.com/6178ec21bdb27bb4cd52c72d/625d76707477fa1efbb3559d_blog%20header.png" width="600px"></a></p>
Compose honors the same proxy, custom certificate authority, and timeout environment variables as osls (`HTTP_PROXY`/`HTTPS_PROXY`, `ca`/`cafile`, and `AWS_CLIENT_TIMEOUT`) for its own AWS requests, such as remote state access. See [Running behind a proxy](https://github.com/oss-serverless/osls/blob/4.x/docs/guides/credentials.md#running-behind-a-proxy).

<p align="center"><a href="https://github.com/oss-serverless/osls/blob/4.x/docs/guides/compose.md"><img src="https://assets.website-files.com/6178ec21bdb27bb4cd52c72d/625d76707477fa1efbb3559d_blog%20header.png" width="600px"></a></p>
3 changes: 1 addition & 2 deletions src/state/S3StateStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ const ServerlessError = require('../serverless-error');
const BaseStateStorage = require('./BaseStateStorage');
const normalizeState = require('./normalize-state');
const { buildClientConfig } = require('../utils/aws/config');

const getAwsErrorCode = (error) => error && (error.Code || error.code || error.name);
const { getAwsErrorCode } = require('../utils/aws');

class S3StateStorage extends BaseStateStorage {
constructor(config = {}) {
Expand Down
3 changes: 1 addition & 2 deletions src/state/utils/get-state-bucket-name.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

const crypto = require('crypto');
const { CloudFormation } = require('@aws-sdk/client-cloudformation');
const { getAwsClientConfig } = require('../../utils/aws');
const { getAwsClientConfig, getAwsErrorCode } = require('../../utils/aws');
const { sleep } = require('../../utils');
const getConfiguredStateBucketName = require('./get-configured-state-bucket-name');
const remoteStateCloudFormationTemplate = require('./remote-state-cloudformation-template.json');
const ServerlessError = require('../../serverless-error');

const COMPOSE_REMOTE_STATE_STACK_NAME = 'serverless-compose-state';
const getAwsErrorCode = (error) => error && (error.Code || error.code || error.name);

const getCloudFormationClient = (stateConfiguration = {}, context = {}) => {
// We are enforcing us-east-1 as the intention (that might change in the future if we find a good reason)
Expand Down
4 changes: 1 addition & 3 deletions src/state/utils/get-state-bucket-region.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
'use strict';

const { S3 } = require('@aws-sdk/client-s3');
const { getAwsClientConfig } = require('../../utils/aws');
const { getAwsClientConfig, getAwsErrorCode } = require('../../utils/aws');
const ServerlessError = require('../../serverless-error');

const getAwsErrorCode = (error) => error && (error.Code || error.code || error.name);

const getStateBucketRegion = async (bucketName, stateConfiguration = {}, context = {}) => {
const client = new S3(
getAwsClientConfig({
Expand Down
3 changes: 3 additions & 0 deletions src/utils/aws/get-aws-error-code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports = (error) => error && (error.Code || error.code || error.name);
2 changes: 2 additions & 0 deletions src/utils/aws/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use strict';

const getAwsClientConfig = require('./get-client-config');
const getAwsErrorCode = require('./get-aws-error-code');
const getCredentialProvider = require('./get-credential-provider');

module.exports = {
getAwsClientConfig,
getAwsErrorCode,
getCredentialProvider,
};
19 changes: 17 additions & 2 deletions src/utils/tokenize-exception.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,33 @@

const { inspect } = require('util');
const isError = require('type/error/is');
const { hasOwn } = require('./safe-object');

const userErrorNames = new Set(['ServerlessError']);

// AWS SDK v3 service errors carry response metadata in `$metadata`
const isAwsSdkV3ServiceError = (exception) =>
Boolean(hasOwn(exception, '$metadata') && exception.$metadata && exception.name);

const toConstantCase = (name) =>
name
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
.toUpperCase();

module.exports = (exception) => {
if (isError(exception)) {
const isAwsServiceError = isAwsSdkV3ServiceError(exception);
return {
title: exception.name.replace(/([A-Z])/g, ' $1').trim(),
name: exception.name,
stack: exception.stack,
message: exception.message,
isUserError: userErrorNames.has(exception.name),
code: exception.code,
// AWS service errors (access denied, throttling exhaustion, ...) reflect the user's
// account or environment, not framework bugs - render them without a stack trace
isUserError: userErrorNames.has(exception.name) || isAwsServiceError,
code:
exception.code ?? (isAwsServiceError ? `AWS_${toConstantCase(exception.name)}` : undefined),
};
}
return {
Expand Down
33 changes: 33 additions & 0 deletions test/unit/src/utils/aws/get-aws-error-code.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';

const chai = require('chai');

const getAwsErrorCode = require('../../../../../src/utils/aws/get-aws-error-code');

const expect = chai.expect;

describe('test/unit/src/utils/aws/get-aws-error-code.test.js', () => {
it('prefers the capitalized Code property', () => {
const error = Object.assign(new Error('failed'), {
Code: 'ValidationError',
code: 'lower',
name: 'SomeName',
});
expect(getAwsErrorCode(error)).to.equal('ValidationError');
});

it('falls back to the lowercase code property', () => {
const error = Object.assign(new Error('failed'), { code: 'NoSuchKey' });
expect(getAwsErrorCode(error)).to.equal('NoSuchKey');
});

it('falls back to the error name', () => {
const error = Object.assign(new Error('failed'), { name: 'AccessDenied' });
expect(getAwsErrorCode(error)).to.equal('AccessDenied');
});

it('returns a falsy value for missing errors', () => {
expect(getAwsErrorCode(null)).to.equal(null);
expect(getAwsErrorCode(undefined)).to.equal(undefined);
});
});
39 changes: 38 additions & 1 deletion test/unit/src/utils/tokenize-exception.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const expect = require('chai').expect;
const ServerlessError = require('../../../../src/serverless-error');
const tokenizeError = require('../../../../src/utils/tokenize-exception');

describe('test/unit/lib/utils/tokenize-exception.test.js', () => {
describe('test/unit/src/utils/tokenize-exception.test.js', () => {
it('Should tokenize user error', () => {
const errorTokens = tokenizeError(new ServerlessError('Some error', 'ERR_CODE'));
expect(errorTokens.title).to.equal('Serverless Error');
Expand All @@ -25,6 +25,43 @@ describe('test/unit/lib/utils/tokenize-exception.test.js', () => {
expect(errorTokens.isUserError).to.equal(false);
});

it('Should tokenize AWS SDK service error as user error', () => {
const exception = Object.assign(
new Error('User is not authorized to perform: cloudformation:CreateStack'),
{ name: 'AccessDenied', $metadata: { httpStatusCode: 403 } }
);
const errorTokens = tokenizeError(exception);
expect(errorTokens.title).to.equal('Access Denied');
expect(errorTokens.isUserError).to.equal(true);
expect(errorTokens.code).to.equal('AWS_ACCESS_DENIED');
});

it('Should normalize AWS SDK service error names into codes', () => {
const exception = Object.assign(new Error('Rate exceeded'), {
name: 'ThrottlingException',
$metadata: { httpStatusCode: 429 },
});
expect(tokenizeError(exception).code).to.equal('AWS_THROTTLING_EXCEPTION');
});

it('Should preserve explicit codes on AWS SDK service errors', () => {
const exception = Object.assign(new Error('Some error'), {
name: 'ThrottlingException',
code: 'CUSTOM_CODE',
$metadata: { httpStatusCode: 429 },
});
const errorTokens = tokenizeError(exception);
expect(errorTokens.isUserError).to.equal(true);
expect(errorTokens.code).to.equal('CUSTOM_CODE');
});

it('Should not classify errors without $metadata as AWS service errors', () => {
const exception = Object.assign(new Error('Some error'), { name: 'AccessDenied' });
const errorTokens = tokenizeError(exception);
expect(errorTokens.isUserError).to.equal(false);
expect(errorTokens.code).to.equal(undefined);
});

it('Should tokenize non-error exception', () => {
const errorTokens = tokenizeError(null);
expect(errorTokens.title).to.equal('Exception');
Expand Down
Loading