Skip to content

Commit

Permalink
refactor(apprunner): consolidate name validations for App Runner Reso…
Browse files Browse the repository at this point in the history
…urces and improve error messages (#32062)

### Issue # (if applicable)

N/A

### Reason for this change
Consolidate name validations for App Runner Resources and improve error messages.



### Description of changes
* Split regex-pattern check and length check to be more user-friendly.
  * `autoScalingConfigurationName` in `AutoScalingConfiguration`
  * `observabilityConfigurationName` in `ObservabilityConfigurationName`
  * `vpcIngressConnectionName` in `VpcIngressConnectionName`
* Add name validations.
  * `serviceName` in `ServiceName`. (Ref: [CFn document](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apprunner-service.html#cfn-apprunner-service-servicename))
  * `vpcConnectorName` in `VpcConnectorName`. (Ref: [CFn document](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apprunner-vpcconnector.html#cfn-apprunner-vpcconnector-vpcconnectorname))




### Description of how you validated changes
Modify and add unit tests.



### Checklist
- [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
mazyu36 authored and hemige committed Nov 19, 2024
1 parent 92a432d commit d6b8c75
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export class AutoScalingConfiguration extends cdk.Resource implements IAutoScali
const resourceParts = cdk.Fn.split('/', autoScalingConfigurationArn);

if (!resourceParts || resourceParts.length < 3) {
throw new Error(`Unexpected ARN format: ${autoScalingConfigurationArn}`);
throw new Error(`Unexpected ARN format: ${autoScalingConfigurationArn}.`);
}

const autoScalingConfigurationName = cdk.Fn.select(0, resourceParts);
Expand Down Expand Up @@ -170,32 +170,39 @@ export class AutoScalingConfiguration extends cdk.Resource implements IAutoScali
}

private validateAutoScalingConfiguration(props: AutoScalingConfigurationProps) {
if (
props.autoScalingConfigurationName !== undefined &&
!cdk.Token.isUnresolved(props.autoScalingConfigurationName) &&
!/^[A-Za-z0-9][A-Za-z0-9\-_]{3,31}$/.test(props.autoScalingConfigurationName)
) {
throw new Error(`autoScalingConfigurationName must match the ^[A-Za-z0-9][A-Za-z0-9\-_]{3,31}$ pattern, got ${props.autoScalingConfigurationName}`);
if (props.autoScalingConfigurationName !== undefined && !cdk.Token.isUnresolved(props.autoScalingConfigurationName)) {

if (props.autoScalingConfigurationName.length < 4 || props.autoScalingConfigurationName.length > 32) {
throw new Error(
`\`autoScalingConfigurationName\` must be between 4 and 32 characters, got: ${props.autoScalingConfigurationName.length} characters.`,
);
}

if (!/^[A-Za-z0-9][A-Za-z0-9\-_]*$/.test(props.autoScalingConfigurationName)) {
throw new Error(
`\`autoScalingConfigurationName\` must start with an alphanumeric character and contain only alphanumeric characters, hyphens, or underscores after that, got: ${props.autoScalingConfigurationName}.`,
);
}
}

const isMinSizeDefined = typeof props.minSize === 'number';
const isMaxSizeDefined = typeof props.maxSize === 'number';
const isMaxConcurrencyDefined = typeof props.maxConcurrency === 'number';

if (isMinSizeDefined && (props.minSize < 1 || props.minSize > 25)) {
throw new Error(`minSize must be between 1 and 25, got ${props.minSize}`);
throw new Error(`minSize must be between 1 and 25, got ${props.minSize}.`);
}

if (isMaxSizeDefined && (props.maxSize < 1 || props.maxSize > 25)) {
throw new Error(`maxSize must be between 1 and 25, got ${props.maxSize}`);
throw new Error(`maxSize must be between 1 and 25, got ${props.maxSize}.`);
}

if (isMinSizeDefined && isMaxSizeDefined && !(props.minSize < props.maxSize)) {
throw new Error('maxSize must be greater than minSize');
throw new Error('maxSize must be greater than minSize.');
}

if (isMaxConcurrencyDefined && (props.maxConcurrency < 1 || props.maxConcurrency > 200)) {
throw new Error(`maxConcurrency must be between 1 and 200, got ${props.maxConcurrency}`);
throw new Error(`maxConcurrency must be between 1 and 200, got ${props.maxConcurrency}.`);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export class ObservabilityConfiguration extends cdk.Resource implements IObserva
const resourceParts = cdk.Fn.split('/', observabilityConfigurationArn);

if (!resourceParts || resourceParts.length < 3) {
throw new Error(`Unexpected ARN format: ${observabilityConfigurationArn}`);
throw new Error(`Unexpected ARN format: ${observabilityConfigurationArn}.`);
}

const observabilityConfigurationName = cdk.Fn.select(0, resourceParts);
Expand Down Expand Up @@ -141,12 +141,19 @@ export class ObservabilityConfiguration extends cdk.Resource implements IObserva
physicalName: props.observabilityConfigurationName,
});

if (
props.observabilityConfigurationName !== undefined &&
!cdk.Token.isUnresolved(props.observabilityConfigurationName) &&
!/^[A-Za-z0-9][A-Za-z0-9\-_]{3,31}$/.test(props.observabilityConfigurationName)
) {
throw new Error(`observabilityConfigurationName must match the \`^[A-Za-z0-9][A-Za-z0-9\-_]{3,31}$\` pattern, got ${props.observabilityConfigurationName}`);
if (props.observabilityConfigurationName !== undefined && !cdk.Token.isUnresolved(props.observabilityConfigurationName)) {

if (props.observabilityConfigurationName.length < 4 || props.observabilityConfigurationName.length > 32) {
throw new Error(
`\`observabilityConfigurationName\` must be between 4 and 32 characters, got: ${props.observabilityConfigurationName.length} characters.`,
);
}

if (!/^[A-Za-z0-9][A-Za-z0-9\-_]*$/.test(props.observabilityConfigurationName)) {
throw new Error(
`\`observabilityConfigurationName\` must start with an alphanumeric character and contain only alphanumeric characters, hyphens, or underscores after that, got: ${props.observabilityConfigurationName}.`,
);
}
}

const resource = new CfnObservabilityConfiguration(this, 'Resource', {
Expand Down
15 changes: 15 additions & 0 deletions packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1293,6 +1293,21 @@ export class Service extends cdk.Resource implements iam.IGrantable {
throw new Error('configurationValues cannot be provided if the ConfigurationSource is Repository');
}

if (props.serviceName !== undefined && !cdk.Token.isUnresolved(props.serviceName)) {

if (props.serviceName.length < 4 || props.serviceName.length > 40) {
throw new Error(
`\`serviceName\` must be between 4 and 40 characters, got: ${props.serviceName.length} characters.`,
);
}

if (!/^[A-Za-z0-9][A-Za-z0-9\-_]*$/.test(props.serviceName)) {
throw new Error(
`\`serviceName\` must start with an alphanumeric character and contain only alphanumeric characters, hyphens, or underscores after that, got: ${props.serviceName}.`,
);
}
}

const resource = new CfnService(this, 'Resource', {
serviceName: this.props.serviceName,
instanceConfiguration: {
Expand Down
15 changes: 15 additions & 0 deletions packages/@aws-cdk/aws-apprunner-alpha/lib/vpc-connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,21 @@ export class VpcConnector extends cdk.Resource implements IVpcConnector {
physicalName: props.vpcConnectorName,
});

if (props.vpcConnectorName !== undefined && !cdk.Token.isUnresolved(props.vpcConnectorName)) {

if (props.vpcConnectorName.length < 4 || props.vpcConnectorName.length > 40) {
throw new Error(
`\`vpcConnectorName\` must be between 4 and 40 characters, got: ${props.vpcConnectorName.length} characters.`,
);
}

if (!/^[A-Za-z0-9][A-Za-z0-9\-_]*$/.test(props.vpcConnectorName)) {
throw new Error(
`\`vpcConnectorName\` must start with an alphanumeric character and contain only alphanumeric characters, hyphens, or underscores after that, got: ${props.vpcConnectorName}.`,
);
}
}

const securityGroups = props.securityGroups?.length ?
props.securityGroups
: [new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: props.vpc })];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,19 @@ export class VpcIngressConnection extends cdk.Resource implements IVpcIngressCon
physicalName: props.vpcIngressConnectionName,
});

if (
props.vpcIngressConnectionName !== undefined &&
!cdk.Token.isUnresolved(props.vpcIngressConnectionName) &&
!/^[A-Za-z0-9][A-Za-z0-9\-_]{3,39}$/.test(props.vpcIngressConnectionName)
) {
throw new Error(`vpcIngressConnectionName must match the \`^[A-Za-z0-9][A-Za-z0-9\-_]{3,39}\` pattern, got ${props.vpcIngressConnectionName}`);
if (props.vpcIngressConnectionName !== undefined && !cdk.Token.isUnresolved(props.vpcIngressConnectionName)) {

if (props.vpcIngressConnectionName.length < 4 || props.vpcIngressConnectionName.length > 40) {
throw new Error(
`\`vpcIngressConnectionName\` must be between 4 and 40 characters, got: ${props.vpcIngressConnectionName.length} characters.`,
);
}

if (!/^[A-Za-z0-9][A-Za-z0-9\-_]*$/.test(props.vpcIngressConnectionName)) {
throw new Error(
`\`vpcIngressConnectionName\` must start with an alphanumeric character and contain only alphanumeric characters, hyphens, or underscores after that, got: ${props.vpcIngressConnectionName}.`,
);
}
}

const resource = new CfnVpcIngressConnection(this, 'Resource', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,28 +63,38 @@ test('minSize greater than maxSize', () => {
minSize: 5,
maxSize: 3,
});
}).toThrow('maxSize must be greater than minSize');
}).toThrow('maxSize must be greater than minSize.');
});

test.each([0, 201])('invalid maxConcurrency', (maxConcurrency: number) => {
expect(() => {
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
maxConcurrency,
});
}).toThrow(`maxConcurrency must be between 1 and 200, got ${maxConcurrency}`);
}).toThrow(`maxConcurrency must be between 1 and 200, got ${maxConcurrency}.`);
});

test.each([
['tes'],
['test-autoscaling-configuration-name-over-limitation'],
])('autoScalingConfigurationName length is invalid(name: %s)', (autoScalingConfigurationName: string) => {
expect(() => {
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
autoScalingConfigurationName,
});
}).toThrow(`\`autoScalingConfigurationName\` must be between 4 and 32 characters, got: ${autoScalingConfigurationName.length} characters.`);
});

test.each([
['-test'],
['test-?'],
])('invalid autoScalingConfigurationName (name: %s)', (autoScalingConfigurationName: string) => {
['test-\\'],
])('autoScalingConfigurationName includes invalid characters(name: %s)', (autoScalingConfigurationName: string) => {
expect(() => {
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
autoScalingConfigurationName,
});
}).toThrow(`autoScalingConfigurationName must match the ^[A-Za-z0-9][A-Za-z0-9\-_]{3,31}$ pattern, got ${autoScalingConfigurationName}`);
}).toThrow(`\`autoScalingConfigurationName\` must start with an alphanumeric character and contain only alphanumeric characters, hyphens, or underscores after that, got: ${autoScalingConfigurationName}.`);
});

test('create an Auto scaling Configuration with tags', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,26 @@ test.each([
test.each([
['tes'],
['test-observability-configuration-name-over-limitation'],
])('observabilityConfigurationName length is invalid (name: %s)', (observabilityConfigurationName: string) => {
expect(() => {
new ObservabilityConfiguration(stack, 'ObservabilityConfiguration', {
observabilityConfigurationName,
traceConfigurationVendor: TraceConfigurationVendor.AWSXRAY,
});
}).toThrow(`\`observabilityConfigurationName\` must be between 4 and 32 characters, got: ${observabilityConfigurationName.length} characters.`);
});

test.each([
['-test'],
['test-?'],
])('observabilityConfigurationName over length limitation (name: %s)', (observabilityConfigurationName: string) => {
['test-\\'],
])('observabilityConfigurationName includes invalid characters (name: %s)', (observabilityConfigurationName: string) => {
expect(() => {
new ObservabilityConfiguration(stack, 'ObservabilityConfiguration', {
observabilityConfigurationName,
traceConfigurationVendor: TraceConfigurationVendor.AWSXRAY,
});
}).toThrow(`observabilityConfigurationName must match the \`^[A-Za-z0-9][A-Za-z0-9\-_]{3,31}$\` pattern, got ${observabilityConfigurationName}`);
}).toThrow(`\`observabilityConfigurationName\` must start with an alphanumeric character and contain only alphanumeric characters, hyphens, or underscores after that, got: ${observabilityConfigurationName}.`);
});

test('create an Auto scaling Configuration with tags', () => {
Expand Down
39 changes: 39 additions & 0 deletions packages/@aws-cdk/aws-apprunner-alpha/test/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1781,3 +1781,42 @@ test.each([true, false])('isPubliclyAccessible is set %s', (isPubliclyAccessible
},
});
});

