Skip to content

Commit 9e150a7

Browse files
committed
feat(elasticache): add UserGroup construct
1 parent 6755a4e commit 9e150a7

File tree

2 files changed

+343
-0
lines changed

2 files changed

+343
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Engine type for ElastiCache users and user groups
3+
*/
4+
export enum UserEngine {
5+
/**
6+
* Valkey engine
7+
*/
8+
VALKEY = 'valkey',
9+
10+
/**
11+
* Redis engine
12+
*/
13+
REDIS = 'redis',
14+
}
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
import { Construct } from 'constructs';
2+
import { UserEngine } from './common';
3+
import { CfnUser, CfnUserGroup } from './elasticache.generated';
4+
import { IUser } from './user';
5+
import { IResource, Resource, ArnFormat, Stack, Lazy } from '../../core';
6+
import { ValidationError, UnscopedValidationError } from '../../core/lib/errors';
7+
import { addConstructMetadata } from '../../core/lib/metadata-resource';
8+
import { propertyInjectable } from '../../core/lib/prop-injectable';
9+
10+
const ELASTICACHE_USERGROUP_SYMBOL = Symbol.for('@aws-cdk/aws-elasticache.UserGroup');
11+
12+
/**
13+
* Properties for defining an ElastiCache UserGroup
14+
*/
15+
export interface UserGroupProps {
16+
/**
17+
* Enforces a particular physical user group name.
18+
* @default <generated>
19+
*/
20+
readonly userGroupName?: string;
21+
/**
22+
* The engine type for the user group
23+
* Enum options: UserEngine.VALKEY, UserEngine.REDIS
24+
*
25+
* @default UserEngine.VALKEY
26+
*/
27+
readonly engine?: UserEngine;
28+
/**
29+
* List of users inside the user group
30+
*
31+
* @default - no users
32+
*/
33+
readonly users?: IUser[];
34+
}
35+
36+
/**
37+
* Represents an ElastiCache UserGroup
38+
*/
39+
export interface IUserGroup extends IResource {
40+
/**
41+
* The name of the user group
42+
*
43+
* @attribute
44+
*/
45+
readonly userGroupName: string;
46+
/**
47+
* The engine type for the user group
48+
*/
49+
readonly engine?: UserEngine;
50+
/**
51+
* List of users in the user group
52+
*/
53+
readonly users?: IUser[];
54+
/**
55+
* The ARN of the user group
56+
* @attribute
57+
*/
58+
readonly userGroupArn: string;
59+
/**
60+
* Add a user to this user group
61+
*
62+
* @param user The user to add
63+
*/
64+
addUser(user: IUser): void;
65+
}
66+
67+
/**
68+
* Base class for UserGroup constructs
69+
*/
70+
export abstract class UserGroupBase extends Resource implements IUserGroup {
71+
/**
72+
* The name of the user group
73+
*
74+
* @attribute
75+
*/
76+
public abstract readonly userGroupName: string;
77+
/**
78+
* The engine type for the user group
79+
*/
80+
public abstract readonly engine?: UserEngine;
81+
/**
82+
* List of users in the user group
83+
*/
84+
public abstract readonly users?: IUser[];
85+
/**
86+
* The ARN of the user group
87+
* @attribute
88+
*/
89+
public abstract readonly userGroupArn: string;
90+
/**
91+
* Add a user to this user group
92+
*
93+
* @param _user The user to add
94+
*/
95+
public addUser(_user: IUser): void {
96+
throw new UnscopedValidationError('Cannot add users to an imported UserGroup. Only UserGroups created in this stack can be modified.');
97+
}
98+
}
99+
100+
/**
101+
* Attributes that can be specified when importing a UserGroup
102+
*/
103+
export interface UserGroupAttributes {
104+
/**
105+
* The name of the user group
106+
*
107+
* One of `userGroupName` or `userGroupArn` is required.
108+
*
109+
* @default - derived from userGroupArn
110+
*/
111+
readonly userGroupName?: string;
112+
/**
113+
* The engine type for the user group
114+
*
115+
* @default - engine type is unknown
116+
*/
117+
readonly engine?: UserEngine;
118+
/**
119+
* List of users in the user group
120+
*
121+
* @default - users are unknown
122+
*/
123+
readonly users?: IUser[];
124+
/**
125+
* The ARN of the user group
126+
*
127+
* One of `userGroupName` or `userGroupArn` is required.
128+
*
129+
* @default - derived from userGroupName
130+
*/
131+
readonly userGroupArn?: string;
132+
}
133+
134+
/**
135+
* An ElastiCache UserGroup
136+
*
137+
* @resource AWS::ElastiCache::UserGroup
138+
*/
139+
@propertyInjectable
140+
export class UserGroup extends UserGroupBase {
141+
/**
142+
* Uniquely identifies this class
143+
*/
144+
public static readonly PROPERTY_INJECTION_ID: string = 'aws-cdk-lib.aws-elasticache.UserGroup';
145+
146+
/**
147+
* Return whether the given object is a `UserGroup`
148+
*/
149+
public static isUserGroup(x: any) : x is UserGroup {
150+
return x !== null && typeof(x) === 'object' && ELASTICACHE_USERGROUP_SYMBOL in x;
151+
}
152+
153+
/**
154+
* Import an existing user group by name
155+
*
156+
* @param scope The parent creating construct (usually `this`)
157+
* @param id The construct's name
158+
* @param userGroupName The name of the existing user group
159+
*/
160+
public static fromUserGroupName(scope: Construct, id: string, userGroupName: string): IUserGroup {
161+
return UserGroup.fromUserGroupAttributes(scope, id, { userGroupName });
162+
}
163+
164+
/**
165+
* Import an existing user group by ARN
166+
*
167+
* @param scope The parent creating construct (usually `this`)
168+
* @param id The construct's name
169+
* @param userGroupArn The ARN of the existing user group
170+
*/
171+
public static fromUserGroupArn(scope: Construct, id: string, userGroupArn: string): IUserGroup {
172+
return UserGroup.fromUserGroupAttributes(scope, id, { userGroupArn });
173+
}
174+
175+
/**
176+
* Import an existing user group using attributes
177+
*
178+
* @param scope The parent creating construct (usually `this`)
179+
* @param id The construct's name
180+
* @param attrs A `UserGroupAttributes` object
181+
*/
182+
public static fromUserGroupAttributes(scope: Construct, id: string, attrs: UserGroupAttributes): IUserGroup {
183+
let userGroupName: string;
184+
let userGroupArn: string;
185+
const stack = Stack.of(scope);
186+
187+
if (!attrs.userGroupName) {
188+
if (!attrs.userGroupArn) {
189+
throw new ValidationError('One of userGroupName or userGroupArn is required!', scope);
190+
}
191+
userGroupArn = attrs.userGroupArn;
192+
const maybeUserGroupName = stack.splitArn(attrs.userGroupArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName;
193+
if (!maybeUserGroupName) {
194+
throw new ValidationError('Unable to extract user group name from ARN', scope);
195+
}
196+
userGroupName = maybeUserGroupName;
197+
} else {
198+
if (attrs.userGroupArn) {
199+
throw new ValidationError('Only one of userGroupArn or userGroupName can be provided', scope);
200+
}
201+
userGroupName = attrs.userGroupName;
202+
userGroupArn = stack.formatArn({
203+
service: 'elasticache',
204+
resource: 'usergroup',
205+
resourceName: attrs.userGroupName,
206+
});
207+
}
208+
209+
class Import extends UserGroupBase {
210+
public readonly engine?: UserEngine;
211+
public readonly userGroupName: string;
212+
public readonly userGroupArn: string;
213+
214+
public get users(): IUser[] | undefined {
215+
return attrs.users ? [...attrs.users] : undefined;
216+
}
217+
218+
constructor(_userGroupArn: string, _userGroupName: string) {
219+
super(scope, id);
220+
this.userGroupArn = _userGroupArn;
221+
this.userGroupName = _userGroupName;
222+
this.engine = attrs.engine;
223+
}
224+
}
225+
226+
return new Import(userGroupArn, userGroupName);
227+
}
228+
229+
public readonly engine?: UserEngine;
230+
public readonly userGroupName: string;
231+
private readonly _users: IUser[] = [];
232+
/**
233+
* The ARN of the user group
234+
* @attribute
235+
*/
236+
public readonly userGroupArn: string;
237+
/**
238+
* The status of the user group
239+
* Can be 'creating', 'active', 'modifying', 'deleting'
240+
*
241+
* @attribute
242+
*/
243+
public readonly userGroupStatus: string;
244+
245+
private readonly resource: CfnUserGroup;
246+
247+
constructor(scope: Construct, id: string, props: UserGroupProps = {}) {
248+
super(scope, id, {
249+
physicalName: props.userGroupName,
250+
});
251+
252+
// Enhanced CDK Analytics Telemetry
253+
addConstructMetadata(this, props);
254+
255+
this.engine = props.engine ?? UserEngine.VALKEY;
256+
this.userGroupName = this.physicalName;
257+
258+
if (props.users) {
259+
this._users.push(...props.users);
260+
}
261+
262+
if (this.engine === UserEngine.REDIS) {
263+
this._users.forEach(user => {
264+
if (user.engine !== UserEngine.REDIS) {
265+
throw new ValidationError(`Redis user group can only contain Redis users. User ${user.userId} has engine ${user.engine}.`, this);
266+
}
267+
});
268+
}
269+
270+
this.resource = new CfnUserGroup(this, 'Resource', {
271+
engine: this.engine,
272+
userGroupId: this.physicalName,
273+
userIds: Lazy.list({
274+
produce: () => {
275+
if (this.engine === UserEngine.REDIS) {
276+
const hasDefaultUser = this._users.some(user => user.userName === 'default');
277+
if (!hasDefaultUser) {
278+
throw new ValidationError('Redis user groups need to contain a user with the user name "default".', this);
279+
}
280+
}
281+
282+
return this._users.map(user => user.userId);
283+
},
284+
}),
285+
});
286+
287+
if (props.users) {
288+
props.users.forEach(user => this.addUserDependency(user));
289+
}
290+
291+
this.userGroupArn = this.resource.attrArn;
292+
this.userGroupStatus = this.resource.attrStatus;
293+
294+
Object.defineProperty(this, ELASTICACHE_USERGROUP_SYMBOL, { value: true });
295+
}
296+
297+
private addUserDependency(user: IUser): void {
298+
const userResource = user.node.tryFindChild('Resource') as CfnUser;
299+
if (userResource) {
300+
this.resource.addDependency(userResource);
301+
}
302+
}
303+
304+
/**
305+
* Array of users in the user group
306+
*
307+
* Do not push directly to this array.
308+
* Use addUser() instead to ensure proper validation and dependency management.
309+
*/
310+
public get users(): IUser[] | undefined {
311+
return this._users;
312+
}
313+
314+
/**
315+
* Add a user to this user group
316+
*
317+
* @param user The user to add to the group
318+
*/
319+
public addUser(user: IUser): void {
320+
if (this._users.find(u => u.userId === user.userId)) {
321+
return;
322+
}
323+
if (this.engine === UserEngine.REDIS && user.engine !== UserEngine.REDIS) {
324+
throw new ValidationError(`Redis user group can only contain Redis users. User ${user.userId} has engine ${user.engine}.`, this);
325+
}
326+
this._users.push(user);
327+
this.addUserDependency(user);
328+
}
329+
}

0 commit comments

Comments
 (0)