diff --git a/src/AzureServiceBus.Tests/AzureServiceBus.Tests.csproj b/src/AzureServiceBus.Tests/AzureServiceBus.Tests.csproj new file mode 100644 index 00000000..231a00d8 --- /dev/null +++ b/src/AzureServiceBus.Tests/AzureServiceBus.Tests.csproj @@ -0,0 +1,18 @@ + + + + Squadron.AzureServiceBus.Tests + Squadron.AzureServiceBus.Tests + + + + + + + + + Always + + + + diff --git a/src/AzureServiceBus.Tests/AzureServiceBusCustomConfigTests.cs b/src/AzureServiceBus.Tests/AzureServiceBusCustomConfigTests.cs new file mode 100644 index 00000000..d5006200 --- /dev/null +++ b/src/AzureServiceBus.Tests/AzureServiceBusCustomConfigTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using FluentAssertions; +using Xunit; + +namespace Squadron.AzureServiceBus.Tests; + +public class AzureServiceBusCustomConfigTests( + AzureServiceBusResources azureServiceBusResource) : + IClassFixture> +{ + [Fact] + public async Task Send_And_Receive() + { + var sender = azureServiceBusResource.Client + .CreateSender("custom.topic"); + var serviceBusMessage = new ServiceBusMessage(BinaryData.FromString("custom_message")); + await sender.SendMessageAsync(serviceBusMessage); + + var receiver = azureServiceBusResource.Client + .CreateReceiver("custom.queue"); + var message = await receiver.ReceiveMessageAsync(); + var receivedMessage = message.Body.ToString(); + + receivedMessage.Should().Be("custom_message"); + } +} + +public class CustomConfig : AzureServiceBusConfig +{ + protected override Queue[] CreateQueues() + { + return [new Queue("custom.queue")]; + } + + protected override Topic[] CreateTopics() + { + var subscription = new Subscription( + "custom.subscription", + new SubscriptionProperties(ForwardTo: "custom.queue")); + + return [new Topic("custom.topic", Subscriptions: [subscription])]; + } +} diff --git a/src/AzureServiceBus.Tests/AzureServiceBusResourceTests.cs b/src/AzureServiceBus.Tests/AzureServiceBusResourceTests.cs new file mode 100644 index 00000000..1d17b653 --- /dev/null +++ b/src/AzureServiceBus.Tests/AzureServiceBusResourceTests.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace Squadron.AzureServiceBus.Tests; + +public class AzureServiceBusResourceTests( + AzureServiceBusResources azureServiceBusResource) : + IClassFixture +{ + [Fact] + public async Task Send_And_Receive() + { + var receiver = azureServiceBusResource.Client + .CreateReceiver(AzureServiceBusStatus.QueueName); + var message = await receiver.ReceiveMessageAsync(); + var receivedMessage = message.Body.ToString(); + + receivedMessage.Should().Be("status_check"); + } +} \ No newline at end of file diff --git a/src/AzureServiceBus.Tests/xunit.runner.json b/src/AzureServiceBus.Tests/xunit.runner.json new file mode 100644 index 00000000..bd5fcdd2 --- /dev/null +++ b/src/AzureServiceBus.Tests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "appDomain": "denied", + "parallelizeAssembly": true +} \ No newline at end of file diff --git a/src/AzureServiceBus/AzureServiceBus.csproj b/src/AzureServiceBus/AzureServiceBus.csproj new file mode 100644 index 00000000..7a739284 --- /dev/null +++ b/src/AzureServiceBus/AzureServiceBus.csproj @@ -0,0 +1,18 @@ + + + + + Squadron.AzureServiceBus + + + + + + + + + + + + + diff --git a/src/AzureServiceBus/AzureServiceBusConfig.cs b/src/AzureServiceBus/AzureServiceBusConfig.cs new file mode 100644 index 00000000..6942e671 --- /dev/null +++ b/src/AzureServiceBus/AzureServiceBusConfig.cs @@ -0,0 +1,102 @@ +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Squadron; + +public class AzureServiceBusConfig +{ + private static readonly JsonSerializerOptions _options = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + protected virtual Queue[] CreateQueues() + { + return []; + } + + protected virtual Topic[] CreateTopics() + { + return []; + } + + public string Build() + { + var fileName = Path.GetTempFileName(); + + Queue[] queues = CreateQueues() + .Concat([new Queue(AzureServiceBusStatus.QueueName)]) + .ToArray(); + + var config = new + { + UserConfig = new UserConfig( + [new Namespace(queues, CreateTopics())], + new Logging()) + }; + + var serializedConfig = JsonSerializer.Serialize(config, options: _options); + File.WriteAllText(fileName, serializedConfig); + + return fileName; + } +} + +public record UserConfig( + Namespace[] Namespaces, + Logging Logging); +public record Namespace( + Queue[] Queues, + Topic[] Topics, + string Name = "sbemulatorns"); +public record Queue( + string Name, + QueueProperties? Properties = null); +public record QueueProperties( + bool DeadLetteringOnMessageExpiration = true, + string DefaultMessageTimeToLive= "PT1H", + string DuplicateDetectionHistoryTimeWindow = "PT1M", + string ForwardDeadLetteredMessagesTo = "", + string ForwardTo = "", + string LockDuration= "PT1M", + int MaxDeliveryCount = 5, + bool RequiresDuplicateDetection = false, + bool RequiresSession = false); +public record Topic( + string Name, + TopicProperties? Properties = null, + Subscription[]? Subscriptions = null); +public record TopicProperties( + string DefaultMessageTimeToLive = "PT1H", + string DuplicateDetectionHistoryTimeWindow = "PT1M", + bool RequiresDuplicateDetection = false); +public record Subscription( + string Name, + SubscriptionProperties? Properties = null, + SubscriptionRule[]? Rules = null); +public record SubscriptionProperties( + bool DeadLetteringOnMessageExpiration = true, + string DefaultMessageTimeToLive = "PT1H", + string LockDuration = "PT1M", + int MaxDeliveryCount = 5, + string ForwardDeadLetteredMessagesTo = "", + string ForwardTo = "", + bool RequiresSession = false); +public record SubscriptionRule( + string Name, + TopicRuleProperties? Properties = null); +public record TopicRuleProperties( + string FilterType, + CorrelationFilter CorrelationFilter); +public record CorrelationFilter( + string ContentType, + string CorrelationId, + string Label, + string MessageId, + string ReplyTo, + string ReplyToSessionId, + string SessionId, + string To); +public record Logging(string Type = "Console"); diff --git a/src/AzureServiceBus/AzureServiceBusConstants.cs b/src/AzureServiceBus/AzureServiceBusConstants.cs new file mode 100644 index 00000000..23b1a0e2 --- /dev/null +++ b/src/AzureServiceBus/AzureServiceBusConstants.cs @@ -0,0 +1,7 @@ +namespace Squadron; + +internal class AzureServiceBusConstants +{ + internal static string SqlServerResourceName { get; } = "asb_sql_server"; + internal static string AzureServiceBusEmulatorResourceName { get; } = "asb_emulator"; +} diff --git a/src/AzureServiceBus/AzureServiceBusDefaultOptions.cs b/src/AzureServiceBus/AzureServiceBusDefaultOptions.cs new file mode 100644 index 00000000..5ee5c067 --- /dev/null +++ b/src/AzureServiceBus/AzureServiceBusDefaultOptions.cs @@ -0,0 +1,18 @@ +namespace Squadron; + +public class AzureServiceBusDefaultOptions : ComposeResourceOptions + where TConfig : AzureServiceBusConfig, new() +{ + public override void Configure(ComposeResourceBuilder builder) + { + builder.AddContainer( + AzureServiceBusConstants.SqlServerResourceName); + + builder + .AddContainer>( + AzureServiceBusConstants.AzureServiceBusEmulatorResourceName) + .AddLink(AzureServiceBusConstants.SqlServerResourceName, + new EnvironmentVariableMapping("SQL_SERVER", "#NAME#"), + new EnvironmentVariableMapping("MSSQL_SA_PASSWORD", "#MSSQL_SA_PASSWORD#")); + } +} \ No newline at end of file diff --git a/src/AzureServiceBus/AzureServiceBusEmulatorDefaultOptions.cs b/src/AzureServiceBus/AzureServiceBusEmulatorDefaultOptions.cs new file mode 100644 index 00000000..f354afa3 --- /dev/null +++ b/src/AzureServiceBus/AzureServiceBusEmulatorDefaultOptions.cs @@ -0,0 +1,30 @@ +using System; + +namespace Squadron; + +public class AzureServiceBusEmulatorDefaultOptions : + ContainerResourceOptions, + IComposableResourceOption + where TConfig : AzureServiceBusConfig, new() +{ + public Type ResourceType => typeof(AzureServiceBusEmulatorResource); + + /// + /// Configure resource options for AzureServiceBusEmulatorDefaultOptions + /// + /// + public override void Configure(ContainerResourceBuilder builder) + { + var azureServiceBusConfig = new TConfig(); + var configFile = azureServiceBusConfig.Build(); + + builder + .Name("asb_emulator") + .Image("mcr.microsoft.com/azure-messaging/servicebus-emulator") + .InternalPort(5672) + .WaitTimeout(120) + .AddEnvironmentVariable("ACCEPT_EULA=Y") + .AddVolume($"{configFile}:/ServiceBus_Emulator/ConfigFiles/Config.json") + .PreferLocalImage(); + } +} diff --git a/src/AzureServiceBus/AzureServiceBusResource.cs b/src/AzureServiceBus/AzureServiceBusResource.cs new file mode 100644 index 00000000..8e9924e5 --- /dev/null +++ b/src/AzureServiceBus/AzureServiceBusResource.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Azure.Messaging.ServiceBus.Administration; +using Xunit; + +namespace Squadron; + +/// +public class AzureServiceBusEmulatorResource + : AzureServiceBusEmulatorResource, TConfig> + where TConfig : AzureServiceBusConfig, new(); + +/// +/// Represents an AzureServiceBus resource that can be used by unit tests. +/// +/// +public class AzureServiceBusEmulatorResource : + ContainerResource, + IAsyncLifetime, + IComposableResource + where TOptions : ContainerResourceOptions, new() +{ + /// + /// Connection string to access the Azure Service Bus + /// + public string ConnectionString { get; private set; } + + /// + /// Azure Service Bus client + /// + public ServiceBusClient Client { get; private set; } + + /// + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + ConnectionString = + $"Endpoint=sb://{Manager.Instance.Address}:{Manager.Instance.HostPort}" + + ";SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"; + + Client = new ServiceBusClient(ConnectionString); + await Initializer.WaitAsync(new AzureServiceBusStatus(Client)); + } +} \ No newline at end of file diff --git a/src/AzureServiceBus/AzureServiceBusResources.cs b/src/AzureServiceBus/AzureServiceBusResources.cs new file mode 100644 index 00000000..b308e5a1 --- /dev/null +++ b/src/AzureServiceBus/AzureServiceBusResources.cs @@ -0,0 +1,25 @@ +using Azure.Messaging.ServiceBus; +using SharpCompress; + +namespace Squadron; + +public class AzureServiceBusResources : + AzureServiceBusResources; + +public class AzureServiceBusResources : + ComposeResource> + where TConfig : AzureServiceBusConfig, new() +{ + private readonly Lazy> _azureServiceBusEmulatorResource; + + public AzureServiceBusResources() + { + _azureServiceBusEmulatorResource = new Lazy>(() => + (Managers[AzureServiceBusConstants.AzureServiceBusEmulatorResourceName].Resource + as AzureServiceBusEmulatorResource)!); + } + + public string ConnectionString => _azureServiceBusEmulatorResource.Value.ConnectionString!; + + public ServiceBusClient Client => _azureServiceBusEmulatorResource.Value.Client!; +} diff --git a/src/AzureServiceBus/AzureServiceBusStatus.cs b/src/AzureServiceBus/AzureServiceBusStatus.cs new file mode 100644 index 00000000..07a8a8cd --- /dev/null +++ b/src/AzureServiceBus/AzureServiceBusStatus.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; + +namespace Squadron; + +public class AzureServiceBusStatus(ServiceBusClient client) : + IResourceStatusProvider +{ + public static string QueueName = "squadron.status.check"; + + public async Task IsReadyAsync(CancellationToken cancellationToken) + { + try + { + var sender = client.CreateSender(QueueName); + var serviceBusMessage = new ServiceBusMessage(BinaryData.FromString("status_check")); + await sender.SendMessageAsync(serviceBusMessage, cancellationToken); + + return new Status { IsReady = true }; + } + catch (Exception ex) + { + return new Status { IsReady = false, Message = "Not ready" }; + } + } +} diff --git a/src/Compose.Tests/NetworkTests.cs b/src/Compose.Tests/NetworkTests.cs index 48b6ea11..8f49a31d 100644 --- a/src/Compose.Tests/NetworkTests.cs +++ b/src/Compose.Tests/NetworkTests.cs @@ -32,14 +32,14 @@ public async Task TwoContainer_Network_BothInSameNetwork() string connectionString = mongoResource.GetComposeExports()["CONNECTIONSTRING_INTERNAL"]; string containerName = GetNameFromConnectionString(connectionString); - IList response = (await _dockerClient.Containers.ListContainersAsync( - new ContainersListParameters())); + IList response = await _dockerClient.Containers.ListContainersAsync( + new ContainersListParameters()); - ContainerListResponse container = response.Where(c => c.Names.Contains($"/{containerName}")).Single(); + ContainerListResponse container = response.FirstOrDefault(c => c.Names.Contains($"/{containerName}")); - string networkName = container.NetworkSettings.Networks.Keys.Where(n => n.Contains("squa_network")).Single(); + string networkName = container?.NetworkSettings.Networks.Keys.FirstOrDefault(n => n.Contains("squa_network")); - NetworkResponse network = (await _dockerClient.Networks.ListNetworksAsync()).Where(n => n.Name == networkName).SingleOrDefault(); + NetworkResponse network = (await _dockerClient.Networks.ListNetworksAsync()).FirstOrDefault(n => n.Name == networkName); network.Should().NotBeNull(); } diff --git a/src/Compose/ComposeResource.cs b/src/Compose/ComposeResource.cs index 21c9c6da..ff3a0630 100644 --- a/src/Compose/ComposeResource.cs +++ b/src/Compose/ComposeResource.cs @@ -1,83 +1,78 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using Xunit; -namespace Squadron +namespace Squadron; + +public class ComposeResource : IAsyncLifetime + where TOptions : ComposeResourceOptions, new() { - public class ComposeResource : IAsyncLifetime - where TOptions : ComposeResourceOptions, new() - { - public ComposeResourceSettings Settings { get; set; } + public ComposeResourceSettings Settings { get; set; } - protected Dictionary Managers { get; set; } - = new Dictionary(); + protected Dictionary Managers { get; set; } + = new Dictionary(); + + public virtual async Task InitializeAsync() + { + var options = new TOptions(); + var builder = ComposeResourceBuilder.New(); + options.Configure(builder); + Settings = builder.Build(); - public async Task InitializeAsync() + foreach (var name in BuildStartOrder()) { - var options = new TOptions(); - var builder = ComposeResourceBuilder.New(); - options.Configure(builder); - Settings = builder.Build(); + var mgr = new ComposeResourceManager(Settings, name); + Managers.Add(name, mgr); - foreach (var name in BuildStartOrder()) + foreach (ComposeResourceLink link in mgr.ResourceSettings.Links) { - var mgr = new ComposeResourceManager(); - mgr.ResourceSettings = Settings.Resources.First(x => x.Name == name); - Managers.Add(name, mgr); - - var variables = new List(); - foreach (ComposeResourceLink link in mgr.ResourceSettings.Links) + foreach (EnvironmentVariableMapping map in link.EnvironmentVariables) { - foreach (EnvironmentVariableMapping map in link.EnvironmentVariables) - { - Dictionary exports = Managers[link.Name].Exports; - variables.Add($"{map.Name}={GetVariableValue(map.Value, exports)}"); - } + Dictionary exports = Managers[link.Name].Exports; + mgr.AddEnvironmentVariable($"{map.Name}={GetVariableValue(map.Value, exports)}"); } - mgr.EnvironmentVariables = Settings.GlobalEnvionmentVariables.Concat(variables); - await mgr.StartAsync(); } + + await mgr.StartAsync(); } + } - public TResource GetResource(string name) - { - ComposeResourceManager manager = Managers[name]; - return (TResource)manager.Resource; - } + public TResource GetResource(string name) + { + ComposeResourceManager manager = Managers[name]; + return (TResource)manager.Resource; + } - private string GetVariableValue(string template, Dictionary exports) + private string GetVariableValue(string template, Dictionary exports) + { + var value = template; + foreach (KeyValuePair export in exports) { - var value = template; - foreach (KeyValuePair export in exports) - { - value = value.Replace($"#{export.Key}#", export.Value); - } - return value; + value = value.Replace($"#{export.Key}#", export.Value); } + return value; + } - public async Task DisposeAsync() - { - var stopTasks = new List(); - - foreach (KeyValuePair mgr in Managers) - { - stopTasks.Add(mgr.Value.StopAsync()); - } - await Task.WhenAll(stopTasks); - } + public async Task DisposeAsync() + { + var stopTasks = new List(); - private IEnumerable BuildStartOrder() + foreach (KeyValuePair mgr in Managers) { - //TODO: Build dependency graph - return Settings.Resources.Select(x => x.Name); + stopTasks.Add(mgr.Value.StopAsync()); } + await Task.WhenAll(stopTasks); + } - private ComposableResourceSettings GetResourceSetting(string name) - { - return Settings.Resources.FirstOrDefault(x => x.Name == name); - } + private IEnumerable BuildStartOrder() + { + //TODO: Build dependency graph + return Settings.Resources.Select(x => x.Name); + } + + private ComposableResourceSettings GetResourceSetting(string name) + { + return Settings.Resources.FirstOrDefault(x => x.Name == name); } -} +} \ No newline at end of file diff --git a/src/Compose/ComposeResourceBuilder.cs b/src/Compose/ComposeResourceBuilder.cs index 6e291afc..60238a2d 100644 --- a/src/Compose/ComposeResourceBuilder.cs +++ b/src/Compose/ComposeResourceBuilder.cs @@ -2,101 +2,95 @@ using System.Collections.Generic; using System.Linq; -namespace Squadron +namespace Squadron; + +public interface IComposableResourceBuilder +{ + ComposableResourceSettings Build(); +} + +public class ComposableResourceBuilder : IComposableResourceBuilder + where TResourceOptions : ContainerResourceOptions, IComposableResourceOption, new() { - public interface IComposableResourceBuilder + private readonly string _name; + private readonly ComposableResourceType _resourceType; + private List _links = new List(); + private Action _onStarted = null; + + public static ComposableResourceBuilder New( + string name, + ComposableResourceType resourceType) + => new ComposableResourceBuilder(name, resourceType); + + private ComposableResourceBuilder(string name, ComposableResourceType resourceType) { - ComposableResourceSettings Build(); + _name = name; + _resourceType = resourceType; } - public class ComposableResourceBuilder : IComposableResourceBuilder - where TResourceOptions : ContainerResourceOptions, IComposableResourceOption, new() + public ComposableResourceBuilder AddLink( + string name, + params EnvironmentVariableMapping[] mappings) { - private readonly string _name; - private readonly ComposableResourceType _resourceType; - private List _links = new List(); - private Action _onStarted = null; - - public static ComposableResourceBuilder New( - string name, - ComposableResourceType resourceType) - => new ComposableResourceBuilder(name, resourceType); - - private ComposableResourceBuilder(string name, ComposableResourceType resourceType) + _links.Add(new ComposeResourceLink { - _name = name; - _resourceType = resourceType; - } + Name = name, + EnvironmentVariables = new List(mappings) + }); - public ComposableResourceBuilder AddLink( - string name, - params EnvironmentVariableMapping[] mappings) - { - _links.Add(new ComposeResourceLink - { - Name = name, - EnvironmentVariables = new List(mappings) - }); - - return this; - } - public ComposableResourceBuilder WithOnStarted( - Action onStarted) - { - _onStarted = onStarted; - return this; - } + return this; + } + public ComposableResourceBuilder WithOnStarted( + Action onStarted) + { + _onStarted = onStarted; + return this; + } - public ComposableResourceSettings Build() - { - var setting = new ComposableResourceSettings( - _name, - _resourceType, - new List(_links), - new TResourceOptions(), - _onStarted - ); - - return setting; - } + public ComposableResourceSettings Build() + { + return new ComposableResourceSettings( + _name, + _resourceType, + new List(_links), + new TResourceOptions(), + _onStarted); } +} - public class ComposeResourceBuilder - { - public static ComposeResourceBuilder New() => new ComposeResourceBuilder(); +public class ComposeResourceBuilder +{ + public static ComposeResourceBuilder New() => new ComposeResourceBuilder(); - private List _settingsBuilder = - new List(); + private List _settingsBuilder = + new List(); - private List _globaEnvironmentVariables = new List(); + private List _globaEnvironmentVariables = new List(); - public ComposeResourceSettings Build() - { - return new ComposeResourceSettings( - new List(_globaEnvironmentVariables), - _settingsBuilder.Select(x => x.Build()).ToList() - ); - } - - public ComposableResourceBuilder AddContainer( - string name - ) - where TResourceOptions : ContainerResourceOptions, IComposableResourceOption, new() - { + public ComposeResourceSettings Build() + { + return new ComposeResourceSettings( + new List(_globaEnvironmentVariables), + _settingsBuilder.Select(x => x.Build()).ToList()); + } - var crb = ComposableResourceBuilder.New( - name, - ComposableResourceType.Container); - _settingsBuilder.Add(crb); - return crb; - } + public ComposableResourceBuilder AddContainer( + string name) + where TResourceOptions : ContainerResourceOptions, IComposableResourceOption, new() + { - public ComposeResourceBuilder AddGlobalEnvironmentVariable(string name, string value) - { - _globaEnvironmentVariables.Add($"{name}={value}"); - return this; - } + var crb = ComposableResourceBuilder.New( + name, + ComposableResourceType.Container); + _settingsBuilder.Add(crb); + return crb; } -} + + public ComposeResourceBuilder AddGlobalEnvironmentVariable(string name, string value) + { + _globaEnvironmentVariables.Add($"{name}={value}"); + return this; + } +} \ No newline at end of file diff --git a/src/Compose/ComposeResourceManager.cs b/src/Compose/ComposeResourceManager.cs index 52868d18..ec65b2c9 100644 --- a/src/Compose/ComposeResourceManager.cs +++ b/src/Compose/ComposeResourceManager.cs @@ -1,60 +1,76 @@ -using System; -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using System.Linq; -namespace Squadron +namespace Squadron; + +public class ComposeResourceManager { - public class ComposeResourceManager - { - public ComposableResourceSettings ResourceSettings { get; internal set; } - public ContainerResourceSettings ContainerSettings { get; private set; } - public Dictionary Exports { get; private set; } + private readonly ComposeResourceSettings _settings; + private readonly List _environmentVariables = new List(); - public IComposableResource Resource { get; set; } - public IEnumerable EnvironmentVariables { get; internal set; } + public ComposeResourceManager(ComposeResourceSettings settings, string name) + { + _settings = settings; + ResourceSettings = settings.Resources.First(x => x.Name == name); + _environmentVariables.AddRange(settings.GlobalEnvionmentVariables); + } - internal async Task StartAsync() - { - if (ResourceSettings.Type == ComposableResourceType.Container) - { - var builder = ContainerResourceBuilder.New(); - ResourceSettings.ContainerOptions.Configure(builder); - BuildResourceInstance(); - Resource.SetEnvironmentVariables(EnvironmentVariables.ToList()); - - // Give over the networks if resource is not generic - if(!IsResourceGenericType()) - { - IList networks = builder.Build().Networks; - Resource.SetNetworks(networks); - } - - await Resource.InitializeAsync(); - Exports = Resource.GetComposeExports(); - } - } + public ComposableResourceSettings ResourceSettings { get; internal set; } + public ContainerResourceSettings ContainerSettings { get; private set; } + public Dictionary Exports { get; private set; } + public IComposableResource Resource { get; set; } + public IReadOnlyList EnvironmentVariables => _environmentVariables; + + public void AddEnvironmentVariable(params string[] variable) + { + _environmentVariables.AddRange(variable); + } - private void BuildResourceInstance() + internal async Task StartAsync() + { + if (ResourceSettings.Type == ComposableResourceType.Container) { - var composableOptions = (IComposableResourceOption)ResourceSettings.ContainerOptions; - Type activateType = composableOptions.ResourceType; + var builder = ContainerResourceBuilder.New(); + ResourceSettings.ContainerOptions.Configure(builder); + BuildResourceInstance(); + Resource.SetEnvironmentVariables(EnvironmentVariables.ToList()); - if (IsResourceGenericType()) + // Give over the networks if resource is not generic + var networks = new List(); + if(!IsResourceGenericType()) { - activateType = composableOptions.ResourceType - .MakeGenericType(ResourceSettings.ContainerOptions.GetType()); + networks.AddRange(builder.Build().Networks); } - Resource = (IComposableResource)Activator.CreateInstance(activateType); + + networks.Add(_settings.Identifier); + Resource.SetNetworks(networks); + + await Resource.InitializeAsync(); + Exports = Resource.GetComposeExports(); } - - private bool IsResourceGenericType() => - ((IComposableResourceOption)ResourceSettings.ContainerOptions) - .ResourceType.IsGenericType; + } + + private void BuildResourceInstance() + { + var composableOptions = (IComposableResourceOption)ResourceSettings.ContainerOptions; + Type activateType = composableOptions.ResourceType; - internal async Task StopAsync() + if (IsResourceGenericType()) { - await Resource.DisposeAsync(); + activateType = composableOptions.ResourceType + .MakeGenericType(ResourceSettings.ContainerOptions.GetType()); } + Resource = (IComposableResource)Activator.CreateInstance(activateType); + } + + private bool IsResourceGenericType() => + ((IComposableResourceOption)ResourceSettings.ContainerOptions) + .ResourceType.IsGenericTypeDefinition; + + internal async Task StopAsync() + { + await Resource.DisposeAsync(); } -} +} \ No newline at end of file diff --git a/src/Compose/ComposeResourceSettings.cs b/src/Compose/ComposeResourceSettings.cs index 3c428ccc..7b8fedaf 100644 --- a/src/Compose/ComposeResourceSettings.cs +++ b/src/Compose/ComposeResourceSettings.cs @@ -1,21 +1,21 @@ -using System; +using System; using System.Collections.Generic; -namespace Squadron +namespace Squadron; + +public class ComposeResourceSettings { - public class ComposeResourceSettings - { - public IReadOnlyList GlobalEnvionmentVariables { get; internal set; } + public string Identifier { get; } = $"compose_{Guid.NewGuid():N}"; + public IReadOnlyList GlobalEnvionmentVariables { get; internal set; } - public IReadOnlyList Resources { get; set; } + public IReadOnlyList Resources { get; set; } - internal ComposeResourceSettings( - IReadOnlyList globalEnvionmentVariables, - IReadOnlyList resources) - { - GlobalEnvionmentVariables = globalEnvionmentVariables ?? - throw new ArgumentNullException(nameof(globalEnvionmentVariables)); - Resources = resources ?? throw new ArgumentNullException(nameof(resources)); - } + internal ComposeResourceSettings( + IReadOnlyList globalEnvionmentVariables, + IReadOnlyList resources) + { + GlobalEnvionmentVariables = globalEnvionmentVariables ?? + throw new ArgumentNullException(nameof(globalEnvionmentVariables)); + Resources = resources ?? throw new ArgumentNullException(nameof(resources)); } -} +} \ No newline at end of file diff --git a/src/SqlServer/SqlServerDefaultOptions.cs b/src/SqlServer/SqlServerDefaultOptions.cs index 1e0c1d7b..e7ccf2a6 100644 --- a/src/SqlServer/SqlServerDefaultOptions.cs +++ b/src/SqlServer/SqlServerDefaultOptions.cs @@ -1,29 +1,32 @@ using System; -namespace Squadron +namespace Squadron; + +/// +/// Default SqlServer resource options +/// +public class SqlServerDefaultOptions : + ContainerResourceOptions, + IComposableResourceOption { + public Type ResourceType => typeof(SqlServerResource); + /// - /// Default SqlServer resource options + /// Configure resource options /// - public class SqlServerDefaultOptions : ContainerResourceOptions + /// + public override void Configure(ContainerResourceBuilder builder) { - /// - /// Configure resource options - /// - /// - public override void Configure(ContainerResourceBuilder builder) - { - var password = "_Qtp" + Guid.NewGuid().ToString("N"); - builder - .Name("mssql") - .Image("mcr.microsoft.com/mssql/server:2019-latest") - .InternalPort(1433) - .WaitTimeout(60) - .Username("sa") - .Password(password) - .AddEnvironmentVariable("ACCEPT_EULA=Y") - .AddEnvironmentVariable($"SA_PASSWORD={password}") - .PreferLocalImage(); - } + var password = "_Qtp" + Guid.NewGuid().ToString("N"); + builder + .Name("mssql") + .Image("mcr.microsoft.com/mssql/server:2019-latest") + .InternalPort(1433) + .WaitTimeout(60) + .Username("sa") + .Password(password) + .AddEnvironmentVariable("ACCEPT_EULA=Y") + .AddEnvironmentVariable($"SA_PASSWORD={password}") + .PreferLocalImage(); } -} +} \ No newline at end of file diff --git a/src/SqlServer/SqlServerResource.cs b/src/SqlServer/SqlServerResource.cs index 47ce924f..73b2a551 100644 --- a/src/SqlServer/SqlServerResource.cs +++ b/src/SqlServer/SqlServerResource.cs @@ -6,193 +6,193 @@ using System.Threading.Tasks; using Xunit; -namespace Squadron +namespace Squadron; + +/// +public partial class SqlServerResource : + SqlServerResource; + +/// +/// Represents a SqlServer database resource that can be used by unit tests. +/// +/// +public partial class SqlServerResource : + ContainerResource, + IAsyncLifetime, + IComposableResource + where TOptions : ContainerResourceOptions, new() { - /// - public partial class SqlServerResource : SqlServerResource { } + private readonly SemaphoreSlim _sync = new SemaphoreSlim(1,1); + private readonly HashSet _databases = new HashSet(); + private string _serverConnectionString; - /// - /// Represents a SqlServer database resource that can be used by unit tests. - /// - /// - public partial class SqlServerResource - : ContainerResource, - IAsyncLifetime - where TOptions : ContainerResourceOptions, new() + /// + public override async Task InitializeAsync() { - /// - /// Sync lock - /// - protected readonly SemaphoreSlim _sync = new SemaphoreSlim(1,1); - /// - /// The databases - /// - protected readonly HashSet _databases = new HashSet(); - /// - /// The SqlServer connection string - /// - protected string _serverConnectionString; - - /// - public override async Task InitializeAsync() - { - await base.InitializeAsync(); + await base.InitializeAsync(); - _serverConnectionString = CreateServerConnectionString(); + _serverConnectionString = CreateServerConnectionString(); - await Initializer.WaitAsync( - new SqlServerStatus(_serverConnectionString)); - } + await Initializer.WaitAsync( + new SqlServerStatus(_serverConnectionString)); + } + + /// + /// Creates a connection string targeting the given + /// + /// Database name + public string CreateConnectionString(string databaseName) + => CreateDatabaseConnectionString(databaseName); - /// - /// Creates a connection string targeting the given - /// - /// Database name - public string CreateConnectionString(string databaseName) - => CreateDatabaseConnectionString(databaseName); - - /// - /// Create a empty database - /// - /// Optional: the database name. - /// Database connection string. - public Task CreateDatabaseAsync(string databaseName = null) + /// + /// Create a empty database + /// + /// Optional: the database name. + /// Database connection string. + public Task CreateDatabaseAsync(string databaseName = null) + { + var name = databaseName ?? UniqueNameGenerator.Create("db"); + var script = $"CREATE DATABASE {name};"; + return CreateDatabaseAsync(script, name); + } + + /// + /// Create a database from an SQL script + /// + /// The SQL script content + /// The database name. + /// + /// is null or + /// or + /// is null or + /// + /// Database connection string. + public Task CreateDatabaseAsync(string sqlScript, string databaseName) + { + if (string.IsNullOrEmpty(sqlScript)) { - var name = databaseName ?? UniqueNameGenerator.Create("db"); - var script = $"CREATE DATABASE {name};"; - return CreateDatabaseAsync(script, name); + throw new ArgumentException( + "The sql script cannot be null or empty.", nameof(sqlScript)); } - /// - /// Create a database from an SQL script - /// - /// The SQL script content - /// The database name. - /// - /// is null or - /// or - /// is null or - /// - /// Database connection string. - public Task CreateDatabaseAsync(string sqlScript, string databaseName) + if (string.IsNullOrEmpty(databaseName)) { - if (string.IsNullOrEmpty(sqlScript)) - { - throw new ArgumentException( - "The sql script cannot be null or empty.", nameof(sqlScript)); - } - - if (string.IsNullOrEmpty(databaseName)) - { - throw new ArgumentException( - "The database name cannot be null or empty.", nameof(databaseName)); - } - - return CreateDatabaseInternalAsync(sqlScript, databaseName); + throw new ArgumentException( + "The database name cannot be null or empty.", nameof(databaseName)); } - private async Task CreateDatabaseInternalAsync(string sqlScript, string databaseName) + return CreateDatabaseInternalAsync(sqlScript, databaseName); + } + + private async Task CreateDatabaseInternalAsync(string sqlScript, string databaseName) + { + await _sync.WaitAsync(); + try { - await _sync.WaitAsync(); - try - { - FileInfo scriptFile = CreateSqlFile(sqlScript); - var copyContext = new CopyContext(scriptFile.FullName, $"/tmp/{scriptFile.Name}"); + FileInfo scriptFile = CreateSqlFile(sqlScript); + var copyContext = new CopyContext(scriptFile.FullName, $"/tmp/{scriptFile.Name}"); - await Manager.CopyToContainerAsync(copyContext); + await Manager.CopyToContainerAsync(copyContext); - await Manager.InvokeCommandAsync( - ChmodCommand.ReadWrite($"/tmp/{scriptFile.Name}")); + await Manager.InvokeCommandAsync( + ChmodCommand.ReadWrite($"/tmp/{scriptFile.Name}")); - await Manager.InvokeCommandAsync( - SqlCommand.ExecuteFile(copyContext.Destination, Settings)); + await Manager.InvokeCommandAsync( + SqlCommand.ExecuteFile(copyContext.Destination, Settings)); - _databases.Add(databaseName); + _databases.Add(databaseName); - return CreateDatabaseConnectionString(databaseName); - } - finally - { - _sync.Release(); - } + return CreateDatabaseConnectionString(databaseName); } - - /// - /// Executes the specified on the currently running server. - /// - /// - /// The SQL that shall be executed on the currently running server. - /// - /// - /// is null or - /// - /// - /// There is currently no SQL Server running. - /// - public Task ExecuteSqlAsync(string sql) + finally { - if (string.IsNullOrEmpty(sql)) - { - throw new ArgumentException("The sql script cannot be null or empty.", nameof(sql)); - } - - if (string.IsNullOrEmpty(_serverConnectionString)) - { - throw new InvalidOperationException("There is currently no databse deployed."); - } + _sync.Release(); + } + } - return ExecuteSqlInternalAsync(sql); + /// + /// Executes the specified on the currently running server. + /// + /// + /// The SQL that shall be executed on the currently running server. + /// + /// + /// is null or + /// + /// + /// There is currently no SQL Server running. + /// + public Task ExecuteSqlAsync(string sql) + { + if (string.IsNullOrEmpty(sql)) + { + throw new ArgumentException("The sql script cannot be null or empty.", nameof(sql)); } - private async Task ExecuteSqlInternalAsync(string sql) + if (string.IsNullOrEmpty(_serverConnectionString)) { - await _sync.WaitAsync(); - try - { - await Manager.InvokeCommandAsync( - SqlCommand.ExecuteQuery(sql, Settings)); - } - finally - { - _sync.Release(); - } + throw new InvalidOperationException("There is currently no databse deployed."); } - /// - /// Creates the database connection string. - /// - /// Name of the database. - /// - protected string CreateDatabaseConnectionString(string databaseName) - => $"{_serverConnectionString}Database={databaseName}"; + return ExecuteSqlInternalAsync(sql); + } + + public override Dictionary GetComposeExports() + { + Dictionary exports = base.GetComposeExports(); + exports.Add("CONNECTIONSTRING", _serverConnectionString); + exports.Add("MSSQL_SA_PASSWORD", Settings.Password); + return exports; + } - private string CreateServerConnectionString() - => new StringBuilder() - .Append($"Data Source={Manager.Instance.Address},{Manager.Instance.HostPort};") - .Append("Integrated Security=False;") - .Append($"User ID={Settings.Username};") - .Append($"Password={Settings.Password};") - .Append("MultipleActiveResultSets=True;") - .Append("Encrypt=no;") - .ToString(); - - internal async Task DeployAndExecute(string sqlScript) + private async Task ExecuteSqlInternalAsync(string sql) + { + await _sync.WaitAsync(); + try { - FileInfo scriptFile = CreateSqlFile(sqlScript); - var copyContext = new CopyContext(scriptFile.FullName, $"/tmp/{scriptFile.Name}"); - - await Manager.CopyToContainerAsync(copyContext); await Manager.InvokeCommandAsync( - SqlCommand.ExecuteFile(copyContext.Destination, Settings)); - - File.Delete(scriptFile.FullName); + SqlCommand.ExecuteQuery(sql, Settings)); } - - private FileInfo CreateSqlFile(string content) + finally { - var scriptFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".sql"); - File.WriteAllText(scriptFile, content); - return new FileInfo(scriptFile); + _sync.Release(); } } -} + + /// + /// Creates the database connection string. + /// + /// Name of the database. + /// + protected string CreateDatabaseConnectionString(string databaseName) + => $"{_serverConnectionString}Database={databaseName}"; + + private string CreateServerConnectionString() + => new StringBuilder() + .Append($"Data Source={Manager.Instance.Address},{Manager.Instance.HostPort};") + .Append("Integrated Security=False;") + .Append($"User ID={Settings.Username};") + .Append($"Password={Settings.Password};") + .Append("MultipleActiveResultSets=True;") + .Append("Encrypt=no;") + .ToString(); + + internal async Task DeployAndExecute(string sqlScript) + { + FileInfo scriptFile = CreateSqlFile(sqlScript); + var copyContext = new CopyContext(scriptFile.FullName, $"/tmp/{scriptFile.Name}"); + + await Manager.CopyToContainerAsync(copyContext); + await Manager.InvokeCommandAsync( + SqlCommand.ExecuteFile(copyContext.Destination, Settings)); + + File.Delete(scriptFile.FullName); + } + + private FileInfo CreateSqlFile(string content) + { + var scriptFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".sql"); + File.WriteAllText(scriptFile, content); + return new FileInfo(scriptFile); + } +} \ No newline at end of file diff --git a/src/Squadron.sln b/src/Squadron.sln index 9873b15b..33987553 100644 --- a/src/Squadron.sln +++ b/src/Squadron.sln @@ -94,6 +94,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gitea", "Gitea\Gitea.csproj EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gitea.Tests", "Gitea.Tests\Gitea.Tests.csproj", "{8FFFFA62-88DC-44BB-A451-E74455AFC8D0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureServiceBus", "AzureServiceBus\AzureServiceBus.csproj", "{5EEC5295-84E9-43CB-97F9-65ACECBE45D6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureServiceBus.Tests", "AzureServiceBus.Tests\AzureServiceBus.Tests.csproj", "{BFAFE620-C81C-4158-87CB-BC2A53DF16B6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -644,6 +648,30 @@ Global {8FFFFA62-88DC-44BB-A451-E74455AFC8D0}.Release|x64.Build.0 = Release|Any CPU {8FFFFA62-88DC-44BB-A451-E74455AFC8D0}.Release|x86.ActiveCfg = Release|Any CPU {8FFFFA62-88DC-44BB-A451-E74455AFC8D0}.Release|x86.Build.0 = Release|Any CPU + {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Debug|x64.ActiveCfg = Debug|Any CPU + {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Debug|x64.Build.0 = Debug|Any CPU + {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Debug|x86.ActiveCfg = Debug|Any CPU + {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Debug|x86.Build.0 = Debug|Any CPU + {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Release|Any CPU.Build.0 = Release|Any CPU + {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Release|x64.ActiveCfg = Release|Any CPU + {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Release|x64.Build.0 = Release|Any CPU + {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Release|x86.ActiveCfg = Release|Any CPU + {5EEC5295-84E9-43CB-97F9-65ACECBE45D6}.Release|x86.Build.0 = Release|Any CPU + {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Debug|x64.ActiveCfg = Debug|Any CPU + {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Debug|x64.Build.0 = Debug|Any CPU + {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Debug|x86.ActiveCfg = Debug|Any CPU + {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Debug|x86.Build.0 = Debug|Any CPU + {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Release|Any CPU.Build.0 = Release|Any CPU + {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Release|x64.ActiveCfg = Release|Any CPU + {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Release|x64.Build.0 = Release|Any CPU + {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Release|x86.ActiveCfg = Release|Any CPU + {BFAFE620-C81C-4158-87CB-BC2A53DF16B6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE