Skip to content

Commit eef4b0d

Browse files
committed
refactor(elasticache): improve code organization
1 parent 06537ee commit eef4b0d

File tree

2 files changed

+159
-84
lines changed

2 files changed

+159
-84
lines changed

packages/aws-cdk-lib/aws-elasticache/lib/serverless-cache.ts

Lines changed: 128 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,23 @@ export enum DataStorageUnit {
2323
GIGABYTES = 'GB',
2424
}
2525

26+
/**
27+
* Minimum data storage size in GB for ServerlessCache
28+
*/
29+
const DATA_STORAGE_MIN_GB = 1;
30+
/**
31+
* Maximum data storage size in GB for ServerlessCache
32+
*/
33+
const DATA_STORAGE_MAX_GB = 5000;
34+
/**
35+
* Minimum request rate limit in ECPUs per second for ServerlessCache
36+
*/
37+
const REQUEST_RATE_MIN_ECPU = 1000;
38+
/**
39+
* Maximum request rate limit in ECPUs per second for ServerlessCache
40+
*/
41+
const REQUEST_RATE_MAX_ECPU = 15000000;
42+
2643
/**
2744
* Usage limits configuration for ServerlessCache
2845
*/
@@ -266,27 +283,26 @@ export class ServerlessCache extends ServerlessCacheBase {
266283
let arn: string;
267284
const stack = Stack.of(scope);
268285

269-
if (!attrs.serverlessCacheName) {
270-
if (!attrs.serverlessCacheArn) {
271-
throw new ValidationError('One of serverlessCacheName or serverlessCacheArn is required!', scope);
272-
}
286+
if (attrs.serverlessCacheArn && attrs.serverlessCacheName) {
287+
throw new ValidationError('Only one of serverlessCacheArn or serverlessCacheName can be provided.', scope);
288+
}
273289

290+
if (attrs.serverlessCacheArn) {
274291
arn = attrs.serverlessCacheArn;
275-
const maybeServerlessCacheName = stack.splitArn(attrs.serverlessCacheArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName;
276-
if (!maybeServerlessCacheName) {
277-
throw new ValidationError('Unable to extract serverless cache name from ARN', scope);
278-
}
279-
name = maybeServerlessCacheName;
280-
} else {
281-
if (attrs.serverlessCacheArn) {
282-
throw new ValidationError('Only one of serverlessCacheArn or serverlessCacheName can be provided', scope);
292+
const extractedServerlessCacheName = stack.splitArn(attrs.serverlessCacheArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName;
293+
if (!extractedServerlessCacheName) {
294+
throw new ValidationError('Unable to extract serverless cache name from ARN.', scope);
283295
}
296+
name = extractedServerlessCacheName;
297+
} else if (attrs.serverlessCacheName) {
284298
name = attrs.serverlessCacheName;
285299
arn = stack.formatArn({
286300
service: 'elasticache',
287301
resource: 'serverlesscache',
288302
resourceName: attrs.serverlessCacheName,
289303
});
304+
} else {
305+
throw new ValidationError('One of serverlessCacheName or serverlessCacheArn is required.', scope);
290306
}
291307

292308
class Import extends ServerlessCacheBase {
@@ -308,23 +324,24 @@ export class ServerlessCache extends ServerlessCacheBase {
308324
this.serverlessCacheName = serverlessCacheName;
309325

310326
if (this.engine) {
311-
const getDefaultPort = (engine: CacheEngine): ec2.Port => {
312-
switch (engine) {
313-
case CacheEngine.VALKEY_DEFAULT:
314-
case CacheEngine.VALKEY_7:
315-
case CacheEngine.VALKEY_8:
316-
case CacheEngine.REDIS_DEFAULT:
317-
return ec2.Port.tcp(6379);
318-
case CacheEngine.MEMCACHED_DEFAULT:
319-
return ec2.Port.tcp(11211);
320-
default:
321-
throw new ValidationError(`Unsupported cache engine: ${engine}`, scope);
322-
}
323-
};
327+
let defaultPort: ec2.Port;
328+
switch (this.engine) {
329+
case CacheEngine.VALKEY_DEFAULT:
330+
case CacheEngine.VALKEY_7:
331+
case CacheEngine.VALKEY_8:
332+
case CacheEngine.REDIS_DEFAULT:
333+
defaultPort = ec2.Port.tcp(6379);
334+
break;
335+
case CacheEngine.MEMCACHED_DEFAULT:
336+
defaultPort = ec2.Port.tcp(11211);
337+
break;
338+
default:
339+
throw new ValidationError(`Unsupported cache engine: ${this.engine}`, scope);
340+
}
324341

325342
this.connections = new ec2.Connections({
326343
securityGroups: this.securityGroups,
327-
defaultPort: getDefaultPort(this.engine),
344+
defaultPort: defaultPort,
328345
});
329346
} else {
330347
this.connections = new ec2.Connections({
@@ -404,35 +421,18 @@ export class ServerlessCache extends ServerlessCacheBase {
404421
this.userGroup = props.userGroup;
405422

406423
this.validateDescription(props.description);
407-
this.validateUsageLimits(props.cacheUsageLimits);
424+
this.validateDataStorageLimits(props.cacheUsageLimits);
425+
this.validateRequestRateLimits(props.cacheUsageLimits);
408426
this.validateBackupSettings(props.backup);
409427
this.validateUserGroupCompatibility(this.engine, this.userGroup);
410428

411-
let subnetIds: string[] | undefined;
412-
let securityGroupIds: string[];
413-
414-
let selectedSubnets;
415-
if (props.vpcSubnets) {
416-
selectedSubnets = props.vpc.selectSubnets(props.vpcSubnets);
417-
} else {
418-
selectedSubnets = props.vpc.selectSubnets({
419-
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
420-
});
421-
}
422-
subnetIds = selectedSubnets.subnetIds.length > 0 ? selectedSubnets.subnetIds : undefined;
423-
this.subnets = selectedSubnets.subnets.length > 0 ? selectedSubnets.subnets : undefined;
429+
const subnetConfig = this.configureSubnets(props);
430+
const subnetIds = subnetConfig.subnetIds;
431+
this.subnets = subnetConfig.subnets;
424432

425-
if (props.securityGroups && props.securityGroups.length > 0) {
426-
securityGroupIds = props.securityGroups.map(sg => sg.securityGroupId);
427-
this.securityGroups = props.securityGroups;
428-
} else {
429-
const newSecurityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', {
430-
description: `Security group for ${this.node.id} cache.`,
431-
vpc: props.vpc,
432-
});
433-
securityGroupIds = [newSecurityGroup.securityGroupId];
434-
this.securityGroups = [newSecurityGroup];
435-
}
433+
const securityGroupConfig = this.configureSecurityGroups(props);
434+
const securityGroupIds = securityGroupConfig.securityGroupIds;
435+
this.securityGroups = securityGroupConfig.securityGroups;
436436

437437
const { engine, version } = this.parseEngine(this.engine);
438438

@@ -491,34 +491,47 @@ export class ServerlessCache extends ServerlessCacheBase {
491491
}
492492

493493
/**
494-
* Validate usage limits are within AWS constraints
494+
* Validate data storage size limits
495495
*
496-
* @param limits The usage limits to validate
496+
* @param limits The usage limits containing data storage settings
497497
*/
498-
private validateUsageLimits(limits?: CacheUsageLimitsProperty): void {
499-
if (limits?.dataStorageMinimumSize && !limits.dataStorageMinimumSize.isUnresolved() &&
500-
(limits.dataStorageMinimumSize.toGibibytes() < 1 || limits.dataStorageMinimumSize.toGibibytes() > 5000)) {
498+
private validateDataStorageLimits(limits?: CacheUsageLimitsProperty): void {
499+
if (!limits) return;
500+
501+
if (limits.dataStorageMinimumSize && !limits.dataStorageMinimumSize.isUnresolved() &&
502+
(limits.dataStorageMinimumSize.toGibibytes() < DATA_STORAGE_MIN_GB || limits.dataStorageMinimumSize.toGibibytes() > DATA_STORAGE_MAX_GB)) {
501503
throw new ValidationError('Data storage minimum must be between 1 and 5000 GB.', this);
502504
}
503-
504-
if (limits?.dataStorageMaximumSize && !limits.dataStorageMaximumSize.isUnresolved() &&
505-
(limits.dataStorageMaximumSize.toGibibytes() < 1 || limits.dataStorageMaximumSize.toGibibytes() > 5000)) {
505+
if (limits.dataStorageMaximumSize && !limits.dataStorageMaximumSize.isUnresolved() &&
506+
(limits.dataStorageMaximumSize.toGibibytes() < DATA_STORAGE_MIN_GB || limits.dataStorageMaximumSize.toGibibytes() > DATA_STORAGE_MAX_GB)) {
506507
throw new ValidationError('Data storage maximum must be between 1 and 5000 GB.', this);
507508
}
508509

509-
if (limits?.dataStorageMinimumSize && limits?.dataStorageMaximumSize &&
510-
!limits.dataStorageMinimumSize.isUnresolved() && !limits.dataStorageMaximumSize.isUnresolved() &&
511-
limits.dataStorageMinimumSize.toGibibytes() > limits.dataStorageMaximumSize.toGibibytes()) {
510+
if (limits.dataStorageMinimumSize && limits.dataStorageMaximumSize &&
511+
!limits.dataStorageMinimumSize.isUnresolved() && !limits.dataStorageMaximumSize.isUnresolved() &&
512+
limits.dataStorageMinimumSize.toGibibytes() > limits.dataStorageMaximumSize.toGibibytes()) {
512513
throw new ValidationError('Data storage minimum cannot be greater than maximum', this);
513514
}
515+
}
516+
517+
/**
518+
* Validate request rate limits
519+
*
520+
* @param limits The usage limits containing request rate settings
521+
*/
522+
private validateRequestRateLimits(limits?: CacheUsageLimitsProperty): void {
523+
if (!limits) return;
514524

515-
if (limits?.requestRateLimitMinimum !== undefined && (limits.requestRateLimitMinimum < 1000 || limits.requestRateLimitMinimum > 15000000)) {
525+
if (limits.requestRateLimitMinimum !== undefined &&
526+
(limits.requestRateLimitMinimum < REQUEST_RATE_MIN_ECPU || limits.requestRateLimitMinimum > REQUEST_RATE_MAX_ECPU)) {
516527
throw new ValidationError('Request rate minimum must be between 1,000 and 15,000,000 ECPUs per second', this);
517528
}
518-
if (limits?.requestRateLimitMaximum !== undefined && (limits.requestRateLimitMaximum < 1000 || limits.requestRateLimitMaximum > 15000000)) {
529+
if (limits.requestRateLimitMaximum !== undefined &&
530+
(limits.requestRateLimitMaximum < REQUEST_RATE_MIN_ECPU || limits.requestRateLimitMaximum > REQUEST_RATE_MAX_ECPU)) {
519531
throw new ValidationError('Request rate maximum must be between 1,000 and 15,000,000 ECPUs per second', this);
520532
}
521-
if (limits?.requestRateLimitMinimum !== undefined && limits?.requestRateLimitMaximum !== undefined &&
533+
534+
if (limits.requestRateLimitMinimum !== undefined && limits.requestRateLimitMaximum !== undefined &&
522535
limits.requestRateLimitMinimum > limits.requestRateLimitMaximum) {
523536
throw new ValidationError('Request rate minimum cannot be greater than maximum', this);
524537
}
@@ -567,6 +580,10 @@ export class ServerlessCache extends ServerlessCacheBase {
567580
private validateUserGroupCompatibility(engine: CacheEngine, userGroup?: IUserGroup): void {
568581
if (!userGroup) return;
569582

583+
if (engine === CacheEngine.MEMCACHED_DEFAULT) {
584+
throw new ValidationError('User groups cannot be used with Memcached engines. Only Redis and Valkey engines support user groups.', this);
585+
}
586+
570587
if (engine === CacheEngine.REDIS_DEFAULT && userGroup.engine !== UserEngine.REDIS) {
571588
throw new ValidationError('Redis cache can only use Redis user groups.', this);
572589
}
@@ -601,6 +618,52 @@ export class ServerlessCache extends ServerlessCacheBase {
601618
return Object.keys(cacheUsageLimits).length > 0 ? cacheUsageLimits : undefined;
602619
}
603620

621+
/**
622+
* Configure subnets for the cache
623+
*
624+
* @param props The ServerlessCache properties
625+
* @returns Object containing subnet IDs and subnet objects
626+
*/
627+
private configureSubnets(props: ServerlessCacheProps): { subnetIds: string[] | undefined; subnets: ec2.ISubnet[] | undefined } {
628+
let selectedSubnets;
629+
if (props.vpcSubnets) {
630+
selectedSubnets = props.vpc.selectSubnets(props.vpcSubnets);
631+
} else {
632+
selectedSubnets = props.vpc.selectSubnets({
633+
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
634+
});
635+
}
636+
637+
return {
638+
subnetIds: selectedSubnets.subnetIds.length > 0 ? selectedSubnets.subnetIds : undefined,
639+
subnets: selectedSubnets.subnets.length > 0 ? selectedSubnets.subnets : undefined,
640+
};
641+
}
642+
643+
/**
644+
* Configure security groups for the cache
645+
*
646+
* @param props The ServerlessCache properties
647+
* @returns Object containing security group IDs and security group objects
648+
*/
649+
private configureSecurityGroups(props: ServerlessCacheProps): { securityGroupIds: string[]; securityGroups: ec2.ISecurityGroup[] } {
650+
if (props.securityGroups && props.securityGroups.length > 0) {
651+
return {
652+
securityGroupIds: props.securityGroups.map(sg => sg.securityGroupId),
653+
securityGroups: props.securityGroups,
654+
};
655+
} else {
656+
const newSecurityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', {
657+
description: `Security group for ${this.node.id} cache.`,
658+
vpc: props.vpc,
659+
});
660+
return {
661+
securityGroupIds: [newSecurityGroup.securityGroupId],
662+
securityGroups: [newSecurityGroup],
663+
};
664+
}
665+
}
666+
604667
/**
605668
* Format schedule to HH:MM format for daily backups
606669
*

packages/aws-cdk-lib/aws-elasticache/lib/user-group.ts

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -185,26 +185,26 @@ export class UserGroup extends UserGroupBase {
185185
let userGroupArn: string;
186186
const stack = Stack.of(scope);
187187

188-
if (!attrs.userGroupName) {
189-
if (!attrs.userGroupArn) {
190-
throw new ValidationError('One of userGroupName or userGroupArn is required!', scope);
191-
}
188+
if (attrs.userGroupArn && attrs.userGroupName) {
189+
throw new ValidationError('Only one of userGroupArn or userGroupName can be provided.', scope);
190+
}
191+
192+
if (attrs.userGroupArn) {
192193
userGroupArn = attrs.userGroupArn;
193-
const maybeUserGroupName = stack.splitArn(attrs.userGroupArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName;
194-
if (!maybeUserGroupName) {
195-
throw new ValidationError('Unable to extract user group name from ARN', scope);
196-
}
197-
userGroupName = maybeUserGroupName;
198-
} else {
199-
if (attrs.userGroupArn) {
200-
throw new ValidationError('Only one of userGroupArn or userGroupName can be provided', scope);
194+
const extractedUserGroupName = stack.splitArn(attrs.userGroupArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName;
195+
if (!extractedUserGroupName) {
196+
throw new ValidationError('Unable to extract user group name from ARN.', scope);
201197
}
198+
userGroupName = extractedUserGroupName;
199+
} else if (attrs.userGroupName) {
202200
userGroupName = attrs.userGroupName;
203201
userGroupArn = stack.formatArn({
204202
service: 'elasticache',
205203
resource: 'usergroup',
206204
resourceName: attrs.userGroupName,
207205
});
206+
} else {
207+
throw new ValidationError('One of userGroupName or userGroupArn is required.', scope);
208208
}
209209

210210
class Import extends UserGroupBase {
@@ -274,13 +274,7 @@ export class UserGroup extends UserGroupBase {
274274
userGroupId: this.physicalName,
275275
userIds: Lazy.list({
276276
produce: () => {
277-
if (this.engine === UserEngine.REDIS) {
278-
const hasDefaultUser = this._users.some(user => user.userName === 'default');
279-
if (!hasDefaultUser) {
280-
throw new ValidationError('Redis user groups need to contain a user with the user name "default".', this);
281-
}
282-
}
283-
277+
this.validateUsers();
284278
return this._users.map(user => user.userId);
285279
},
286280
}),
@@ -316,6 +310,24 @@ export class UserGroup extends UserGroupBase {
316310
return this._users;
317311
}
318312

313+
/**
314+
* Validates users in the user group for duplicate usernames and Redis-specific requirements.
315+
*/
316+
private validateUsers(): void {
317+
const userNames = this._users.map(user => user.userName);
318+
const duplicates = userNames.filter((name, index) => userNames.indexOf(name) !== index);
319+
if (duplicates.length > 0) {
320+
throw new ValidationError('User group cannot have users with the same user name.', this);
321+
}
322+
323+
if (this.engine === UserEngine.REDIS) {
324+
const hasDefaultUser = this._users.some(user => user.userName === 'default');
325+
if (!hasDefaultUser) {
326+
throw new ValidationError('Redis user groups need to contain a user with the user name "default".', this);
327+
}
328+
}
329+
}
330+
319331
/**
320332
* Add a user to this user group
321333
*

0 commit comments

Comments
 (0)