Skip to content

Commit

Permalink
add validation for environment prop (#2290)
Browse files Browse the repository at this point in the history
* add validation for environment prop

* remove redundant length validation

* PR feedback
  • Loading branch information
rtpascual authored Dec 3, 2024
1 parent 0cf5c26 commit d227f96
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/shy-lions-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/backend-function': patch
---

change errors in FunctionFactory to AmplifyUserError
5 changes: 5 additions & 0 deletions .changeset/weak-nails-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/backend-function': patch
---

add validation for environment prop
141 changes: 122 additions & 19 deletions packages/backend-function/src/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import fsp from 'fs/promises';
import path from 'node:path';
import { AmplifyUserError } from '@aws-amplify/platform-core';

const createStackAndSetContext = (): Stack => {
const app = new App();
Expand Down Expand Up @@ -207,9 +208,10 @@ void describe('AmplifyFunctionFactory', () => {
entry: './test-assets/default-lambda/handler.ts',
timeoutSeconds: 0,
}).getInstance(getInstanceProps),
new Error(
'timeoutSeconds must be a whole number between 1 and 900 inclusive'
)
new AmplifyUserError('InvalidTimeoutError', {
message: `Invalid function timeout of 0`,
resolution: `timeoutSeconds must be a whole number between 1 and 900 inclusive`,
})
);
});

Expand All @@ -220,9 +222,10 @@ void describe('AmplifyFunctionFactory', () => {
entry: './test-assets/default-lambda/handler.ts',
timeoutSeconds: 901,
}).getInstance(getInstanceProps),
new Error(
'timeoutSeconds must be a whole number between 1 and 900 inclusive'
)
new AmplifyUserError('InvalidTimeoutError', {
message: `Invalid function timeout of 901`,
resolution: `timeoutSeconds must be a whole number between 1 and 900 inclusive`,
})
);
});

Expand All @@ -233,9 +236,10 @@ void describe('AmplifyFunctionFactory', () => {
entry: './test-assets/default-lambda/handler.ts',
timeoutSeconds: 10.5,
}).getInstance(getInstanceProps),
new Error(
'timeoutSeconds must be a whole number between 1 and 900 inclusive'
)
new AmplifyUserError('InvalidTimeoutError', {
message: `Invalid function timeout of 10.5`,
resolution: `timeoutSeconds must be a whole number between 1 and 900 inclusive`,
})
);
});
});
Expand Down Expand Up @@ -271,9 +275,10 @@ void describe('AmplifyFunctionFactory', () => {
entry: './test-assets/default-lambda/handler.ts',
memoryMB: 127,
}).getInstance(getInstanceProps),
new Error(
'memoryMB must be a whole number between 128 and 10240 inclusive'
)
new AmplifyUserError('InvalidMemoryMBError', {
message: `Invalid function memoryMB of 127`,
resolution: `memoryMB must be a whole number between 128 and 10240 inclusive`,
})
);
});

Expand All @@ -284,9 +289,10 @@ void describe('AmplifyFunctionFactory', () => {
entry: './test-assets/default-lambda/handler.ts',
memoryMB: 10241,
}).getInstance(getInstanceProps),
new Error(
'memoryMB must be a whole number between 128 and 10240 inclusive'
)
new AmplifyUserError('InvalidMemoryMBError', {
message: `Invalid function memoryMB of 10241`,
resolution: `memoryMB must be a whole number between 128 and 10240 inclusive`,
})
);
});

Expand All @@ -297,9 +303,103 @@ void describe('AmplifyFunctionFactory', () => {
entry: './test-assets/default-lambda/handler.ts',
memoryMB: 256.2,
}).getInstance(getInstanceProps),
new Error(
'memoryMB must be a whole number between 128 and 10240 inclusive'
)
new AmplifyUserError('InvalidMemoryMBError', {
message: `Invalid function memoryMB of 256.2`,
resolution: `memoryMB must be a whole number between 128 and 10240 inclusive`,
})
);
});
});

void describe('environment property', () => {
void it('sets valid environment', () => {
const functionFactory = defineFunction({
entry: './test-assets/default-lambda/handler.ts',
name: 'myCoolLambda',
environment: {
TEST_ENV: 'testValue',
},
});
const lambda = functionFactory.getInstance(getInstanceProps);
const stack = lambda.stack;
const template = Template.fromStack(stack);
template.resourceCountIs('AWS::Lambda::Function', 1);
template.hasResourceProperties('AWS::Lambda::Function', {
Environment: {
Variables: {
TEST_ENV: 'testValue',
},
},
});
});

void it('sets default environment', () => {
const functionFactory = defineFunction({
entry: './test-assets/default-lambda/handler.ts',
name: 'myCoolLambda',
});
const lambda = functionFactory.getInstance(getInstanceProps);
const stack = lambda.stack;
const template = Template.fromStack(stack);
template.resourceCountIs('AWS::Lambda::Function', 1);
template.hasResourceProperties('AWS::Lambda::Function', {
Environment: {},
});
});

void it('throws when adding environment variables with invalid key', () => {
assert.throws(
() =>
defineFunction({
entry: './test-assets/default-lambda/handler.ts',
name: 'myCoolLambda',
environment: {
'this.is.wrong': 'testValue',
},
}).getInstance(getInstanceProps),
new AmplifyUserError('InvalidEnvironmentKeyError', {
message: `Invalid function environment key(s): this.is.wrong`,
resolution:
'Environment keys must match [a-zA-Z]([a-zA-Z0-9_])+ and be at least 2 characters',
})
);
});

void it('throws when adding environment variables with key less than 2 characters', () => {
assert.throws(
() =>
defineFunction({
entry: './test-assets/default-lambda/handler.ts',
name: 'myCoolLambda',
environment: {
A: 'testValue',
},
}).getInstance(getInstanceProps),
new AmplifyUserError('InvalidEnvironmentKeyError', {
message: `Invalid function environment key(s): A`,
resolution:
'Environment keys must match [a-zA-Z]([a-zA-Z0-9_])+ and be at least 2 characters',
})
);
});

void it('throws when multiple environment variables are invalid', () => {
assert.throws(
() =>
defineFunction({
entry: './test-assets/default-lambda/handler.ts',
name: 'lambdaWithMultipleEnvVars',
environment: {
A: 'testValueA',
TEST_ENV: 'envValue',
'this.is.wrong': 'testValue',
},
}).getInstance(getInstanceProps),
new AmplifyUserError('InvalidEnvironmentKeyError', {
message: `Invalid function environment key(s): A, this.is.wrong`,
resolution:
'Environment keys must match [a-zA-Z]([a-zA-Z0-9_])+ and be at least 2 characters',
})
);
});
});
Expand Down Expand Up @@ -335,7 +435,10 @@ void describe('AmplifyFunctionFactory', () => {
entry: './test-assets/default-lambda/handler.ts',
runtime: 14 as NodeVersion,
}).getInstance(getInstanceProps),
new Error('runtime must be one of the following: 16, 18, 20, 22')
new AmplifyUserError('InvalidRuntimeError', {
message: `Invalid function runtime of 14`,
resolution: 'runtime must be one of the following: 16, 18, 20, 22',
})
);
});

Expand Down
52 changes: 41 additions & 11 deletions packages/backend-function/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ class FunctionFactory implements ConstructFactory<AmplifyFunction> {
entry: this.resolveEntry(),
timeoutSeconds: this.resolveTimeout(),
memoryMB: this.resolveMemory(),
environment: this.props.environment ?? {},
environment: this.resolveEnvironment(),
runtime: this.resolveRuntime(),
schedule: this.resolveSchedule(),
bundling: this.resolveBundling(),
Expand Down Expand Up @@ -294,9 +294,10 @@ class FunctionFactory implements ConstructFactory<AmplifyFunction> {
timeoutMax
)
) {
throw new Error(
`timeoutSeconds must be a whole number between ${timeoutMin} and ${timeoutMax} inclusive`
);
throw new AmplifyUserError('InvalidTimeoutError', {
message: `Invalid function timeout of ${this.props.timeoutSeconds}`,
resolution: `timeoutSeconds must be a whole number between ${timeoutMin} and ${timeoutMax} inclusive`,
});
}
return this.props.timeoutSeconds;
};
Expand All @@ -311,13 +312,41 @@ class FunctionFactory implements ConstructFactory<AmplifyFunction> {
if (
!isWholeNumberBetweenInclusive(this.props.memoryMB, memoryMin, memoryMax)
) {
throw new Error(
`memoryMB must be a whole number between ${memoryMin} and ${memoryMax} inclusive`
);
throw new AmplifyUserError('InvalidMemoryMBError', {
message: `Invalid function memoryMB of ${this.props.memoryMB}`,
resolution: `memoryMB must be a whole number between ${memoryMin} and ${memoryMax} inclusive`,
});
}
return this.props.memoryMB;
};

private resolveEnvironment = () => {
if (this.props.environment === undefined) {
return {};
}

const invalidKeys: string[] = [];

Object.keys(this.props.environment).forEach((key) => {
// validate using key pattern from https://docs.aws.amazon.com/lambda/latest/api/API_Environment.html
if (!key.match(/^[a-zA-Z]([a-zA-Z0-9_])+$/)) {
invalidKeys.push(key);
}
});

if (invalidKeys.length > 0) {
throw new AmplifyUserError('InvalidEnvironmentKeyError', {
message: `Invalid function environment key(s): ${invalidKeys.join(
', '
)}`,
resolution:
'Environment keys must match [a-zA-Z]([a-zA-Z0-9_])+ and be at least 2 characters',
});
}

return this.props.environment;
};

private resolveRuntime = () => {
const runtimeDefault = 18;

Expand All @@ -327,11 +356,12 @@ class FunctionFactory implements ConstructFactory<AmplifyFunction> {
}

if (!(this.props.runtime in nodeVersionMap)) {
throw new Error(
`runtime must be one of the following: ${Object.keys(
throw new AmplifyUserError('InvalidRuntimeError', {
message: `Invalid function runtime of ${this.props.runtime}`,
resolution: `runtime must be one of the following: ${Object.keys(
nodeVersionMap
).join(', ')}`
);
).join(', ')}`,
});
}

return this.props.runtime;
Expand Down

0 comments on commit d227f96

Please sign in to comment.