diff --git a/src/LocalStack.AwsLocal/CommandDispatcher.cs b/src/LocalStack.AwsLocal/CommandDispatcher.cs new file mode 100644 index 0000000..afa821e --- /dev/null +++ b/src/LocalStack.AwsLocal/CommandDispatcher.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using LocalStack.AwsLocal.Contracts; +using LocalStack.AwsLocal.EnvironmentContext; +using LocalStack.AwsLocal.Extensions; +using LocalStack.Client.Contracts; +using LocalStack.Client.Models; + +namespace LocalStack.AwsLocal +{ + public class CommandDispatcher + { + private const string UsageResource = "LocalStack.AwsLocal.Docs.Usage.txt"; + + private readonly IProcessHelper _processHelper; + private readonly IConfig _config; + private readonly TextWriter _textWriter; + private readonly string[] _args; + + private CommandDispatcher() + { + } + + public CommandDispatcher(IProcessHelper processHelper, IConfig config, TextWriter textWriter, string[] args) + { + _processHelper = processHelper; + _config = config; + _textWriter = textWriter; + _args = args; + } + + public void Run() + { + if (_args.Length == 0 || (_args[0] == "-h")) + { + string usageInfo = GetUsageInfo(); + _textWriter.WriteLine(usageInfo); + EnvironmentControl.Current.Exit(0); + return; + } + + string serviceName = _args.ExtractServiceName(); + + if (string.IsNullOrEmpty(serviceName)) + { + _textWriter.WriteLine("ERROR: Invalid argument, please enter a valid aws cli command"); + EnvironmentControl.Current.Exit(1); + return; + } + + AwsServiceEndpoint awsServiceEndpoint = _config.GetServiceEndpoint(serviceName); + + if (awsServiceEndpoint == null) + { + _textWriter.WriteLine($"ERROR: Unable to find LocalStack endpoint for service {serviceName}"); + EnvironmentControl.Current.Exit(1); + return; + } + + string cliCommand = _args.GetCliCommand(awsServiceEndpoint.ServiceUrl); + + string awsDefaultRegion = Environment.GetEnvironmentVariable("AWS_DEFAULT_REGION") ?? "us-east-1"; + string awsAccessKeyId = Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID") ?? "_not_needed_locally_"; + string awsSecretAccessKey = Environment.GetEnvironmentVariable("AWS_SECRET_ACCESS_KEY") ?? "_not_needed_locally_"; + + _processHelper.CmdExecute(cliCommand, null, true, true, new Dictionary + { + {"AWS_DEFAULT_REGION", awsDefaultRegion}, + {"AWS_ACCESS_KEY_ID", awsAccessKeyId}, + {"AWS_SECRET_ACCESS_KEY", awsSecretAccessKey} + }); + } + + private static string GetUsageInfo() + { + using (Stream stream = Assembly.GetCallingAssembly().GetManifestResourceStream(UsageResource)) + { + using (var reader = new StreamReader(stream)) + { + string result = reader.ReadToEnd(); + + return result; + } + } + } + } +} diff --git a/src/LocalStack.AwsLocal/Contracts/IProcessHelper.cs b/src/LocalStack.AwsLocal/Contracts/IProcessHelper.cs index 1b2d40b..ab89023 100644 --- a/src/LocalStack.AwsLocal/Contracts/IProcessHelper.cs +++ b/src/LocalStack.AwsLocal/Contracts/IProcessHelper.cs @@ -2,7 +2,7 @@ namespace LocalStack.AwsLocal.Contracts { - internal interface IProcessHelper + public interface IProcessHelper { int CmdExecute(string command, string workingDirectoryPath, bool output = true, bool waitForExit = true, IDictionary environmentVariables = null); } diff --git a/src/LocalStack.AwsLocal/EnvironmentContext/DefaultEnvironmentControl.cs b/src/LocalStack.AwsLocal/EnvironmentContext/DefaultEnvironmentControl.cs new file mode 100644 index 0000000..b75e8af --- /dev/null +++ b/src/LocalStack.AwsLocal/EnvironmentContext/DefaultEnvironmentControl.cs @@ -0,0 +1,16 @@ +using System; + +namespace LocalStack.AwsLocal.EnvironmentContext +{ + public class DefaultEnvironmentControl : EnvironmentControl + { + private static readonly Lazy LazyInstance = new Lazy(() => new DefaultEnvironmentControl()); + + public override void Exit(int value) + { + Environment.Exit(value); + } + + public static DefaultEnvironmentControl Instance => LazyInstance.Value; + } +} \ No newline at end of file diff --git a/src/LocalStack.AwsLocal/EnvironmentContext/EnvironmentControl.cs b/src/LocalStack.AwsLocal/EnvironmentContext/EnvironmentControl.cs new file mode 100644 index 0000000..2c2f36a --- /dev/null +++ b/src/LocalStack.AwsLocal/EnvironmentContext/EnvironmentControl.cs @@ -0,0 +1,23 @@ +using System; + +namespace LocalStack.AwsLocal.EnvironmentContext +{ + public abstract class EnvironmentControl + { + private static EnvironmentControl _current = DefaultEnvironmentControl.Instance; + + public static EnvironmentControl Current + { + get => _current; + + set => _current = value ?? throw new ArgumentNullException(nameof(value)); + } + + public abstract void Exit(int value); + + public static void ResetToDefault() + { + _current = DefaultEnvironmentControl.Instance; + } + } +} diff --git a/src/LocalStack.AwsLocal/Extensions/ArgumentExtensions.cs b/src/LocalStack.AwsLocal/Extensions/ArgumentExtensions.cs new file mode 100644 index 0000000..32e95c6 --- /dev/null +++ b/src/LocalStack.AwsLocal/Extensions/ArgumentExtensions.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; + +namespace LocalStack.AwsLocal.Extensions +{ + public static class ArgumentExtensions + { + public static string ExtractServiceName(this IEnumerable args) + { + foreach (string arg in args) + { + if (arg.StartsWith('-')) + { + continue; + } + + return arg == "s3api" ? "s3" : arg; + } + + return string.Empty; + } + + public static string GetCliCommand(this IEnumerable args, string serviceUrl) + { + var arguments = args.ToList(); + arguments.Insert(0, "aws"); + arguments.Insert(1, $"--endpoint-url={serviceUrl}"); + + if (serviceUrl.StartsWith("https")) + { + arguments.Insert(2, "--no-verify-ssl"); + } + + return string.Join(' ', arguments); + } + } +} diff --git a/src/LocalStack.AwsLocal/Extensions/ConfigExtensions.cs b/src/LocalStack.AwsLocal/Extensions/ConfigExtensions.cs new file mode 100644 index 0000000..874c245 --- /dev/null +++ b/src/LocalStack.AwsLocal/Extensions/ConfigExtensions.cs @@ -0,0 +1,16 @@ +using System.Linq; +using LocalStack.Client.Contracts; +using LocalStack.Client.Models; + +namespace LocalStack.AwsLocal.Extensions +{ + public static class ConfigExtensions + { + public static AwsServiceEndpoint GetServiceEndpoint(this IConfig config, string serviceName) + { + var awsServiceEndpoints = config.GetAwsServiceEndpoints(); + + return awsServiceEndpoints.SingleOrDefault(endpoint => endpoint.CliName == serviceName); + } + } +} diff --git a/src/LocalStack.AwsLocal/Program.cs b/src/LocalStack.AwsLocal/Program.cs index a4ad907..5d33c8f 100644 --- a/src/LocalStack.AwsLocal/Program.cs +++ b/src/LocalStack.AwsLocal/Program.cs @@ -1,100 +1,21 @@ -using LocalStack.AwsLocal.Contracts; -using LocalStack.Client; -using LocalStack.Client.Contracts; -using LocalStack.Client.Models; +using LocalStack.Client; using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; namespace LocalStack.AwsLocal { internal static class Program { - private const string UsageResource = "LocalStack.AwsLocal.Docs.Usage.txt"; - - private static readonly IProcessHelper ProcessHelper = new ProcessHelper(); private static readonly string LocalStackHost = Environment.GetEnvironmentVariable("LOCALSTACK_HOST"); - private static readonly IConfig Config = new Config(LocalStackHost); - - private static IEnumerable Args { get; set; } private static void Main(string[] args) { - Args = args; - - if (args.Length == 0 || (args[0] == "-h")) - { - Usage(); - } - - (string service, AwsServiceEndpoint awsServiceEndpoint) = GetServiceEndpoint(); - - if (awsServiceEndpoint == null) - { - Console.WriteLine($"ERROR: Unable to find LocalStack endpoint for service {service}"); - Environment.Exit(1); - } - - var arguments = args.ToList(); - arguments.Insert(0, "aws"); - arguments.Insert(1, $"--endpoint-url={awsServiceEndpoint.ServiceUrl}"); - - if (awsServiceEndpoint.Host.Contains("https")) - { - arguments.Insert(2, "--no-verify-ssl"); - } - - string awsDefaultRegion = Environment.GetEnvironmentVariable("AWS_DEFAULT_REGION") ?? "us-east-1"; - string awsAccessKeyId = Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID") ?? "_not_needed_locally_"; - string awsSecretAccessKey = Environment.GetEnvironmentVariable("AWS_SECRET_ACCESS_KEY") ?? "_not_needed_locally_"; - - ProcessHelper.CmdExecute(string.Join(' ', arguments), null, true, true, new Dictionary - { - {"AWS_DEFAULT_REGION", awsDefaultRegion}, - {"AWS_ACCESS_KEY_ID", awsAccessKeyId}, - {"AWS_SECRET_ACCESS_KEY", awsSecretAccessKey} - }); - } + var processHelper = new ProcessHelper(); + var config = new Config(LocalStackHost); + var textWriter = Console.Out; - private static string GetService() - { - foreach (string arg in Args) - { - if (!arg.StartsWith('-')) - { - return arg; - } - } - - return string.Empty; - } - - private static (string service, AwsServiceEndpoint awsServiceEndpoint) GetServiceEndpoint() - { - string service = GetService(); - if (service == "s3api") - { - service = "s3"; - } - - var awsServiceEndpoints = Config.GetAwsServiceEndpoints(); - return (service, awsServiceEndpoints.SingleOrDefault(endpoint => endpoint.CliName == service)); - } - - private static void Usage() - { - using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(UsageResource)) - { - using (var reader = new StreamReader(stream)) - { - string result = reader.ReadToEnd(); - Console.WriteLine(result); - } - } + var commandDispatcher = new CommandDispatcher(processHelper, config, textWriter, args); - Environment.Exit(0); + commandDispatcher.Run(); } } } \ No newline at end of file diff --git a/src/LocalStack.sln b/src/LocalStack.sln index c9138cf..21ea1c9 100644 --- a/src/LocalStack.sln +++ b/src/LocalStack.sln @@ -5,6 +5,10 @@ VisualStudioVersion = 16.0.28803.452 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.AwsLocal", "LocalStack.AwsLocal\LocalStack.AwsLocal.csproj", "{D5116356-24F8-4B01-AB33-64CCFC5F0713}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{D9F1CEF9-248C-49EA-BD93-5B5E6FD02967}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalStack.AwsLocal.Tests", "..\tests\LocalStack.AwsLocal.Tests\LocalStack.AwsLocal.Tests.csproj", "{5D6C622C-F940-4851-BFD9-B2755641C6D5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,10 +19,17 @@ Global {D5116356-24F8-4B01-AB33-64CCFC5F0713}.Debug|Any CPU.Build.0 = Debug|Any CPU {D5116356-24F8-4B01-AB33-64CCFC5F0713}.Release|Any CPU.ActiveCfg = Release|Any CPU {D5116356-24F8-4B01-AB33-64CCFC5F0713}.Release|Any CPU.Build.0 = Release|Any CPU + {5D6C622C-F940-4851-BFD9-B2755641C6D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D6C622C-F940-4851-BFD9-B2755641C6D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D6C622C-F940-4851-BFD9-B2755641C6D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D6C622C-F940-4851-BFD9-B2755641C6D5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {5D6C622C-F940-4851-BFD9-B2755641C6D5} = {D9F1CEF9-248C-49EA-BD93-5B5E6FD02967} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {479893D2-EC31-4B10-8F7C-DDA704071C4D} EndGlobalSection diff --git a/tests/LocalStack.AwsLocal.Tests/ArgumentExtensionsTests.cs b/tests/LocalStack.AwsLocal.Tests/ArgumentExtensionsTests.cs new file mode 100644 index 0000000..cec75e9 --- /dev/null +++ b/tests/LocalStack.AwsLocal.Tests/ArgumentExtensionsTests.cs @@ -0,0 +1,83 @@ +using System.Linq; +using LocalStack.AwsLocal.Extensions; +using Xunit; + +namespace LocalStack.AwsLocal.Tests +{ + public class ArgumentExtensionsTests + { + [Fact] + public void ExtractServiceName_Should_Return_Empty_String_If_Arguments_Contains_Dash() + { + var args = new[] {"-foo", "-bar"}; + + string serviceName = args.ExtractServiceName(); + + Assert.Equal(string.Empty, serviceName); + } + + [Fact] + public void ExtractServiceName_Should_Return_S3_If_One_Of_The_Argument_Is_S3Api() + { + var args = new[] { "-foo", "-bar", "s3api" }; + + string serviceName = args.ExtractServiceName(); + + Assert.Equal("s3", serviceName); + } + + [Fact] + public void ExtractServiceName_Should_Extract_First_Valid_Argument_As_ServiceName_From_Arguments() + { + var args = new[] { "-foo", "-bar", "kinesis", "list-streams" }; + + string serviceName = args.ExtractServiceName(); + + Assert.Equal("kinesis", serviceName); + } + + [Fact] + public void GetCliCommand_Should_Add_LocalStack_Service_EndPoint_As_Endpoint_Switch_To_Command() + { + var args = new[] { "kinesis", "list-streams" }; + const string serviceUrl = "http://localhost:1234"; + + string cliCommand = args.GetCliCommand(serviceUrl); + + Assert.Contains($"--endpoint-url={serviceUrl}", cliCommand); + } + + [Fact] + public void GetCliCommand_Should_Add_No_Verify_Ssl_Switch_To_Command() + { + var args = new[] { "kinesis", "list-streams" }; + const string serviceUrl = "https://localhost:1234"; + + string cliCommand = args.GetCliCommand(serviceUrl); + + Assert.Contains("--no-verify-ssl", cliCommand); + } + + [Fact] + public void GetCliCommand_Should_Add_Aws_To_Command_As_First_Argument() + { + var args = new[] { "kinesis", "list-streams" }; + const string serviceUrl = "http://localhost:1234"; + + string cliCommand = args.GetCliCommand(serviceUrl); + + Assert.StartsWith("aws ", cliCommand); + } + + [Fact] + public void GetCliCommand_Should_Add_Arguments_To_Command() + { + var args = new[] { "-foo", "-bar", "kinesis", "list-streams" }; + const string serviceUrl = "http://localhost:1234"; + + string cliCommand = args.GetCliCommand(serviceUrl); + + Assert.Contains(args, s => cliCommand.Split(" ").Contains(s)); + } + } +} diff --git a/tests/LocalStack.AwsLocal.Tests/CommandDispatcherTests.cs b/tests/LocalStack.AwsLocal.Tests/CommandDispatcherTests.cs new file mode 100644 index 0000000..8cc5dff --- /dev/null +++ b/tests/LocalStack.AwsLocal.Tests/CommandDispatcherTests.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; +using LocalStack.AwsLocal.Tests.Mocks; +using Microsoft.VisualStudio.TestPlatform.TestHost; +using Xunit; + +namespace LocalStack.AwsLocal.Tests +{ + public class CommandDispatcherTests + { + [Fact] + public void Run_Should_Write_Help_Info_And_Exit_If_Argument_Count_Zero() + { + CommandDispatcherMock commandDispatcherMock = CommandDispatcherMock.Create(new string[0]); + + + } + } +} diff --git a/tests/LocalStack.AwsLocal.Tests/LocalStack.AwsLocal.Tests.csproj b/tests/LocalStack.AwsLocal.Tests/LocalStack.AwsLocal.Tests.csproj new file mode 100644 index 0000000..993d669 --- /dev/null +++ b/tests/LocalStack.AwsLocal.Tests/LocalStack.AwsLocal.Tests.csproj @@ -0,0 +1,32 @@ + + + + netcoreapp2.2 + + true + latest + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + diff --git a/tests/LocalStack.AwsLocal.Tests/Mocks/CommandDispatcherMock.cs b/tests/LocalStack.AwsLocal.Tests/Mocks/CommandDispatcherMock.cs new file mode 100644 index 0000000..90ecad6 --- /dev/null +++ b/tests/LocalStack.AwsLocal.Tests/Mocks/CommandDispatcherMock.cs @@ -0,0 +1,33 @@ +using System.IO; +using LocalStack.AwsLocal.Contracts; +using LocalStack.Client.Contracts; +using Moq; + +namespace LocalStack.AwsLocal.Tests.Mocks +{ + public class CommandDispatcherMock : CommandDispatcher + { + public CommandDispatcherMock(Mock processHelper, Mock config, Mock textWriter, string[] args) + : base(processHelper.Object, config.Object, textWriter.Object, args) + { + ProcessHelper = processHelper; + Config = config; + TextWriter = textWriter; + } + + public Mock ProcessHelper { get; } + + public Mock Config { get; } + + public Mock TextWriter { get; } + + public static CommandDispatcherMock Create(string[] args) + { + return new CommandDispatcherMock( + new Mock(MockBehavior.Strict), + new Mock(MockBehavior.Strict), + new Mock(MockBehavior.Strict), + args); + } + } +}