Skip to content

Commit

Permalink
Merge branch 'master' into prod_stack
Browse files Browse the repository at this point in the history
  • Loading branch information
mergify[bot] authored Nov 2, 2021
2 parents 4502669 + d4952c3 commit 96e9689
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 73 deletions.
152 changes: 80 additions & 72 deletions packages/@aws-cdk/aws-ec2/lib/security-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ abstract class SecurityGroupBase extends Resource implements ISecurityGroup {
public readonly connections: Connections = new Connections({ securityGroups: [this] });
public readonly defaultPort?: Port;

private peerAsTokenCount: number = 0;

constructor(scope: Construct, id: string, props?: ResourceProps) {
super(scope, id, props);

Expand All @@ -83,7 +85,7 @@ abstract class SecurityGroupBase extends Resource implements ISecurityGroup {
description = `from ${peer.uniqueId}:${connection}`;
}

const [scope, id] = determineRuleScope(this, peer, connection, 'from', remoteRule);
const [scope, id] = this.determineRuleScope(peer, connection, 'from', remoteRule);

// Skip duplicates
if (scope.node.tryFindChild(id) === undefined) {
Expand All @@ -101,7 +103,7 @@ abstract class SecurityGroupBase extends Resource implements ISecurityGroup {
description = `to ${peer.uniqueId}:${connection}`;
}

const [scope, id] = determineRuleScope(this, peer, connection, 'to', remoteRule);
const [scope, id] = this.determineRuleScope(peer, connection, 'to', remoteRule);

// Skip duplicates
if (scope.node.tryFindChild(id) === undefined) {
Expand All @@ -121,75 +123,82 @@ abstract class SecurityGroupBase extends Resource implements ISecurityGroup {
public toEgressRuleConfig(): any {
return { destinationSecurityGroupId: this.securityGroupId };
}
}

/**
* Determine where to parent a new ingress/egress rule
*
* A SecurityGroup rule is parented under the group it's related to, UNLESS
* we're in a cross-stack scenario with another Security Group. In that case,
* we respect the 'remoteRule' flag and will parent under the other security
* group.
*
* This is necessary to avoid cyclic dependencies between stacks, since both
* ingress and egress rules will reference both security groups, and a naive
* parenting will lead to the following situation:
*
* ╔════════════════════╗ ╔════════════════════╗
* ║ ┌───────────┐ ║ ║ ┌───────────┐ ║
* ║ │ GroupA │◀────╬─┐ ┌───╬───▶│ GroupB │ ║
* ║ └───────────┘ ║ │ │ ║ └───────────┘ ║
* ║ ▲ ║ │ │ ║ ▲ ║
* ║ │ ║ │ │ ║ │ ║
* ║ │ ║ │ │ ║ │ ║
* ║ ┌───────────┐ ║ └───┼───╬────┌───────────┐ ║
* ║ │ EgressA │─────╬─────┘ ║ │ IngressB │ ║
* ║ └───────────┘ ║ ║ └───────────┘ ║
* ║ ║ ║ ║
* ╚════════════════════╝ ╚════════════════════╝
*
* By having the ability to switch the parent, we avoid the cyclic reference by
* keeping all rules in a single stack.
*
* If this happens, we also have to change the construct ID, because
* otherwise we might have two objects with the same ID if we have
* multiple reversed security group relationships.
*
* ╔═══════════════════════════════════╗
* ║┌───────────┐ ║
* ║│ GroupB │ ║
* ║└───────────┘ ║
* ║ ▲ ║
* ║ │ ┌───────────┐ ║
* ║ ├────"from A"──│ IngressB │ ║
* ║ │ └───────────┘ ║
* ║ │ ┌───────────┐ ║
* ║ ├─────"to B"───│ EgressA │ ║
* ║ │ └───────────┘ ║
* ║ │ ┌───────────┐ ║
* ║ └─────"to B"───│ EgressC │ ║ <-- oops
* ║ └───────────┘ ║
* ╚═══════════════════════════════════╝
*/
function determineRuleScope(
group: SecurityGroupBase,
peer: IPeer,
connection: Port,
fromTo: 'from' | 'to',
remoteRule?: boolean): [SecurityGroupBase, string] {

if (remoteRule && SecurityGroupBase.isSecurityGroup(peer) && differentStacks(group, peer)) {
// Reversed
const reversedFromTo = fromTo === 'from' ? 'to' : 'from';
return [peer, `${group.uniqueId}:${connection} ${reversedFromTo}`];
} else {
// Regular (do old ID escaping to in order to not disturb existing deployments)
return [group, `${fromTo} ${renderPeer(peer)}:${connection}`.replace('/', '_')];
/**
* Determine where to parent a new ingress/egress rule
*
* A SecurityGroup rule is parented under the group it's related to, UNLESS
* we're in a cross-stack scenario with another Security Group. In that case,
* we respect the 'remoteRule' flag and will parent under the other security
* group.
*
* This is necessary to avoid cyclic dependencies between stacks, since both
* ingress and egress rules will reference both security groups, and a naive
* parenting will lead to the following situation:
*
* ╔════════════════════╗ ╔════════════════════╗
* ║ ┌───────────┐ ║ ║ ┌───────────┐ ║
* ║ │ GroupA │◀────╬─┐ ┌───╬───▶│ GroupB │ ║
* ║ └───────────┘ ║ │ │ ║ └───────────┘ ║
* ║ ▲ ║ │ │ ║ ▲ ║
* ║ │ ║ │ │ ║ │ ║
* ║ │ ║ │ │ ║ │ ║
* ║ ┌───────────┐ ║ └───┼───╬────┌───────────┐ ║
* ║ │ EgressA │─────╬─────┘ ║ │ IngressB │ ║
* ║ └───────────┘ ║ ║ └───────────┘ ║
* ║ ║ ║ ║
* ╚════════════════════╝ ╚════════════════════╝
*
* By having the ability to switch the parent, we avoid the cyclic reference by
* keeping all rules in a single stack.
*
* If this happens, we also have to change the construct ID, because
* otherwise we might have two objects with the same ID if we have
* multiple reversed security group relationships.
*
* ╔═══════════════════════════════════╗
* ║┌───────────┐ ║
* ║│ GroupB │ ║
* ║└───────────┘ ║
* ║ ▲ ║
* ║ │ ┌───────────┐ ║
* ║ ├────"from A"──│ IngressB │ ║
* ║ │ └───────────┘ ║
* ║ │ ┌───────────┐ ║
* ║ ├─────"to B"───│ EgressA │ ║
* ║ │ └───────────┘ ║
* ║ │ ┌───────────┐ ║
* ║ └─────"to B"───│ EgressC │ ║ <-- oops
* ║ └───────────┘ ║
* ╚═══════════════════════════════════╝
*/

protected determineRuleScope(
peer: IPeer,
connection: Port,
fromTo: 'from' | 'to',
remoteRule?: boolean): [SecurityGroupBase, string] {

if (remoteRule && SecurityGroupBase.isSecurityGroup(peer) && differentStacks(this, peer)) {
// Reversed
const reversedFromTo = fromTo === 'from' ? 'to' : 'from';
return [peer, `${this.uniqueId}:${connection} ${reversedFromTo}`];
} else {
// Regular (do old ID escaping to in order to not disturb existing deployments)
return [this, `${fromTo} ${this.renderPeer(peer)}:${connection}`.replace('/', '_')];
}
}
}

function renderPeer(peer: IPeer) {
return Token.isUnresolved(peer.uniqueId) ? '{IndirectPeer}' : peer.uniqueId;
private renderPeer(peer: IPeer) {
if (Token.isUnresolved(peer.uniqueId)) {
// Need to return a unique value each time a peer
// is an unresolved token, else the duplicate skipper
// in `sg.addXxxRule` can detect unique rules as duplicates
return this.peerAsTokenCount++ ? `'{IndirectPeer${this.peerAsTokenCount}}'` : '{IndirectPeer}';
} else {
return peer.uniqueId;
}
}
}

function differentStacks(group1: SecurityGroupBase, group2: SecurityGroupBase) {
Expand Down Expand Up @@ -565,13 +574,12 @@ export class SecurityGroup extends SecurityGroupBase {
*/
private removeNoTrafficRule() {
if (this.disableInlineRules) {
const [scope, id] = determineRuleScope(
this,
const [scope, id] = this.determineRuleScope(
NO_TRAFFIC_PEER,
NO_TRAFFIC_PORT,
'to',
false);

false,
);
scope.node.tryRemoveChild(id);
} else {
const i = this.directEgressRules.findIndex(r => egressRulesEqual(r, MATCH_NO_TRAFFIC));
Expand Down
24 changes: 24 additions & 0 deletions packages/@aws-cdk/aws-ec2/test/security-group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,30 @@ describe('security group', () => {

});

test('can add multiple rules using tokens on same security group', () => {
// GIVEN
const stack = new Stack(undefined, 'TestStack', { env: { account: '12345678', region: 'dummy' } });
const vpc = new Vpc(stack, 'VPC');
const sg = new SecurityGroup(stack, 'SG', { vpc });

const p1 = Lazy.string({ produce: () => 'dummyid1' });
const p2 = Lazy.string({ produce: () => 'dummyid2' });
const peer1 = Peer.prefixList(p1);
const peer2 = Peer.prefixList(p2);

// WHEN
sg.addIngressRule(peer1, Port.tcp(5432), 'Rule 1');
sg.addIngressRule(peer2, Port.tcp(5432), 'Rule 2');

// THEN -- no crash
expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroupIngress', {
Description: 'Rule 1',
});
expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroupIngress', {
Description: 'Rule 2',
});
});

test('if tokens are used in ports, `canInlineRule` should be false to avoid cycles', () => {
// GIVEN
const p1 = Lazy.number({ produce: () => 80 });
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export class CdkToolkit {
const long = [];
for (const stack of stacks.stackArtifacts) {
long.push({
id: stack.id,
id: stack.hierarchicalId,
name: stack.stackName,
environment: stack.environment,
});
Expand Down

0 comments on commit 96e9689

Please sign in to comment.