Skip to content

Commit

Permalink
Merge branch 'main' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
philasmar committed Jan 5, 2023
2 parents 2205b98 + 7bf9602 commit 8d10f56
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@ namespace AWS.Deploy.Common.Recipes.Validation
public class SecurityGroupsInVpcValidator : IOptionSettingItemValidator
{
private static readonly string defaultValidationFailedMessage = "The selected security groups are not part of the selected VPC.";

/// <summary>
/// Path to the OptionSetting that stores a selected Vpc Id
/// </summary>
public string VpcId { get; set; } = "";

/// <summary>
/// Path to the OptionSetting that determines if the default VPC should be used
/// </summary>
public string IsDefaultVpcOptionSettingId { get; set; } = "";

public string ValidationFailedMessage { get; set; } = defaultValidationFailedMessage;

private readonly IAWSResourceQueryer _awsResourceQueryer;
Expand All @@ -32,18 +42,77 @@ public async Task<ValidationResult> Validate(object input, Recommendation recomm
{
if (string.IsNullOrEmpty(VpcId))
return ValidationResult.Failed($"The '{nameof(SecurityGroupsInVpcValidator)}' validator is missing the '{nameof(VpcId)}' configuration.");
var vpcIdSetting = _optionSettingHandler.GetOptionSetting(recommendation, VpcId);
var vpcId = _optionSettingHandler.GetOptionSettingValue<string>(recommendation, vpcIdSetting);

var vpcId = "";

// The ECS Fargate recipes expose a separate radio button to select the default VPC which is mutually exclusive
// with specifying an explicit VPC Id. Because we give preference to "UseDefault" in the CDK project,
// we should do so here as well and validate the security groups against the default VPC if it's selected.
if (!string.IsNullOrEmpty(IsDefaultVpcOptionSettingId))
{
var isDefaultVpcOptionSetting = _optionSettingHandler.GetOptionSetting(recommendation, IsDefaultVpcOptionSettingId);
var shouldUseDefaultVpc = _optionSettingHandler.GetOptionSettingValue<bool>(recommendation, isDefaultVpcOptionSetting);

if (shouldUseDefaultVpc)
{
vpcId = (await _awsResourceQueryer.GetDefaultVpc()).VpcId;
}
}

// If the "Use default?" option doesn't exist in the recipe, or it does and was false, or
// we failed to look up the default VPC, then use the explicity VPC Id
if (string.IsNullOrEmpty(vpcId))
{
var vpcIdSetting = _optionSettingHandler.GetOptionSetting(recommendation, VpcId);
vpcId = _optionSettingHandler.GetOptionSettingValue<string>(recommendation, vpcIdSetting);
}

if (string.IsNullOrEmpty(vpcId))
return ValidationResult.Failed("The VpcId setting is not set or is empty. Make sure to set the VPC Id first.");

var securityGroupIds = (await _awsResourceQueryer.DescribeSecurityGroups(vpcId)).Select(x => x.GroupId);

// The ASP.NET Fargate recipe uses a list of security groups
if (input?.TryDeserialize<SortedSet<string>>(out var inputList) ?? false)
{
var invalidSecurityGroups = new List<string>();
foreach (var securityGroup in inputList!)
{
if (!securityGroupIds.Contains(securityGroup))
return ValidationResult.Failed("The selected security group(s) are invalid since they do not belong to the currently selected VPC.");
invalidSecurityGroups.Add(securityGroup);
}

if (invalidSecurityGroups.Any())
{
return ValidationResult.Failed($"The selected security group(s) ({string.Join(", ", invalidSecurityGroups)}) " +
$"are invalid since they do not belong to the currently selected VPC {vpcId}.");
}

return ValidationResult.Valid();
}

// The Console ECS Fargate Service recipe uses a comma-separated string, which will fall through the TryDeserialize above
if (input is string)
{
// Security groups aren't required
if (string.IsNullOrEmpty(input.ToString()))
{
return ValidationResult.Valid();
}

var securityGroupList = input.ToString()?.Split(',') ?? new string[0];
var invalidSecurityGroups = new List<string>();

foreach (var securityGroup in securityGroupList)
{
if (!securityGroupIds.Contains(securityGroup))
invalidSecurityGroups.Add(securityGroup);
}

if (invalidSecurityGroups.Any())
{
return ValidationResult.Failed($"The selected security group(s) ({string.Join(", ", invalidSecurityGroups)}) " +
$"are invalid since they do not belong to the currently selected VPC {vpcId}.");
}

return ValidationResult.Valid();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,13 @@
"AllowEmptyString": true,
"ValidationFailedMessage": "Invalid Security Group ID. The Security Group ID must start with the \"sg-\" prefix, followed by either 8 or 17 characters consisting of digits and letters(lower-case) from a to f. For example sg-abc88de9 is a valid Security Group ID."
}
},
{
"ValidatorType": "SecurityGroupsInVpc",
"Configuration": {
"VpcId": "Vpc.VpcId",
"IsDefaultVpcOptionSettingId": "Vpc.IsDefault"
}
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,16 @@
"Type": "String",
"DefaultValue": "",
"AdvancedSetting": true,
"Updatable": true
"Updatable": true,
"Validators": [
{
"ValidatorType": "SecurityGroupsInVpc",
"Configuration": {
"VpcId": "Vpc.VpcId",
"IsDefaultVpcOptionSettingId": "Vpc.IsDefault"
}
}
]
},
{
"Id": "TaskCpu",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using ResourceNotFoundException = Amazon.CloudControlApi.Model.ResourceNotFoundException;
using Task = System.Threading.Tasks.Task;
using System.Collections.Generic;
using Amazon.EC2.Model;

namespace AWS.Deploy.CLI.Common.UnitTests.Recipes.Validation
{
Expand Down Expand Up @@ -292,6 +293,48 @@ public async Task DockerExecutionDirectory_AbsoluteDoesNotExist()
await Validate(optionSettingItem, Path.Join("C:", "other_project"), false);
}

/// <summary>
/// Tests the relationship between an explicit VPC ID, whether "Default VPC" is checked,
/// and any security groups that are specified.
/// </summary>
/// <param name="vpcId">selected VPC Id</param>
/// <param name="isDefaultVpcSelected">whether the "Default VPC" radio is selected</param>
/// <param name="selectedSecurityGroups">selected security groups</param>
/// <param name="isValid">Whether or not the test case is expected to be valid</param>
[Theory]
// The Console Service recipe uses a comma-seperated string of security groups
[InlineData("vpc1", true, "", true)] // Valid because the security groups are optional
[InlineData("vpc1", true, "sg-1a,sg-1b", true)] // Valid because the security group does belong to the default VPC
[InlineData("vpc1", true, "sg-1a,sg-2a", false)] // Invalid because the security group does not belong to the default VPC
[InlineData("vpc2", false, "sg-2a", true)] // Valid because the security group does belong to the non-default VPC
[InlineData("vpc2", false, "sg-1a", false)] // Invalid because the security group does not belong to the non-default VPC
[InlineData("vpc2", true, "sg-1a", true)] // Valid because "true" for IsDefaultVPC overrides the "vpc2", so the security group matches
[InlineData("vpc2", true, "sg-2a", false)] // Invalid because "true" for IsDefaultVPC overrides the "vpc2", so the security group does not match
//
// The ASP.NET on Fargate recipe uses a JSON list of security groups (these are same cases from above)
//
[InlineData("vpc1", true, "[]", true)]
[InlineData("vpc1", true, "[\"sg-1a\",\"sg-1b\"]", true)]
[InlineData("vpc1", true, "[\"sg-1a\",\"sg-2a\"]", false)]
[InlineData("vpc2", false, "[\"sg-2a\"]", true)]
[InlineData("vpc2", false, "[\"sg-1a\"]", false)]
[InlineData("vpc2", true, "[\"sg-1a\"]", true)]
[InlineData("vpc2", true, "[\"sg-2a\"]", false)]

public async Task VpcId_DefaultVpc_SecurityGroups_Relationship(string vpcId, bool isDefaultVpcSelected, object selectedSecurityGroups, bool isValid)
{
PrepareMockVPCsAndSecurityGroups(_awsResourceQueryer);

var (vpcIdOption, vpcDefaultOption, securityGroupsOption) = PrepareECSVpcOptions();

securityGroupsOption.Validators.Add(GetSecurityGroupsInVpcValidatorConfig(_awsResourceQueryer, _optionSettingHandler));

await _optionSettingHandler.SetOptionSettingValue(_recommendation, vpcIdOption, vpcId);
await _optionSettingHandler.SetOptionSettingValue(_recommendation, vpcDefaultOption, isDefaultVpcSelected);

await Validate(securityGroupsOption, selectedSecurityGroups, isValid);
}

private OptionSettingItemValidatorConfig GetRegexValidatorConfig(string regex)
{
var regexValidatorConfig = new OptionSettingItemValidatorConfig
Expand Down Expand Up @@ -387,5 +430,69 @@ private async Task Validate<T>(OptionSettingItem optionSettingItem, T value, boo
else
exception.ShouldNotBeNull();
}

/// <summary>
/// Prepares a <see cref="SecurityGroupsInVpcValidator"/> for testing
/// </summary>
private OptionSettingItemValidatorConfig GetSecurityGroupsInVpcValidatorConfig(Mock<IAWSResourceQueryer> awsResourceQueryer, IOptionSettingHandler optionSettingHandler)
{
var validator = new SecurityGroupsInVpcValidator(awsResourceQueryer.Object, optionSettingHandler);
validator.VpcId = "Vpc.VpcId";
validator.IsDefaultVpcOptionSettingId = "Vpc.IsDefault";

return new OptionSettingItemValidatorConfig
{
ValidatorType = OptionSettingItemValidatorList.SecurityGroupsInVpc,
Configuration = validator
};
}

/// <summary>
/// Mocks the provided <see cref="IAWSResourceQueryer"> to return the following
/// 1. Default vpc1 with security groups sg-1a and sg-1b
/// 2. Non-default vpc2 with security groups sg-2a and sg-2b
/// </summary>
/// <param name="awsResourceQueryer">Mocked AWS Resource Queryer</param>
private void PrepareMockVPCsAndSecurityGroups(Mock<IAWSResourceQueryer> awsResourceQueryer)
{
awsResourceQueryer.Setup(x => x.GetListOfVpcs()).ReturnsAsync(
new List<Vpc> {
new Vpc { VpcId = "vpc1", IsDefault = true },
new Vpc { VpcId = "vpc2"}
});

awsResourceQueryer.Setup(x => x.DescribeSecurityGroups("vpc1")).ReturnsAsync(
new List<SecurityGroup> {
new SecurityGroup { GroupId = "sg-1a", VpcId = "vpc1" },
new SecurityGroup { GroupId = "sg-1b", VpcId = "vpc1" }
});

awsResourceQueryer.Setup(x => x.DescribeSecurityGroups("vpc2")).ReturnsAsync(
new List<SecurityGroup> {
new SecurityGroup { GroupId = "sg-2a", VpcId = "vpc2" },
new SecurityGroup { GroupId = "sg-2a", VpcId = "vpc2" }
});

awsResourceQueryer.Setup(x => x.GetDefaultVpc()).ReturnsAsync(new Vpc { VpcId = "vpc1", IsDefault = true });
}

/// <summary>
/// Prepares VPC-related options that match the ECS Fargate recipes for testing
/// </summary>
/// <returns>The "Vpc.VpcId" option, the "Vpc.IsDefault" option, and the "ECSServiceSecurityGroups" option</returns>
private (OptionSettingItem, OptionSettingItem, OptionSettingItem) PrepareECSVpcOptions()
{
var vpcIdOption = new OptionSettingItem("VpcId", "Vpc.VpcId", "name", "description");
var vpcDefaultOption = new OptionSettingItem("IsDefault", "Vpc.IsDefault", "name", "description");
var ecsServiceSecurityGroupsOption = new OptionSettingItem("ECSServiceSecurityGroups", "ECSServiceSecurityGroups", "name", "");

var vpc = new OptionSettingItem("Vpc", "Vpc", "", "");
vpc.ChildOptionSettings.Add(vpcIdOption);
vpc.ChildOptionSettings.Add(vpcDefaultOption);

_recipe.OptionSettings.Add(vpc);

return (vpcIdOption, vpcDefaultOption, ecsServiceSecurityGroupsOption);
}
}
}

0 comments on commit 8d10f56

Please sign in to comment.