test.each([
['tes'],
['test-service-name-over-limitation-apprunner'],
])('serviceName length is invalid (name: %s)', (serviceName: string) => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'demo-stack');

expect(() => {
new apprunner.Service(stack, 'DemoService', {
source: apprunner.Source.fromEcrPublic({
imageConfiguration: { port: 8000 },
imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest',
}),
serviceName,
});
}).toThrow(`\`serviceName\` must be between 4 and 40 characters, got: ${serviceName.length} characters.`);
});

test.each([
['-test'],
['test-?'],
['test-\\'],
])('serviceName includes invalid characters (name: %s)', (serviceName: string) => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'demo-stack');

expect(() => {
new apprunner.Service(stack, 'DemoService', {
source: apprunner.Source.fromEcrPublic({
imageConfiguration: { port: 8000 },
imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest',
}),
serviceName,
});
}).toThrow(`\`serviceName\` must start with an alphanumeric character and contain only alphanumeric characters, hyphens, or underscores after that, got: ${serviceName}.`);
});
45 changes: 44 additions & 1 deletion packages/@aws-cdk/aws-apprunner-alpha/test/vpc-connector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,47 @@ test('create a vpcConnector with an empty security group array should create one
},
],
});
});
});

test.each([
['tes'],
['test-vpc-connector-name-over-limitation-apprunner'],
])('vpcConnectorName length is invalid (name: %s)', (vpcConnectorName: string) => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'demo-stack');

const vpc = new ec2.Vpc(stack, 'Vpc', {
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
});

expect(() => {
new VpcConnector(stack, 'VpcConnector', {
vpc,
vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC }),
vpcConnectorName,
});
}).toThrow(`\`vpcConnectorName\` must be between 4 and 40 characters, got: ${vpcConnectorName.length} characters.`);
});

test.each([
['-test'],
['test-?'],
['test-\\'],
])('vpcConnectorName includes invalid characters (name: %s)', (vpcConnectorName: string) => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'demo-stack');

const vpc = new ec2.Vpc(stack, 'Vpc', {
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
});

expect(() => {
new VpcConnector(stack, 'VpcConnector', {
vpc,
vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC }),
vpcConnectorName,
});
}).toThrow(`\`vpcConnectorName\` must start with an alphanumeric character and contain only alphanumeric characters, hyphens, or underscores after that, got: ${vpcConnectorName}.`);
});
Loading

0 comments on commit d6b8c75

Please sign in to comment.