diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/SecurityGroupsInVpcValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/SecurityGroupsInVpcValidator.cs
index 290a495e6..b26f8b374 100644
--- a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/SecurityGroupsInVpcValidator.cs
+++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/SecurityGroupsInVpcValidator.cs
@@ -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.";
+
+ ///
+ /// Path to the OptionSetting that stores a selected Vpc Id
+ ///
public string VpcId { get; set; } = "";
+
+ ///
+ /// Path to the OptionSetting that determines if the default VPC should be used
+ ///
+ public string IsDefaultVpcOptionSettingId { get; set; } = "";
+
public string ValidationFailedMessage { get; set; } = defaultValidationFailedMessage;
private readonly IAWSResourceQueryer _awsResourceQueryer;
@@ -32,18 +42,77 @@ public async Task 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(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(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(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>(out var inputList) ?? false)
{
+ var invalidSecurityGroups = new List();
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();
+
+ 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();
diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe
index 3516c7a77..5d719ef5e 100644
--- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe
+++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe
@@ -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"
+ }
}
]
},
diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe
index a53c91ca2..3a7a51c79 100644
--- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe
+++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe
@@ -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",
diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs
index cdf381939..fba71ba55 100644
--- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs
+++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs
@@ -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
{
@@ -292,6 +293,48 @@ public async Task DockerExecutionDirectory_AbsoluteDoesNotExist()
await Validate(optionSettingItem, Path.Join("C:", "other_project"), false);
}
+ ///
+ /// Tests the relationship between an explicit VPC ID, whether "Default VPC" is checked,
+ /// and any security groups that are specified.
+ ///
+ /// selected VPC Id
+ /// whether the "Default VPC" radio is selected
+ /// selected security groups
+ /// Whether or not the test case is expected to be valid
+ [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
@@ -387,5 +430,69 @@ private async Task Validate(OptionSettingItem optionSettingItem, T value, boo
else
exception.ShouldNotBeNull();
}
+
+ ///
+ /// Prepares a for testing
+ ///
+ private OptionSettingItemValidatorConfig GetSecurityGroupsInVpcValidatorConfig(Mock 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
+ };
+ }
+
+ ///
+ /// Mocks the provided 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
+ ///
+ /// Mocked AWS Resource Queryer
+ private void PrepareMockVPCsAndSecurityGroups(Mock awsResourceQueryer)
+ {
+ awsResourceQueryer.Setup(x => x.GetListOfVpcs()).ReturnsAsync(
+ new List {
+ new Vpc { VpcId = "vpc1", IsDefault = true },
+ new Vpc { VpcId = "vpc2"}
+ });
+
+ awsResourceQueryer.Setup(x => x.DescribeSecurityGroups("vpc1")).ReturnsAsync(
+ new List {
+ new SecurityGroup { GroupId = "sg-1a", VpcId = "vpc1" },
+ new SecurityGroup { GroupId = "sg-1b", VpcId = "vpc1" }
+ });
+
+ awsResourceQueryer.Setup(x => x.DescribeSecurityGroups("vpc2")).ReturnsAsync(
+ new List {
+ 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 });
+ }
+
+ ///
+ /// Prepares VPC-related options that match the ECS Fargate recipes for testing
+ ///
+ /// The "Vpc.VpcId" option, the "Vpc.IsDefault" option, and the "ECSServiceSecurityGroups" option
+ 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);
+ }
}
}