From 762c53835ea278d377ef6f0abab02d9fe45fde8d Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Tue, 5 Nov 2024 14:18:10 +0100 Subject: [PATCH 01/30] Start cleanup --- .../Specifications/IPAssignmentSpecs.cs | 11 +- .../Networks/CatletIpManager.cs | 172 +++++++++---- .../Networks/ICatletIpManager.cs | 8 +- .../UpdateCatletNetworksCommandHandler.cs | 4 +- .../Networks/CatletIpManagerTests.cs | 231 +++++++++++++----- 5 files changed, 305 insertions(+), 121 deletions(-) diff --git a/src/data/src/Eryph.StateDb/Specifications/IPAssignmentSpecs.cs b/src/data/src/Eryph.StateDb/Specifications/IPAssignmentSpecs.cs index ee93d42fa..9d24e3e9f 100644 --- a/src/data/src/Eryph.StateDb/Specifications/IPAssignmentSpecs.cs +++ b/src/data/src/Eryph.StateDb/Specifications/IPAssignmentSpecs.cs @@ -6,13 +6,14 @@ namespace Eryph.StateDb.Specifications; public static class IPAssignmentSpecs { - public sealed class GetByPort : Specification, ISingleResultSpecification + public sealed class GetByPort : Specification { public GetByPort(Guid portId) { - Query.Where(x => x.NetworkPortId == portId); + Query.Where(x => x.NetworkPortId == portId) + .Include(a => ((IpPoolAssignment)a).Pool) + .Include(p => p.Subnet) + .ThenInclude(s => ((VirtualNetworkSubnet)s!).Network); } } - - -} \ No newline at end of file +} diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs index 5c5fb5aa9..e9aa4babe 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs @@ -3,30 +3,98 @@ using System.Net; using System.Threading; using System.Threading.Tasks; +using Eryph.ConfigModel; using Eryph.ConfigModel.Catlets; +using Eryph.Core; using Eryph.StateDb; using Eryph.StateDb.Model; using Eryph.StateDb.Specifications; using LanguageExt; using LanguageExt.Common; +using static LanguageExt.Prelude; +using Array = System.Array; + namespace Eryph.Modules.Controller.Networks; -public class CatletIpManager : BaseIpManager, ICatletIpManager +public class CatletIpManager( + IStateStore stateStore, + IIpPoolManager poolManager) + : BaseIpManager(stateStore, poolManager), ICatletIpManager { - - public CatletIpManager(IStateStore stateStore, IIpPoolManager poolManager): base(stateStore, poolManager) - { - } - - public EitherAsync ConfigurePortIps( Guid projectId, string environment, CatletNetworkPort port, - CatletNetworkConfig[] networkConfigs, CancellationToken cancellationToken) + CatletNetworkConfig networkConfig, + CancellationToken cancellationToken) => + from environmentName in EnvironmentName.NewEither(environment) + .ToAsync() + from networkName in Optional(networkConfig.Name) + .Map(EryphNetworkName.NewEither) + .IfNone(EryphNetworkName.New(EryphConstants.DefaultNetworkName)) + .ToAsync() + let subnetName = Optional(networkConfig.SubnetV4?.Name) + .IfNone(EryphConstants.DefaultSubnetName) + let ipPoolName = Optional(networkConfig.SubnetV4?.IpPool) + .IfNone(EryphConstants.DefaultIpPoolName) + from ipAssignments in _stateStore.For().IO.ListAsync( + new IPAssignmentSpecs.GetByPort(port.Id), + cancellationToken) + let validDirectAssignments = ipAssignments + .Filter(a => a is not IpPoolAssignment && IsValidAssignment(a, networkName, subnetName)) + let validPoolAssignments = ipAssignments + .OfType().ToSeq() + .Filter(a => IsValidPoolAssignment(a, networkName, subnetName, ipPoolName)) + let invalidAssignments = ipAssignments.Except(validDirectAssignments).Except(validPoolAssignments) + from _ in invalidAssignments + .Map(a => _stateStore.For().IO.DeleteAsync(a)) + .SequenceSerial() + from newAssignment in validPoolAssignments.IsEmpty + ? from assignment in CreateAssignment(projectId, environmentName.Value, port, networkName, subnetName, ipPoolName, cancellationToken) + select Some(assignment) + : RightAsync>(None) + select validPoolAssignments.Append(newAssignment).Append(validDirectAssignments) + .Map(a => IPAddress.Parse(a.IpAddress!)) + .ToArray(); + + private EitherAsync CreateAssignment( + Guid projectId, + string environment, + CatletNetworkPort port, + EryphNetworkName networkName, + string subnetName, + string ipPoolName, + CancellationToken cancellationToken) => + from network in _stateStore.Read().IO.GetBySpecAsync( + new VirtualNetworkSpecs.GetByName(projectId, networkName.Value, environment), + cancellationToken) + // It is optional to have an environment specific network. Therefore, + // we fall back to the network in default environment. + from validNetwork in network.IsNone && environment != EryphConstants.DefaultEnvironmentName + ? _stateStore.Read().IO.GetBySpecAsync( + new VirtualNetworkSpecs.GetByName(projectId, networkName.Value, EryphConstants.DefaultEnvironmentName), + cancellationToken) + .Bind(fr => fr.ToEitherAsync(Error.New($"Network {networkName} not found in environment {environment} and default environment."))) + : network.ToEitherAsync(Error.New($"Environment {environment}: Network {networkName} not found.")) + + from subnet in _stateStore.Read().IO.GetBySpecAsync( + new SubnetSpecs.GetByNetwork(validNetwork.Id, subnetName), + cancellationToken) + from validSubnet in subnet.ToEitherAsync( + Error.New($"Environment {environment}: Subnet {subnetName} not found in network {networkName}.")) + from assignment in _poolManager.AcquireIp(validSubnet.Id, ipPoolName, cancellationToken) + let _ = UpdatePortAssignment(port, assignment) + select assignment; + /* + public EitherAsync ConfigurePortIps2( + Guid projectId, + string environment, + CatletNetworkPort port, + CatletNetworkConfig networkConfig, + CancellationToken cancellationToken) { - + // TODO Why does this iterate over all ports? var portNetworks = networkConfigs.Map(x => new PortNetwork(x.Name, x.SubnetV4 == null @@ -38,13 +106,13 @@ public EitherAsync ConfigurePortIps( var getPortAssignments = Prelude.TryAsync(_stateStore.For().ListAsync(new IPAssignmentSpecs.GetByPort(port.Id), - cancellationToken)) + c)) .ToEither(f => Error.New(f)); return from portAssignments in getPortAssignments from validAssignments in portAssignments.Map( - assignment => CheckAssignmentConfigured(assignment, networkConfigs).ToAsync()) + assignment => CheckAssignmentConfigured(assignment, networkConfig).ToAsync()) .TraverseSerial(l => l.AsEnumerable()) .Map(e => e.Flatten()) @@ -87,39 +155,53 @@ select validAndNewAssignments .Select(x => IPAddress.Parse(x.IpAddress)).ToArray(); } - - private async Task>> CheckAssignmentConfigured(IpAssignment assignment, CatletNetworkConfig[] networkConfigs) - { - var networkName = ""; - var poolName = ""; - - await _stateStore.LoadPropertyAsync(assignment, x => x.Subnet); - if (assignment.Subnet is VirtualNetworkSubnet networkSubnet) + */ + /* + private EitherAsync> CheckAssignmentConfigured( + IpAssignment assignment, + EryphNetworkName configuredNetworkName, + string configuredSubnetName, + string configuredPoolName) => + from _ in RightAsync(unit) + let networkName = assignment.Subnet switch { - await _stateStore.LoadPropertyAsync(networkSubnet, x => x.Network); - networkName = networkSubnet.Network.Name; + VirtualNetworkSubnet subnet => Some(EryphNetworkName.New(subnet.Network.Name)), + _ => None } - - if (assignment is IpPoolAssignment poolAssignment) - { - await _stateStore.LoadPropertyAsync(poolAssignment, x => x.Pool); - poolName = poolAssignment.Pool.Name; - } - - if (networkConfigs.Any(x => x.Name == networkName - && (string.IsNullOrWhiteSpace(poolName) || poolName == (x.SubnetV4?.IpPool?? "default") ))) - return Prelude.Right>(assignment); - - // remove invalid - await _stateStore.For().DeleteAsync(assignment); - return Prelude.Right>(Option.None); - - } - - - private record PortNetwork( - Option NetworkName, - Option SubnetName, - Option PoolName); - -} \ No newline at end of file + let subnetName = assignment.Subnet.Name + let isValid = networkName.Match( + Some: validNetworkName => assignment switch + { + IpPoolAssignment poolAssignment => validNetworkName == configuredNetworkName + && subnetName == configuredSubnetName + && poolAssignment.Pool.Name == configuredPoolName, + _ => validNetworkName == configuredNetworkName + && subnetName == configuredSubnetName, + }, + None: () => false) + from result in isValid + ? RightAsync>(assignment) + : from _ in _stateStore.For().IO.DeleteAsync(assignment) + select Option.None + select result; + */ + + // TODO Check environment match + // TODO Check IP address is valid + + private static bool IsValidAssignment( + IpAssignment assignment, + EryphNetworkName configuredNetwork, + string configuredSubnet) => + assignment.Subnet is VirtualNetworkSubnet subnet + && EryphNetworkName.New(subnet.Network.Name) == configuredNetwork + && subnet.Name == configuredSubnet; + + private static bool IsValidPoolAssignment( + IpPoolAssignment assignment, + EryphNetworkName configuredNetwork, + string configuredSubnet, + string configuredPool) => + IsValidAssignment(assignment, configuredNetwork, configuredSubnet) + && assignment.Pool.Name == configuredPool; +} diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/ICatletIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/ICatletIpManager.cs index c9b867fe4..fee0c4aa6 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/ICatletIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/ICatletIpManager.cs @@ -11,8 +11,10 @@ namespace Eryph.Modules.Controller.Networks public interface ICatletIpManager { public EitherAsync ConfigurePortIps( - Guid projectId, string environment, CatletNetworkPort port, - CatletNetworkConfig[] networkConfigs, CancellationToken cancellationToken); - + Guid projectId, + string environment, + CatletNetworkPort port, + CatletNetworkConfig networkConfig, + CancellationToken cancellationToken); } } diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs index 8c9678abe..9f3056d89 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs @@ -90,7 +90,7 @@ from networkProvider in networkProviders.NetworkProviders.Find(x => x.Name == ne let isFlatNetwork = networkProvider.Type == NetworkProviderType.Flat - let c1 = new CancellationTokenSource(5000) + let c1 = new CancellationTokenSource(500000) from networkPort in GetOrAddAdapterPort( network, command.CatletId, catletMetadataId, networkConfig.AdapterName, @@ -119,7 +119,7 @@ from ips in isFlatNetwork : _ipManager.ConfigurePortIps( command.ProjectId, command.Config.Environment ?? "default", networkPort, - command.Config.Networks, + networkConfig, c3.Token) from floatingIps in isFlatNetwork diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs index de3cd9c15..325bc3995 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs @@ -1,9 +1,11 @@ using Eryph.ConfigModel.Catlets; +using Eryph.Core; using Eryph.Modules.Controller.Networks; using Eryph.Resources; using Eryph.StateDb; using Eryph.StateDb.Model; using Eryph.StateDb.Sqlite; +using Eryph.StateDb.TestBase; using FluentAssertions; using FluentAssertions.LanguageExt; using LanguageExt; @@ -11,16 +13,31 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Moq; +using SimpleInjector; +using SimpleInjector.Integration.ServiceCollection; using Xunit; namespace Eryph.Modules.Controller.Tests.Networks { - public sealed class CatletIpManagerTests : IDisposable + public sealed class CatletIpManagerTests : InMemoryStateDbTestBase { - public void Dispose() - { - _connection.Dispose(); - } + // ReSharper disable InconsistentNaming + private const string ProjectA = "96bbd6d7-01f9-4001-8c86-3fba75baa1b5"; + private const string NetworkA_Default = "cb58fe00-3f64-4b66-b58e-23fb15df3cac"; + private const string NetworkA_Default_Subnet = "ed6697cd-836f-4da7-914b-b09ed1567934"; + private const string NetworkA_Other_Subnet = "29fb8b37-4779-427a-bc5c-9a5eccffd5e2"; + + private const string ProjectB = "75c27daf-77c8-4b98-a072-a4706dceb422"; + private const string NetworkB_Default = "a0ce4b1a-03e6-413b-a048-567079b49b28"; + private const string NetworkB_Env2_Default = "29408ed0-f876-4879-9faa-deb519d1df0a"; + private const string NetworkB_Default_Subnet = "ac451fa5-3364-4593-aa4d-14f95529fd54"; + private const string NetworkB_Env2_Subnet = "91a25d95-f417-482d-9264-e4179f61e379"; + + private const string CatletMetadata = "15e2b061-c625-4469-9fe7-7c455058fcc0"; + // ReSharper restore InconsistentNaming + + + private readonly Mock _ipPoolManagerMock = new(); private readonly SqliteConnection _connection; @@ -41,17 +58,17 @@ public CatletIpManagerTests() public async Task Adds_catlet_network_port_to_expected_pool(string projectIdString, string subnetIdString, string environment, string? network, string? subnet, string? pool) { - var networkConfig = new CatletNetworkConfig(); - if (network != null) + var networkConfig = new CatletNetworkConfig() { - networkConfig.Name = network; - if (subnet != null || pool!= null) - networkConfig.SubnetV4 = new CatletSubnetConfig + Name = network, + SubnetV4 = subnet != null || pool != null + ? new CatletSubnetConfig { Name = subnet ?? "default", IpPool = pool - }; - } + } + : null, + }; var catletPort = new CatletNetworkPort { @@ -62,47 +79,19 @@ public async Task Adds_catlet_network_port_to_expected_pool(string projectIdStri var projectId = Guid.Parse(projectIdString); var subnetId = Guid.Parse(subnetIdString); - var contextOptions = new DbContextOptionsBuilder() - .UseSqlite(_connection) - .Options; - - await using var context = new SqliteStateStoreContext(contextOptions); - var stateStore = new StateStore(context); - await context.Database.EnsureCreatedAsync(); - - var networkRepo = stateStore.For(); - foreach (var virtualNetwork in CreateNetworks()) - { - await networkRepo.AddAsync(virtualNetwork); - } - - await context.SaveChangesAsync(); - - var poolManager = new Mock(); - poolManager.Setup(x => x.AcquireIp(subnetId, - pool ?? "default", It.IsAny())) - .Returns(Prelude.RightAsync(new IpPoolAssignment - { - IpAddress = "192.168.2.1" - })) - .Verifiable(); + ArrangeAcquireIp(subnetId, pool ?? "default", "192.168.2.1"); - try + await WithScope(async (ipManager, _) => { - var ipManager = new CatletIpManager(stateStore, poolManager.Object); var result = await ipManager.ConfigurePortIps(projectId, - environment, catletPort, new[] { networkConfig }, + environment, catletPort, networkConfig, CancellationToken.None); - var addresses = result.Should().BeRight().Subject; - addresses.Should().HaveCount(1); - addresses[0].ToString().Should().Be("192.168.2.1"); - } - finally - { - poolManager.Verify(); - } + result.Should().BeRight().Which.Should().SatisfyRespectively( + ipAddress => ipAddress.ToString().Should().Be("192.168.2.1")); + }); + _ipPoolManagerMock.Verify(); } [Fact] @@ -120,8 +109,8 @@ public async Task Deletes_Invalid_Port() Name = "test-catlet-port", NetworkId = Guid.Parse(NetworkA_Default_Subnet), CatletMetadataId = Guid.Parse(CatletMetadata), - IpAssignments = new List - { + IpAssignments = + [ new IpPoolAssignment() { SubnetId = Guid.Parse(NetworkA_Default_Subnet), @@ -131,7 +120,7 @@ public async Task Deletes_Invalid_Port() Name = "other" }, } - } + ] }; var contextOptions = new DbContextOptionsBuilder() @@ -146,7 +135,7 @@ public async Task Deletes_Invalid_Port() port!.IpAssignments.Should().HaveCount(1); var result = await ipManager.ConfigurePortIps(projectId, - "default", catletPort, new[] { networkConfig }, + "default", catletPort, networkConfig, CancellationToken.None); result.Should().BeRight(); @@ -158,6 +147,12 @@ public async Task Deletes_Invalid_Port() port!.IpAssignments.Should().BeNullOrEmpty(); } + [Fact] + public async Task ExistingAssignmentInDifferentEnvironment_RemovesOldAssignment() + { + + } + [Fact] public async Task Keeps_Valid_Port() { @@ -200,7 +195,7 @@ public async Task Keeps_Valid_Port() port!.IpAssignments.Should().HaveCount(1); var result = await ipManager.ConfigurePortIps(projectId, - "default", catletPort, new[] { networkConfig }, + "default", catletPort, networkConfig, CancellationToken.None); result.Should().BeRight().Subject.Should().HaveCount(1) @@ -241,20 +236,6 @@ await stateStore.For().AddAsync(new() return ipManager; } - // ReSharper disable InconsistentNaming - private const string ProjectA = "{96BBD6D7-01F9-4001-8C86-3FBA75BAA1B5}"; - private const string NetworkA_Default = "{CB58FE00-3F64-4B66-B58E-23FB15DF3CAC}"; - private const string NetworkA_Default_Subnet = "{ED6697CD-836F-4DA7-914B-B09ED1567934}"; - private const string NetworkA_Other_Subnet = "{29FB8B37-4779-427A-BC5C-9A5ECCFFD5E2}"; - - private const string ProjectB = "{75C27DAF-77C8-4B98-A072-A4706DCEB422}"; - private const string NetworkB_Default = "{A0CE4B1A-03E6-413B-A048-567079B49B28}"; - private const string NetworkB_Env2_Default = "{29408ED0-F876-4879-9FAA-DEB519D1DF0A}"; - private const string NetworkB_Default_Subnet = "{AC451FA5-3364-4593-AA4D-14F95529FD54}"; - private const string NetworkB_Env2_Subnet = "{91A25D95-F417-482D-9264-E4179F61E379}"; - - private const string CatletMetadata = "{15E2B061-C625-4469-9FE7-7C455058FCC0}"; - // ReSharper restore InconsistentNaming private static IEnumerable CreateNetworks() @@ -350,5 +331,123 @@ private static IEnumerable CreateNetworks() } }; } + + private async Task WithScope(Func func) + { + await using var scope = CreateScope(); + var catletIpManager = scope.GetInstance(); + var stateStore = scope.GetInstance(); + await func(catletIpManager, stateStore); + } + + private void ArrangeAcquireIp(Guid subnetId, string poolName, string ipAddress) + { + _ipPoolManagerMock + .Setup(x => x.AcquireIp(subnetId, poolName, It.IsAny())) + .Returns(Prelude.RightAsync(new IpPoolAssignment + { + IpAddress = ipAddress + })) + .Verifiable(); + } + + protected override void AddSimpleInjector(SimpleInjectorAddOptions options) + { + options.Container.RegisterInstance(_ipPoolManagerMock.Object); + options.Container.Register(Lifestyle.Scoped); + } + + protected override async Task SeedAsync(IStateStore stateStore) + { + await SeedDefaultTenantAndProject(); + + var projectA = new Project() + { + Id = Guid.Parse(ProjectA), + Name = "project-a", + TenantId = EryphConstants.DefaultTenantId, + }; + await stateStore.For().AddAsync(projectA); + + var projectB = new Project() + { + Id = Guid.Parse(ProjectB), + Name = "project-b", + TenantId = EryphConstants.DefaultTenantId, + }; + await stateStore.For().AddAsync(projectB); + + var projectADefaultEnvNetwork = new VirtualNetwork + { + Id = Guid.Parse(NetworkA_Default), + Project = projectA, + Name = "default", + Environment = "default", + ResourceType = ResourceType.VirtualNetwork, + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.Parse(NetworkA_Default_Subnet), + Name = "default" + }, + new VirtualNetworkSubnet + { + Id = Guid.Parse(NetworkA_Other_Subnet), + Name = "other" + }, + ], + }; + await stateStore.For().AddAsync(projectADefaultEnvNetwork); + + var projectBDefaultEnvNetwork = new VirtualNetwork + { + Id = Guid.Parse(NetworkB_Default), + Project = projectB, + Name = "default", + Environment = "default", + ResourceType = ResourceType.VirtualNetwork, + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.Parse(NetworkB_Default_Subnet), + Name = "default", + IpPools = + [ + new IpPool + { + Id = Guid.NewGuid(), + Name = "default", + }, + new IpPool + { + Id = Guid.NewGuid(), + Name = "other", + }, + ], + }, + ], + }; + await stateStore.For().AddAsync(projectBDefaultEnvNetwork); + + var projectBOtherEnvNetwork = new VirtualNetwork + { + Id = Guid.Parse(NetworkB_Env2_Default), + Project = projectB, + Name = "default", + Environment = "env2", + ResourceType = ResourceType.VirtualNetwork, + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.Parse(NetworkB_Env2_Subnet), + Name = "default" + }, + ], + }; + await stateStore.For().AddAsync(projectBOtherEnvNetwork); + } } } From 3a6ff4fa514bf33f35a255e1bfdfe82b2179bef5 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Tue, 5 Nov 2024 17:34:55 +0100 Subject: [PATCH 02/30] Improve tests --- .../Networks/CatletIpManagerTests.cs | 417 ++++++++---------- 1 file changed, 183 insertions(+), 234 deletions(-) diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs index 325bc3995..3d6ab277c 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs @@ -22,18 +22,18 @@ namespace Eryph.Modules.Controller.Tests.Networks public sealed class CatletIpManagerTests : InMemoryStateDbTestBase { // ReSharper disable InconsistentNaming - private const string ProjectA = "96bbd6d7-01f9-4001-8c86-3fba75baa1b5"; - private const string NetworkA_Default = "cb58fe00-3f64-4b66-b58e-23fb15df3cac"; + private const string ProjectAId = "96bbd6d7-01f9-4001-8c86-3fba75baa1b5"; + private const string ProjectA_NetworkId = "cb58fe00-3f64-4b66-b58e-23fb15df3cac"; private const string NetworkA_Default_Subnet = "ed6697cd-836f-4da7-914b-b09ed1567934"; private const string NetworkA_Other_Subnet = "29fb8b37-4779-427a-bc5c-9a5eccffd5e2"; - private const string ProjectB = "75c27daf-77c8-4b98-a072-a4706dceb422"; + private const string ProjectBId = "75c27daf-77c8-4b98-a072-a4706dceb422"; private const string NetworkB_Default = "a0ce4b1a-03e6-413b-a048-567079b49b28"; private const string NetworkB_Env2_Default = "29408ed0-f876-4879-9faa-deb519d1df0a"; private const string NetworkB_Default_Subnet = "ac451fa5-3364-4593-aa4d-14f95529fd54"; private const string NetworkB_Env2_Subnet = "91a25d95-f417-482d-9264-e4179f61e379"; - private const string CatletMetadata = "15e2b061-c625-4469-9fe7-7c455058fcc0"; + private const string CatletMetadataId = "15e2b061-c625-4469-9fe7-7c455058fcc0"; // ReSharper restore InconsistentNaming @@ -49,14 +49,20 @@ public CatletIpManagerTests() [Theory] - [InlineData(ProjectA, NetworkA_Default_Subnet, "default", null, null, null)] - [InlineData(ProjectA, NetworkA_Other_Subnet, "default", "default", "other", null)] - [InlineData(ProjectB, NetworkB_Default_Subnet, "default", "default", "default", "other")] - [InlineData(ProjectB, NetworkB_Default_Subnet, "default", null, null, null)] - [InlineData(ProjectB, NetworkB_Env2_Subnet, "env2", null, null, null)] - [InlineData(ProjectA, NetworkA_Default_Subnet, "env2", null, null, null)] - public async Task Adds_catlet_network_port_to_expected_pool(string projectIdString, string subnetIdString, - string environment, string? network, string? subnet, string? pool) + [InlineData(ProjectAId, ProjectA_NetworkId, "default", null, null, null, "192.0.2.1")] + [InlineData(ProjectAId, ProjectA_NetworkId, "default", "default", "other", null, "192.0.2.17")] + [InlineData(ProjectAId, ProjectA_NetworkId, "env2", null, null, null, "192.0.2.1")] + [InlineData(ProjectBId, NetworkB_Default, "default", null, null, null, "192.0.2.33")] + [InlineData(ProjectBId, NetworkB_Default, "default", "default", "default", "other", "192.0.2.49")] + [InlineData(ProjectBId, NetworkB_Env2_Default, "env2", null, null, null, "192.0.2.65")] + public async Task Adds_catlet_network_port_to_expected_pool( + string projectId, + string networkId, + string environment, + string? network, + string? subnet, + string? pool, + string expectedIpAddress) { var networkConfig = new CatletNetworkConfig() { @@ -70,81 +76,91 @@ public async Task Adds_catlet_network_port_to_expected_pool(string projectIdStri : null, }; - var catletPort = new CatletNetworkPort + await WithScope(async (ipManager, _, stateStore) => { - Id = Guid.NewGuid(), - Name = "test-catlet-port", - CatletMetadataId = Guid.Parse(CatletMetadata), - }; - var projectId = Guid.Parse(projectIdString); - var subnetId = Guid.Parse(subnetIdString); - - ArrangeAcquireIp(subnetId, pool ?? "default", "192.168.2.1"); - - await WithScope(async (ipManager, _) => - { - var result = await ipManager.ConfigurePortIps(projectId, + var catletPort = new CatletNetworkPort + { + Id = Guid.NewGuid(), + Name = "test-catlet-port", + NetworkId = Guid.Parse(networkId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + }; + await stateStore.For().AddAsync(catletPort); + + var result = await ipManager.ConfigurePortIps( + Guid.Parse(projectId), environment, catletPort, networkConfig, CancellationToken.None); result.Should().BeRight().Which.Should().SatisfyRespectively( - ipAddress => ipAddress.ToString().Should().Be("192.168.2.1")); + ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); }); _ipPoolManagerMock.Verify(); } + // TODO Parameterize for different subnets [Fact] public async Task Deletes_Invalid_Port() { - var projectId = Guid.Parse(ProjectA); + var projectId = Guid.Parse(ProjectAId); var networkConfig = new CatletNetworkConfig() { Name = "default" }; - - var catletPort = new CatletNetworkPort + + var catletPortId = Guid.NewGuid(); + var ipAssignmentId = Guid.Empty; + await WithScope(async (_, ipPoolManager, stateStore) => { - Id = Guid.NewGuid(), - Name = "test-catlet-port", - NetworkId = Guid.Parse(NetworkA_Default_Subnet), - CatletMetadataId = Guid.Parse(CatletMetadata), - IpAssignments = - [ - new IpPoolAssignment() - { - SubnetId = Guid.Parse(NetworkA_Default_Subnet), - Pool = new IpPool - { - SubnetId = Guid.Parse(NetworkA_Default_Subnet), - Name = "other" - }, - } - ] - }; + var ipAssignmentResult = ipPoolManager.AcquireIp( + Guid.Parse(NetworkA_Other_Subnet), + EryphConstants.DefaultIpPoolName); + var ipAssignment = ipAssignmentResult.Should().BeRight().Subject; + ipAssignmentId = ipAssignment.Id; - var contextOptions = new DbContextOptionsBuilder() - .UseSqlite(_connection) - .Options; + var catletPort = new CatletNetworkPort + { + Id = catletPortId, + Name = "test-catlet-port", + NetworkId = Guid.Parse(ProjectA_NetworkId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + IpAssignments = [ipAssignment], + }; + + await stateStore.For().AddAsync(catletPort); + await stateStore.SaveChangesAsync(); + }); - await using var context = new SqliteStateStoreContext(contextOptions); - var ipManager = await SetupPortTest(context, catletPort); - var stateStore = new StateStore(context); - var port = await stateStore.Read() - .GetByIdAsync(catletPort.Id, CancellationToken.None); - port!.IpAssignments.Should().HaveCount(1); + await WithScope(async (catletIpManager, _, stateStore) => + { + var catletPort = await stateStore.Read() + .GetByIdAsync(catletPortId); + catletPort.Should().NotBeNull(); + await stateStore.LoadCollectionAsync(catletPort!, p => p.IpAssignments); + catletPort!.IpAssignments.Should().SatisfyRespectively( + ipAssignment => ipAssignment.Id.Should().Be(ipAssignmentId)); + + var result = await catletIpManager.ConfigurePortIps(projectId, + "default", catletPort, networkConfig, + CancellationToken.None); - var result = await ipManager.ConfigurePortIps(projectId, - "default", catletPort, networkConfig, - CancellationToken.None); + result.Should().BeRight().Which.Should().SatisfyRespectively( + ipAddress => ipAddress.ToString().Should().Be("192.0.2.1")); - result.Should().BeRight(); + await stateStore.SaveChangesAsync(); + }); - await context.SaveChangesAsync(); - port = await stateStore.Read() - .GetByIdAsync(catletPort.Id, CancellationToken.None); + await WithScope(async (_, _, stateStore) => + { + var catletPort = await stateStore.Read() + .GetByIdAsync(catletPortId); + catletPort.Should().NotBeNull(); + await stateStore.LoadCollectionAsync(catletPort!, p => p.IpAssignments); - port!.IpAssignments.Should().BeNullOrEmpty(); + catletPort!.IpAssignments.Should().SatisfyRespectively( + ipAssignment => ipAssignment.IpAddress.Should().Be("192.0.2.1")); + }); } [Fact] @@ -156,188 +172,72 @@ public async Task ExistingAssignmentInDifferentEnvironment_RemovesOldAssignment( [Fact] public async Task Keeps_Valid_Port() { - var projectId = Guid.Parse(ProjectA); + var projectId = Guid.Parse(ProjectAId); var networkConfig = new CatletNetworkConfig() { Name = "default" }; - var catletPort = new CatletNetworkPort - { - Id = Guid.NewGuid(), - Name = "test-catlet-port", - NetworkId = Guid.Parse(NetworkA_Default_Subnet), - CatletMetadataId = Guid.Parse(CatletMetadata), - IpAssignments = new List - { - new IpPoolAssignment() - { - SubnetId = Guid.Parse(NetworkA_Default_Subnet), - IpAddress = "192.168.2.10", - Pool = new IpPool - { - SubnetId = Guid.Parse(NetworkA_Default_Subnet), - Name = "default" - }, - } - } - }; - - var contextOptions = new DbContextOptionsBuilder() - .UseSqlite(_connection) - .Options; - - await using var context = new SqliteStateStoreContext(contextOptions); - var ipManager = await SetupPortTest(context, catletPort); - var stateStore = new StateStore(context); - var port = await stateStore.Read() - .GetByIdAsync(catletPort.Id, CancellationToken.None); - port!.IpAssignments.Should().HaveCount(1); - - var result = await ipManager.ConfigurePortIps(projectId, - "default", catletPort, networkConfig, - CancellationToken.None); - - result.Should().BeRight().Subject.Should().HaveCount(1) - .And.Subject.First().ToString().Should().Be("192.168.2.10"); - - } - - private async Task SetupPortTest(StateStoreContext context, CatletNetworkPort catletPort) - { - var subnetId = Guid.Parse(NetworkA_Default_Subnet); - - await context.Database.EnsureCreatedAsync(); - var stateStore = new StateStore(context); - var networkRepo = stateStore.For(); - var network = CreateNetworks().First(); - network.NetworkPorts = new List + var catletPortId = Guid.NewGuid(); + var ipAssignmentId = Guid.Empty; + await WithScope(async (_, ipPoolManager, stateStore) => { - catletPort - }; - await networkRepo.AddAsync(network); - - await stateStore.For().AddAsync(new() - { - Id = Guid.Parse(CatletMetadata), - }); + var ipAssignmentResult = ipPoolManager.AcquireIp( + Guid.Parse(NetworkA_Default_Subnet), + EryphConstants.DefaultIpPoolName); + var ipAssignment = ipAssignmentResult.Should().BeRight().Subject; + ipAssignmentId = ipAssignment.Id; - await context.SaveChangesAsync(); - - var poolManager = new Mock(); - poolManager.Setup(x => x.AcquireIp(subnetId, "default", - It.IsAny())) - .Returns(Prelude.RightAsync(new IpPoolAssignment + var catletPort = new CatletNetworkPort { - IpAddress = "192.168.2.1" - })); - - var ipManager = new CatletIpManager(stateStore, poolManager.Object); - return ipManager; - } - - + Id = catletPortId, + Name = "test-catlet-port", + NetworkId = Guid.Parse(ProjectA_NetworkId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + IpAssignments = [ipAssignment] + }; + + await stateStore.For().AddAsync(catletPort); + await stateStore.SaveChangesAsync(); + }); - private static IEnumerable CreateNetworks() - { - var tenant = new Tenant + await WithScope(async (catletIpManager, _, stateStore) => { - Id = Guid.NewGuid(), - }; + var catletPort = await stateStore.For() + .GetByIdAsync(catletPortId); + await stateStore.LoadCollectionAsync(catletPort!, p => p.IpAssignments); + catletPort!.IpAssignments.Should().SatisfyRespectively( + ipAssignment => ipAssignment.Id.Should().Be(ipAssignmentId)); + + var result = await catletIpManager.ConfigurePortIps(projectId, + "default", catletPort, networkConfig, + CancellationToken.None); - var projectA = new Project - { - Id = Guid.Parse(ProjectA), - Name = "projectA", - Tenant = tenant - }; + result.Should().BeRight().Which.Should().SatisfyRespectively( + ipAddress => ipAddress.ToString().Should().Be("192.0.2.1")); + + await stateStore.SaveChangesAsync(); + }); - var projectB = new Project + await WithScope(async (_, _, stateStore) => { - Id = Guid.Parse(ProjectB), - Name = "projectB", - Tenant = tenant - }; + var catletPort = await stateStore.Read() + .GetByIdAsync(catletPortId); + catletPort.Should().NotBeNull(); + await stateStore.LoadCollectionAsync(catletPort!, p => p.IpAssignments); - return new[] - { - new VirtualNetwork - { - Id = Guid.Parse(NetworkA_Default), - Project = projectA, - Name = "default", - Environment = "default", - ResourceType = ResourceType.VirtualNetwork, - Subnets = new[] - { - new VirtualNetworkSubnet - { - Id = Guid.Parse(NetworkA_Default_Subnet), - Name = "default" - }, - new VirtualNetworkSubnet - { - Id = Guid.Parse(NetworkA_Other_Subnet), - Name = "other" - } - }.ToList() - }, - new VirtualNetwork - { - Id = Guid.Parse(NetworkB_Default), - Project = projectB, - Name = "default", - Environment = "default", - ResourceType = ResourceType.VirtualNetwork, - Subnets = new[] - { - new VirtualNetworkSubnet - { - Id = Guid.Parse(NetworkB_Default_Subnet), - Name = "default", - IpPools = new List(new [] - { - new IpPool - { - Id = Guid.NewGuid(), - Name = "default", - }, - new IpPool - { - Id = Guid.NewGuid(), - Name = "other", - } - }) - }, - }.ToList(), - - }, - new VirtualNetwork - { - Id = Guid.Parse(NetworkB_Env2_Default), - Project = projectB, - Name = "default", - Environment = "env2", - ResourceType = ResourceType.VirtualNetwork, - Subnets = new[] - { - new VirtualNetworkSubnet - { - Id = Guid.Parse(NetworkB_Env2_Subnet), - Name = "default" - }, - }.ToList(), - - } - }; + catletPort!.IpAssignments.Should().SatisfyRespectively( + ipAssignment => ipAssignment.Id.Should().Be(ipAssignmentId)); + }); } - private async Task WithScope(Func func) + private async Task WithScope(Func func) { await using var scope = CreateScope(); var catletIpManager = scope.GetInstance(); + var ipPoolManager = scope.GetInstance(); var stateStore = scope.GetInstance(); - await func(catletIpManager, stateStore); + await func(catletIpManager, ipPoolManager, stateStore); } private void ArrangeAcquireIp(Guid subnetId, string poolName, string ipAddress) @@ -353,7 +253,7 @@ private void ArrangeAcquireIp(Guid subnetId, string poolName, string ipAddress) protected override void AddSimpleInjector(SimpleInjectorAddOptions options) { - options.Container.RegisterInstance(_ipPoolManagerMock.Object); + options.Container.Register(Lifestyle.Scoped); options.Container.Register(Lifestyle.Scoped); } @@ -361,9 +261,14 @@ protected override async Task SeedAsync(IStateStore stateStore) { await SeedDefaultTenantAndProject(); + await stateStore.For().AddAsync(new CatletMetadata + { + Id = Guid.Parse(CatletMetadataId), + }); + var projectA = new Project() { - Id = Guid.Parse(ProjectA), + Id = Guid.Parse(ProjectAId), Name = "project-a", TenantId = EryphConstants.DefaultTenantId, }; @@ -371,7 +276,7 @@ protected override async Task SeedAsync(IStateStore stateStore) var projectB = new Project() { - Id = Guid.Parse(ProjectB), + Id = Guid.Parse(ProjectBId), Name = "project-b", TenantId = EryphConstants.DefaultTenantId, }; @@ -379,22 +284,46 @@ protected override async Task SeedAsync(IStateStore stateStore) var projectADefaultEnvNetwork = new VirtualNetwork { - Id = Guid.Parse(NetworkA_Default), + Id = Guid.Parse(ProjectA_NetworkId), Project = projectA, - Name = "default", - Environment = "default", + Name = EryphConstants.DefaultNetworkName, + Environment = EryphConstants.DefaultEnvironmentName, ResourceType = ResourceType.VirtualNetwork, Subnets = [ new VirtualNetworkSubnet { Id = Guid.Parse(NetworkA_Default_Subnet), - Name = "default" + Name = EryphConstants.DefaultSubnetName, + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "192.0.2.0/28", + FirstIp = "192.0.2.1", + NextIp = "192.0.2.1", + LastIp = "192.0.2.11", + } + ], }, new VirtualNetworkSubnet { Id = Guid.Parse(NetworkA_Other_Subnet), - Name = "other" + Name = "other", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "192.0.2.16/28", + FirstIp = "192.0.2.17", + NextIp = "192.0.2.17", + LastIp = "192.0.2.27", + } + ], }, ], }; @@ -419,11 +348,19 @@ protected override async Task SeedAsync(IStateStore stateStore) { Id = Guid.NewGuid(), Name = "default", + IpNetwork = "192.0.2.32/28", + FirstIp = "192.0.2.33", + NextIp = "192.0.2.33", + LastIp = "192.0.2.43", }, new IpPool { Id = Guid.NewGuid(), Name = "other", + IpNetwork = "192.0.2.48/28", + FirstIp = "192.0.2.49", + NextIp = "192.0.2.49", + LastIp = "192.0.2.59", }, ], }, @@ -443,7 +380,19 @@ protected override async Task SeedAsync(IStateStore stateStore) new VirtualNetworkSubnet { Id = Guid.Parse(NetworkB_Env2_Subnet), - Name = "default" + Name = "default", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "192.0.2.64/28", + FirstIp = "192.0.2.65", + NextIp = "192.0.2.65", + LastIp = "192.0.2.75", + } + ], }, ], }; From b878b5f869cd21e4e1956c66f6b85cf63db4938a Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Mon, 11 Nov 2024 15:35:29 +0100 Subject: [PATCH 03/30] Add todos --- .../src/Eryph.Modules.Controller/Networks/CatletIpManager.cs | 1 - .../Networks/UpdateCatletNetworksCommandHandler.cs | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs index e9aa4babe..ee769d021 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs @@ -13,7 +13,6 @@ using LanguageExt.Common; using static LanguageExt.Prelude; -using Array = System.Array; namespace Eryph.Modules.Controller.Networks; diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs index 9f3056d89..0e6938e37 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs @@ -92,6 +92,9 @@ from networkProvider in networkProviders.NetworkProviders.Find(x => x.Name == ne let c1 = new CancellationTokenSource(500000) + // TODO update assignment of port to network when the network changed in the config + // TODO delete ports which are no longer configured + from networkPort in GetOrAddAdapterPort( network, command.CatletId, catletMetadataId, networkConfig.AdapterName, command.Config.Hostname ?? command.Config.Name, c1.Token) From dedf99e9f7751d93e74989e47a39b0cea214de25 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Mon, 11 Nov 2024 19:35:18 +0100 Subject: [PATCH 04/30] * Fix incorrect cloud-init network config --- .../src/Eryph.VmManagement/Converging/ConvergeCloudInitDisk.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/src/Eryph.VmManagement/Converging/ConvergeCloudInitDisk.cs b/src/core/src/Eryph.VmManagement/Converging/ConvergeCloudInitDisk.cs index 01e759632..e87d3e0cd 100644 --- a/src/core/src/Eryph.VmManagement/Converging/ConvergeCloudInitDisk.cs +++ b/src/core/src/Eryph.VmManagement/Converging/ConvergeCloudInitDisk.cs @@ -76,7 +76,6 @@ private async Task> GenerateNetworkData(TypedPsObject var physicalNetworkSettings = new { type = "physical", - id = adapter.Value.Name, name = adapter.Value.Name, mac_address = macFormatted, subnets = (Context.Config.Networks?.Filter(x=>x.AdapterName == adapter.Value.Name) From 1d4bd47bd252da83648f8906c715dc64af06ff26 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Mon, 11 Nov 2024 20:35:26 +0100 Subject: [PATCH 05/30] Make MAC address mandatory in the database and fix tests --- ... 20241111192857_InitialCreate.Designer.cs} | 3 +- ...ate.cs => 20241111192857_InitialCreate.cs} | 2 +- .../MySqlStateStoreContextModelSnapshot.cs | 1 + ... 20241111192853_InitialCreate.Designer.cs} | 3 +- ...ate.cs => 20241111192853_InitialCreate.cs} | 2 +- .../SqliteStateStoreContextModelSnapshot.cs | 1 + .../src/Eryph.StateDb/Model/NetworkPort.cs | 3 +- .../Specifications/NetworkPortSpecs.cs | 9 ++ .../UpdateCatletNetworksCommandHandler.cs | 96 +++++++++++-------- .../NetworkProvidersChangeTrackingTests.cs | 2 + .../VirtualNetworkChangeTrackingTests.cs | 4 + .../Networks/CatletIpManagerTests.cs | 30 +----- .../Networks/IpPoolManagerTests.cs | 1 + .../Networks/NetworkConfigRealizerTests.cs | 4 + .../Networks/NetworkConfigValidatorTests.cs | 4 + .../StateDbDeleteTests.cs | 1 + 16 files changed, 96 insertions(+), 70 deletions(-) rename src/data/src/Eryph.StateDb.MySql/Migrations/{20241023105139_InitialCreate.Designer.cs => 20241111192857_InitialCreate.Designer.cs} (99%) rename src/data/src/Eryph.StateDb.MySql/Migrations/{20241023105139_InitialCreate.cs => 20241111192857_InitialCreate.cs} (99%) rename src/data/src/Eryph.StateDb.Sqlite/Migrations/{20241023105135_InitialCreate.Designer.cs => 20241111192853_InitialCreate.Designer.cs} (99%) rename src/data/src/Eryph.StateDb.Sqlite/Migrations/{20241023105135_InitialCreate.cs => 20241111192853_InitialCreate.cs} (99%) diff --git a/src/data/src/Eryph.StateDb.MySql/Migrations/20241023105139_InitialCreate.Designer.cs b/src/data/src/Eryph.StateDb.MySql/Migrations/20241111192857_InitialCreate.Designer.cs similarity index 99% rename from src/data/src/Eryph.StateDb.MySql/Migrations/20241023105139_InitialCreate.Designer.cs rename to src/data/src/Eryph.StateDb.MySql/Migrations/20241111192857_InitialCreate.Designer.cs index edb7f096c..ec093d04c 100644 --- a/src/data/src/Eryph.StateDb.MySql/Migrations/20241023105139_InitialCreate.Designer.cs +++ b/src/data/src/Eryph.StateDb.MySql/Migrations/20241111192857_InitialCreate.Designer.cs @@ -12,7 +12,7 @@ namespace Eryph.StateDb.MySql.Migrations { [DbContext(typeof(MySqlStateStoreContext))] - [Migration("20241023105139_InitialCreate")] + [Migration("20241111192857_InitialCreate")] partial class InitialCreate { /// @@ -244,6 +244,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("varchar(21)"); b.Property("MacAddress") + .IsRequired() .HasColumnType("varchar(255)"); b.Property("Name") diff --git a/src/data/src/Eryph.StateDb.MySql/Migrations/20241023105139_InitialCreate.cs b/src/data/src/Eryph.StateDb.MySql/Migrations/20241111192857_InitialCreate.cs similarity index 99% rename from src/data/src/Eryph.StateDb.MySql/Migrations/20241023105139_InitialCreate.cs rename to src/data/src/Eryph.StateDb.MySql/Migrations/20241111192857_InitialCreate.cs index abb4bba33..98744b5d9 100644 --- a/src/data/src/Eryph.StateDb.MySql/Migrations/20241023105139_InitialCreate.cs +++ b/src/data/src/Eryph.StateDb.MySql/Migrations/20241111192857_InitialCreate.cs @@ -461,7 +461,7 @@ protected override void Up(MigrationBuilder migrationBuilder) Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), ProviderName = table.Column(type: "longtext", nullable: true) .Annotation("MySql:CharSet", "utf8mb4"), - MacAddress = table.Column(type: "varchar(255)", nullable: true) + MacAddress = table.Column(type: "varchar(255)", nullable: false) .Annotation("MySql:CharSet", "utf8mb4"), AddressName = table.Column(type: "longtext", nullable: true) .Annotation("MySql:CharSet", "utf8mb4"), diff --git a/src/data/src/Eryph.StateDb.MySql/Migrations/MySqlStateStoreContextModelSnapshot.cs b/src/data/src/Eryph.StateDb.MySql/Migrations/MySqlStateStoreContextModelSnapshot.cs index bcd75bdd0..ca1db2b0b 100644 --- a/src/data/src/Eryph.StateDb.MySql/Migrations/MySqlStateStoreContextModelSnapshot.cs +++ b/src/data/src/Eryph.StateDb.MySql/Migrations/MySqlStateStoreContextModelSnapshot.cs @@ -241,6 +241,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("varchar(21)"); b.Property("MacAddress") + .IsRequired() .HasColumnType("varchar(255)"); b.Property("Name") diff --git a/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241023105135_InitialCreate.Designer.cs b/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241111192853_InitialCreate.Designer.cs similarity index 99% rename from src/data/src/Eryph.StateDb.Sqlite/Migrations/20241023105135_InitialCreate.Designer.cs rename to src/data/src/Eryph.StateDb.Sqlite/Migrations/20241111192853_InitialCreate.Designer.cs index bb80a4b60..06d288497 100644 --- a/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241023105135_InitialCreate.Designer.cs +++ b/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241111192853_InitialCreate.Designer.cs @@ -11,7 +11,7 @@ namespace Eryph.StateDb.Sqlite.Migrations { [DbContext(typeof(SqliteStateStoreContext))] - [Migration("20241023105135_InitialCreate")] + [Migration("20241111192853_InitialCreate")] partial class InitialCreate { /// @@ -239,6 +239,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("MacAddress") + .IsRequired() .HasColumnType("TEXT"); b.Property("Name") diff --git a/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241023105135_InitialCreate.cs b/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241111192853_InitialCreate.cs similarity index 99% rename from src/data/src/Eryph.StateDb.Sqlite/Migrations/20241023105135_InitialCreate.cs rename to src/data/src/Eryph.StateDb.Sqlite/Migrations/20241111192853_InitialCreate.cs index 2a3e3d6e2..605250a27 100644 --- a/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241023105135_InitialCreate.cs +++ b/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241111192853_InitialCreate.cs @@ -395,7 +395,7 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "TEXT", nullable: false), ProviderName = table.Column(type: "TEXT", nullable: true), - MacAddress = table.Column(type: "TEXT", nullable: true), + MacAddress = table.Column(type: "TEXT", nullable: false), AddressName = table.Column(type: "TEXT", nullable: true), Name = table.Column(type: "TEXT", nullable: false), Discriminator = table.Column(type: "TEXT", maxLength: 21, nullable: false), diff --git a/src/data/src/Eryph.StateDb.Sqlite/Migrations/SqliteStateStoreContextModelSnapshot.cs b/src/data/src/Eryph.StateDb.Sqlite/Migrations/SqliteStateStoreContextModelSnapshot.cs index 9019ee83b..ab60f2c3a 100644 --- a/src/data/src/Eryph.StateDb.Sqlite/Migrations/SqliteStateStoreContextModelSnapshot.cs +++ b/src/data/src/Eryph.StateDb.Sqlite/Migrations/SqliteStateStoreContextModelSnapshot.cs @@ -236,6 +236,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("MacAddress") + .IsRequired() .HasColumnType("TEXT"); b.Property("Name") diff --git a/src/data/src/Eryph.StateDb/Model/NetworkPort.cs b/src/data/src/Eryph.StateDb/Model/NetworkPort.cs index d019f5e58..6c49c68ab 100644 --- a/src/data/src/Eryph.StateDb/Model/NetworkPort.cs +++ b/src/data/src/Eryph.StateDb/Model/NetworkPort.cs @@ -8,7 +8,8 @@ public abstract class NetworkPort public string? ProviderName { get; set; } public Guid Id { get; set; } - public string? MacAddress { get; set; } + + public required string MacAddress { get; set; } public string? AddressName { get; set; } diff --git a/src/data/src/Eryph.StateDb/Specifications/NetworkPortSpecs.cs b/src/data/src/Eryph.StateDb/Specifications/NetworkPortSpecs.cs index a982c15e5..570894649 100644 --- a/src/data/src/Eryph.StateDb/Specifications/NetworkPortSpecs.cs +++ b/src/data/src/Eryph.StateDb/Specifications/NetworkPortSpecs.cs @@ -24,5 +24,14 @@ public GetByNetworkAndName(Guid networkId, string name) } + // TODO fix my naming + public sealed class GetByNetworkAndNameForCatlet : Specification, ISingleResultSpecification + { + public GetByNetworkAndNameForCatlet(Guid networkId, string name) + { + Query.Where(x => x.NetworkId == networkId && x.Name == name); + } + + } } \ No newline at end of file diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs index 0e6938e37..2f0058995 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Dbosoft.Rebus.Operations; +using Eryph.ConfigModel; using Eryph.ConfigModel.Catlets; using Eryph.Core; using Eryph.Core.Network; @@ -16,9 +17,12 @@ using Eryph.StateDb.Specifications; using JetBrains.Annotations; using LanguageExt; +using LanguageExt.ClassInstances; using LanguageExt.Common; using Rebus.Handlers; +using static LanguageExt.Prelude; + namespace Eryph.Modules.Controller.Networks; #pragma warning restore 1998 @@ -69,23 +73,23 @@ private EitherAsync UpdateNetwork( Guid catletMetadataId, UpdateCatletNetworksCommand command, CatletNetworkConfig networkConfig) => + from environmentName in EnvironmentName.NewEither(command.Config.Environment) + .ToAsync() + from networkName in EryphNetworkName.NewEither(networkConfig.Name) + .ToAsync() from network in _stateStore.Read().IO.GetBySpecAsync( - new VirtualNetworkSpecs.GetByName(command.ProjectId, networkConfig.Name, command.Config.Environment)) - .Bind(r => - // it is optional to have a environment specific network - // therefore fallback to network in default environment if not found - r.IsNone && command.Config.Environment != "default" - ? _stateStore.Read() - .IO.GetBySpecAsync(new VirtualNetworkSpecs.GetByName(command.ProjectId, networkConfig.Name, - "default")) - .Bind(fr => fr.ToEitherAsync(Error.New( - $"Network {networkConfig.Name} not found in environment {command.Config.Environment} and default environment."))) - : r.ToEitherAsync(Error.New( - $"Environments {command.Config.Environment}: Network {networkConfig.Name} not found."))) + new VirtualNetworkSpecs.GetByName(command.ProjectId, networkName.Value, environmentName.Value)) + // It is optional to have an environment specific network. Therefore, + // we fall back to the network in the default environment. + from validNetwork in network.IsNone && environmentName != EnvironmentName.New(EryphConstants.DefaultEnvironmentName) + ? _stateStore.Read().IO.GetBySpecAsync( + new VirtualNetworkSpecs.GetByName(command.ProjectId, networkName.Value, EryphConstants.DefaultEnvironmentName)) + .Bind(fr => fr.ToEitherAsync(Error.New($"Network {networkName} not found in environment {environmentName} and default environment."))) + : network.ToEitherAsync(Error.New($"Environment {environmentName}: Network {networkName} not found.")) from networkProviders in _providerManager.GetCurrentConfiguration() - from networkProvider in networkProviders.NetworkProviders.Find(x => x.Name == network.NetworkProvider) - .ToEither(Error.New($"network provider {network.NetworkProvider} not found")) + from networkProvider in networkProviders.NetworkProviders.Find(x => x.Name == validNetwork.NetworkProvider) + .ToEither(Error.New($"network provider {validNetwork.NetworkProvider} not found.")) .ToAsync() let isFlatNetwork = networkProvider.Type == NetworkProviderType.Flat @@ -96,7 +100,7 @@ from networkProvider in networkProviders.NetworkProviders.Find(x => x.Name == ne // TODO delete ports which are no longer configured from networkPort in GetOrAddAdapterPort( - network, command.CatletId, catletMetadataId, networkConfig.AdapterName, + validNetwork, command.CatletId, catletMetadataId, networkConfig.AdapterName, command.Config.Hostname ?? command.Config.Name, c1.Token) let c2 = new CancellationTokenSource() @@ -118,7 +122,7 @@ from floatingPort in isFlatNetwork let c3 = new CancellationTokenSource() from ips in isFlatNetwork - ? Prelude.RightAsync(Array.Empty()) + ? Prelude.RightAsync([]) : _ipManager.ConfigurePortIps( command.ProjectId, command.Config.Environment ?? "default", networkPort, @@ -126,14 +130,14 @@ from ips in isFlatNetwork c3.Token) from floatingIps in isFlatNetwork - ? Prelude.RightAsync(Array.Empty()) + ? Prelude.RightAsync([]) : floatingPort.ToEither(Error.New("floating port is missing")) .ToAsync() .Bind(p => _providerIpManager.ConfigureFloatingPortIps(networkProvider, p, c3.Token)) select new MachineNetworkSettings { - NetworkProviderName = network.NetworkProvider, + NetworkProviderName = validNetwork.NetworkProvider, NetworkName = networkConfig.Name, AdapterName = networkConfig.AdapterName, PortName = networkPort.Name, @@ -152,33 +156,41 @@ private EitherAsync GetOrAddAdapterPort( Guid catletMetadataId, string adapterName, string addressName, - CancellationToken cancellationToken) - { - var portName = $"{catletId}_{adapterName}"; - - return _stateStore.For() - .IO.GetBySpecAsync(new NetworkPortSpecs.GetByNetworkAndName(network.Id, portName), cancellationToken) - .MapAsync(async option => await option.IfNoneAsync(() => - { - var port = new CatletNetworkPort + CancellationToken cancellationToken) => + from _ in RightAsync(unit) + let portName = GetPortName(catletId, adapterName) + from existingPort in _stateStore.For().IO.GetBySpecAsync( + new NetworkPortSpecs.GetByNetworkAndNameForCatlet(network.Id, portName), cancellationToken) + from updatedPort in existingPort.Match( + Some: p => from _ in RightAsync(unit) + let __ = fun(() => + { + p.AddressName = addressName; + // TODO use proper fixed MAC address + p.MacAddress = string.IsNullOrEmpty(null) + ? MacAddresses.FormatMacAddress(MacAddresses.GenerateMacAddress(portName)) + : MacAddresses.FormatMacAddress("a"); + }) + select p, + None: () => from _ in RightAsync(unit) + let newPort = new CatletNetworkPort { Id = Guid.NewGuid(), CatletMetadataId = catletMetadataId, Name = portName, + // TODO use proper fixed MAC address + MacAddress = string.IsNullOrEmpty(null) + ? MacAddresses.FormatMacAddress(MacAddresses.GenerateMacAddress(portName)) + : MacAddresses.FormatMacAddress("a"), NetworkId = network.Id, + AddressName = addressName, IpAssignments = new List() + } + from addedPort in _stateStore.For().IO.AddAsync(newPort, cancellationToken) + select addedPort) + select updatedPort; - }; - return _stateStore.For().AddAsync(port, cancellationToken); - } - ).ConfigureAwait(false)) - .Map(p => - { - p.AddressName = addressName; - return (CatletNetworkPort)p; - }); - - } + private async Task> GetOrAddFloatingPort(CatletNetworkPort adapterPort, Option portName, string providerName, string providerSubnetName, string providerPoolName, @@ -231,10 +243,14 @@ private static Unit UpdatePort( networkPort.MacAddress = MacAddresses.FormatMacAddress(fixedMacAddress); else { - networkPort.MacAddress ??= MacAddresses.FormatMacAddress(MacAddresses.GenerateMacAddress( - $"{catletId}_{adapterName}")); + networkPort.MacAddress ??= MacAddresses.FormatMacAddress( + MacAddresses.GenerateMacAddress(GetPortName(catletId, adapterName))); } return Unit.Default; } + + + private static string GetPortName(Guid catletId, string adapterName) => + $"{catletId}_{adapterName}"; } \ No newline at end of file diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/NetworkProvidersChangeTrackingTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/NetworkProvidersChangeTrackingTests.cs index 792854d4f..4abcc5e04 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/NetworkProvidersChangeTrackingTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/NetworkProvidersChangeTrackingTests.cs @@ -94,6 +94,7 @@ await WithHostScope(async stateStore => await stateStore.For().AddAsync(new FloatingNetworkPort() { Name = "new-floating-port", + MacAddress = "00:00:00:00:00:02", ProviderName = "test-provider", SubnetName = "provider-test-subnet", PoolName = "provider-test-pool", @@ -110,6 +111,7 @@ await stateStore.For().AddAsync(new FloatingNetworkPort() new FloatingNetworkPortConfigModel() { Name = "new-floating-port", + MacAddress = "00:00:00:00:00:02", ProviderName = "test-provider", SubnetName = "provider-test-subnet", PoolName = "provider-test-pool", diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs index c4dc2d688..ae0cd1c38 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Mail; using System.Text; using System.Text.Json; using System.Threading; @@ -107,6 +108,7 @@ await WithHostScope(async stateStore => await stateStore.For().AddAsync(new CatletNetworkPort() { Name = "new-catlet-port", + MacAddress = "00:00:00:00:00:02", CatletMetadataId = CatletMetadataId, NetworkId = VirtualNetworkId, }); @@ -124,6 +126,7 @@ await stateStore.For().AddAsync(new CatletNetworkPort() { CatletMetadataId = CatletMetadataId, Name = "new-catlet-port", + MacAddress = "00:00:00:00:00:02", VirtualNetworkName = "virtual-test-network", EnvironmentName = "test-environment", FloatingNetworkPort = null, @@ -495,6 +498,7 @@ await stateStore.For().AddAsync(new FloatingNetworkPort() { Id = FloatingPortId, Name = "test-floating-port", + MacAddress = "00:00:00:00:00:10", ProviderName = "test-provider", SubnetName = "provider-test-subnet", PoolName = "provider-test-pool", diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs index 3d6ab277c..7ddf63faa 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs @@ -35,18 +35,6 @@ public sealed class CatletIpManagerTests : InMemoryStateDbTestBase private const string CatletMetadataId = "15e2b061-c625-4469-9fe7-7c455058fcc0"; // ReSharper restore InconsistentNaming - - - private readonly Mock _ipPoolManagerMock = new(); - - private readonly SqliteConnection _connection; - - public CatletIpManagerTests() - { - _connection = new SqliteConnection("Filename=:memory:"); - _connection.Open(); - } - [Theory] [InlineData(ProjectAId, ProjectA_NetworkId, "default", null, null, null, "192.0.2.1")] @@ -82,6 +70,7 @@ await WithScope(async (ipManager, _, stateStore) => { Id = Guid.NewGuid(), Name = "test-catlet-port", + MacAddress = "00:00:00:00:00:01", NetworkId = Guid.Parse(networkId), CatletMetadataId = Guid.Parse(CatletMetadataId), }; @@ -95,8 +84,6 @@ await WithScope(async (ipManager, _, stateStore) => result.Should().BeRight().Which.Should().SatisfyRespectively( ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); }); - - _ipPoolManagerMock.Verify(); } // TODO Parameterize for different subnets @@ -123,6 +110,7 @@ await WithScope(async (_, ipPoolManager, stateStore) => { Id = catletPortId, Name = "test-catlet-port", + MacAddress = "00:00:00:00:00:01", NetworkId = Guid.Parse(ProjectA_NetworkId), CatletMetadataId = Guid.Parse(CatletMetadataId), IpAssignments = [ipAssignment], @@ -192,6 +180,7 @@ await WithScope(async (_, ipPoolManager, stateStore) => { Id = catletPortId, Name = "test-catlet-port", + MacAddress = "00:00:00:00:00:01", NetworkId = Guid.Parse(ProjectA_NetworkId), CatletMetadataId = Guid.Parse(CatletMetadataId), IpAssignments = [ipAssignment] @@ -240,19 +229,10 @@ private async Task WithScope(Func x.AcquireIp(subnetId, poolName, It.IsAny())) - .Returns(Prelude.RightAsync(new IpPoolAssignment - { - IpAddress = ipAddress - })) - .Verifiable(); - } - protected override void AddSimpleInjector(SimpleInjectorAddOptions options) { + // Use the proper IpPoolManager instead of a mock as the code quite + // interdependent as it modifies the same EF Core entities. options.Container.Register(Lifestyle.Scoped); options.Container.Register(Lifestyle.Scoped); } diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs index 4597071d8..2170f5687 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs @@ -235,6 +235,7 @@ await stateStore.For().AddAsync( { Id = NetworkPortId, Name = "test-catlet-port", + MacAddress = "00:00:00:00:00:01", CatletMetadataId = CatletMetadataId, NetworkId = NetworkId, }); diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs index e30e54782..e47ffbcbc 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs @@ -116,6 +116,7 @@ public async Task Existing_ip_pool_is_updated() var routerPort = new NetworkRouterPort() { Name = "default", + MacAddress = "00:00:00:00:10:10", IpAssignments = new List { new IpPoolAssignment @@ -152,6 +153,7 @@ public async Task Existing_ip_pool_is_updated() new ProviderRouterPort() { Name = "test-provider-port", + MacAddress = "00:00:00:00:00:10", SubnetName = "test-provider-subnet", PoolName = "test-provider-pool", }, @@ -243,6 +245,7 @@ public async Task Cleanup_of_overlay_when_switched_to_flat() var routerPort = new NetworkRouterPort() { Name = "default", + MacAddress = "00:00:00:00:00:10", IpAssignments = new List { new IpPoolAssignment @@ -279,6 +282,7 @@ public async Task Cleanup_of_overlay_when_switched_to_flat() new ProviderRouterPort { Name = "test-provider-port", + MacAddress = "00:00:00:00:10:01", SubnetName = "test-provider-subnet", PoolName = "test-provider-pool", IpAssignments = diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigValidatorTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigValidatorTests.cs index f86121c2f..57bf61f61 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigValidatorTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigValidatorTests.cs @@ -566,11 +566,13 @@ await networkRepo.AddAsync( new CatletNetworkPort() { Name = "test-catlet-port", + MacAddress = "00:00:00:00:00:10", CatletMetadataId = firstCatletMetadata.Id, }, new ProviderRouterPort() { Name = "provider", + MacAddress = "00:00:00:00:00:01", ProviderName = "default", PoolName = "default", SubnetName = "default" @@ -632,12 +634,14 @@ await networkRepo.AddAsync( new CatletNetworkPort() { Id = catletNetworkPortId, + MacAddress = "00:00:00:00:10:10", Name = "test-catlet-port", CatletMetadataId = secondCatletMetadata.Id, }, new ProviderRouterPort() { Name = "provider", + MacAddress = "00:00:00:00:10:01", ProviderName = "default", PoolName = "default", SubnetName = "default" diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs index a772fa642..ccf86b8b8 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs @@ -85,6 +85,7 @@ await stateStore.For().AddAsync(new FloatingNetworkPort() { Id = FloatingPortId, Name = "test-floating-port", + MacAddress = "00:00:00:00:00:10", ProviderName = "test-provider", SubnetName = "provider-test-subnet", PoolName = "provider-test-pool", From d02508bf59e269155c6ad56259d3a6ea884587f6 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Tue, 12 Nov 2024 17:36:05 +0100 Subject: [PATCH 06/30] Cleanup ProviderIpManager Fix tests --- .../Networks/BaseIpManager.cs | 20 +- .../Networks/IProviderIpManager.cs | 8 +- .../Networks/ProviderIpManager.cs | 153 ++--- .../UpdateCatletNetworksCommandHandler.cs | 77 +-- .../Networks/CatletIpManagerTests.cs | 628 ++++++++++-------- .../Networks/ProviderIpManagerTests.cs | 297 +++++++++ 6 files changed, 744 insertions(+), 439 deletions(-) create mode 100644 src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/BaseIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/BaseIpManager.cs index 0c4bd67bd..f25146d0f 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/BaseIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/BaseIpManager.cs @@ -22,22 +22,4 @@ protected static Unit UpdatePortAssignment(NetworkPort port, IpAssignment newAss return Unit.Default; } - - protected static Option CheckAssignmentExist( - IEnumerable validAssignments, - Subnet subnet, string poolName) - { - return validAssignments.Find(x => x.Subnet.Id == subnet.Id) - .Bind(s => - { - if (s is not IpPoolAssignment poolAssignment) - return s; - - return poolAssignment.Pool.Name == poolName - ? s - : Option.None; - }); - } - - -} \ No newline at end of file +} diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/IProviderIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/IProviderIpManager.cs index 6c46a160c..52bdc34e5 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/IProviderIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/IProviderIpManager.cs @@ -1,6 +1,5 @@ using System.Net; using System.Threading; -using Eryph.Core.Network; using Eryph.StateDb.Model; using LanguageExt; using LanguageExt.Common; @@ -10,6 +9,7 @@ namespace Eryph.Modules.Controller.Networks; public interface IProviderIpManager { public EitherAsync ConfigureFloatingPortIps( - NetworkProvider provider, FloatingNetworkPort port, CancellationToken cancellationToken); - -} \ No newline at end of file + string providerName, + FloatingNetworkPort port, + CancellationToken cancellationToken); +} diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs index af19a4c3d..226497f36 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs @@ -3,6 +3,8 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using Eryph.ConfigModel; +using Eryph.Core; using Eryph.Core.Network; using Eryph.StateDb; using Eryph.StateDb.Model; @@ -10,97 +12,66 @@ using LanguageExt; using LanguageExt.Common; +using static LanguageExt.Prelude; + namespace Eryph.Modules.Controller.Networks; -internal class ProviderIpManager : BaseIpManager, IProviderIpManager +internal class ProviderIpManager( + IStateStore stateStore, + IIpPoolManager poolManager) + : BaseIpManager(stateStore, poolManager), IProviderIpManager { - - public ProviderIpManager(IStateStore stateStore, IIpPoolManager poolManager) : base(stateStore, poolManager) - { - } - - public EitherAsync ConfigureFloatingPortIps(NetworkProvider provider, FloatingNetworkPort port, - CancellationToken cancellationToken) - { - - var portProvider = new [] - { - new PortProvider(AddressFamily.InterNetwork, provider.Name, Option.None, Option.None) - }; - - - var getPortAssignments = - Prelude.TryAsync(_stateStore.For().ListAsync(new IPAssignmentSpecs.GetByPort(port.Id), - cancellationToken)) - .ToEither(f => Error.New(f)); - - return from portAssignments in getPortAssignments - from validAssignments in portAssignments.Map( - assignment => CheckAssignmentConfigured(assignment, port).ToAsync()) - .TraverseSerial(l => l.AsEnumerable()) - .Map(e => e.Flatten()) - - from validAndNewAssignments in portProvider.Map(portNetwork => - { - var providerNameString = portNetwork.ProviderName.IfNone("default"); - var subnetNameString = portNetwork.SubnetName.IfNone("default"); - var poolNameString = portNetwork.PoolName.IfNone("default"); - - return - - from subnet in _stateStore.Read().IO - .GetBySpecAsync(new SubnetSpecs.GetByProviderName(providerNameString, subnetNameString), cancellationToken) - .Bind(r => r.ToEitherAsync( - Error.New($"Subnet {subnetNameString} not found for provider {providerNameString}."))) - - let existingAssignment = CheckAssignmentExist(validAssignments, subnet, poolNameString) - - from assignment in existingAssignment.IsSome ? - existingAssignment.ToEitherAsync(Errors.None) - : from newAssignment in _poolManager.AcquireIp(subnet.Id, poolNameString, cancellationToken) - .Map(a => (IpAssignment)a) - let _ = UpdatePortAssignment(port, newAssignment) - select newAssignment - select assignment; - - }).SequenceSerial() - - select validAndNewAssignments - .Select(x => IPAddress.Parse((string)x.IpAddress)).ToArray(); - - } - - - private async Task>> CheckAssignmentConfigured(IpAssignment assignment, FloatingNetworkPort port) - { - var subnetName = ""; - var poolName = ""; - - await _stateStore.LoadPropertyAsync(assignment, x => x.Subnet); - if (assignment.Subnet is ProviderSubnet providerSubnet) - { - subnetName = providerSubnet.Name; - - } - - if (assignment is IpPoolAssignment poolAssignment) - { - await _stateStore.LoadPropertyAsync(poolAssignment, x => x.Pool); - poolName = poolAssignment.Pool.Name; - } - - if (port.SubnetName == subnetName && (string.IsNullOrWhiteSpace(poolName) && port.PoolName == poolName)) - return Prelude.Right>(assignment); - - // remove invalid - await _stateStore.For().DeleteAsync(assignment); - return Prelude.Right>(Option.None); - - } - - private record PortProvider( - AddressFamily AddressFamily, - Option ProviderName, - Option SubnetName, - Option PoolName); -} \ No newline at end of file + public EitherAsync ConfigureFloatingPortIps( + string providerName, + FloatingNetworkPort port, + CancellationToken cancellationToken) => + from ipAssignments in _stateStore.For().IO.ListAsync( + new IPAssignmentSpecs.GetByPort(port.Id), cancellationToken) + let validDirectAssignments = ipAssignments + .Filter(a => a is not IpPoolAssignment && IsValidAssignment(a, providerName, port.SubnetName)) + let validPoolAssignments = ipAssignments + .OfType().ToSeq() + .Filter(a => IsValidPoolAssignment(a, providerName, port.SubnetName, port.PoolName)) + let invalidAssignments = ipAssignments.Except(validDirectAssignments).Except(validPoolAssignments) + from _ in invalidAssignments + .Map(a => _stateStore.For().IO.DeleteAsync(a)) + .SequenceSerial() + from newAssignment in validPoolAssignments.IsEmpty + ? from assignment in CreateAssignment(port, providerName, port.SubnetName, port.PoolName, cancellationToken) + select Some(assignment) + : RightAsync>(None) + select validPoolAssignments.Append(newAssignment).Append(validDirectAssignments) + .Map(a => IPAddress.Parse(a.IpAddress!)) + .ToArray(); + + private EitherAsync CreateAssignment( + FloatingNetworkPort port, + string providerName, + string subnetName, + string ipPoolName, + CancellationToken cancellationToken) => + from subnet in _stateStore.Read().IO.GetBySpecAsync( + new SubnetSpecs.GetByProviderName(providerName, subnetName), + cancellationToken) + from validSubnet in subnet.ToEitherAsync( + Error.New($"Subnet {subnetName} not found for provider {providerName}.")) + from assignment in _poolManager.AcquireIp(validSubnet.Id, ipPoolName, cancellationToken) + let _ = UpdatePortAssignment(port, assignment) + select assignment; + + private static bool IsValidAssignment( + IpAssignment assignment, + string configuredProvider, + string configuredSubnet) => + assignment.Subnet is ProviderSubnet subnet + && subnet.ProviderName == configuredProvider + && subnet.Name == configuredSubnet; + + private static bool IsValidPoolAssignment( + IpPoolAssignment assignment, + string configuredProvider, + string configuredSubnet, + string configuredPool) => + IsValidAssignment(assignment, configuredProvider, configuredSubnet) + && assignment.Pool.Name == configuredPool; +} diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs index 2f0058995..e9d6c311f 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs @@ -84,8 +84,8 @@ from network in _stateStore.Read().IO.GetBySpecAsync( from validNetwork in network.IsNone && environmentName != EnvironmentName.New(EryphConstants.DefaultEnvironmentName) ? _stateStore.Read().IO.GetBySpecAsync( new VirtualNetworkSpecs.GetByName(command.ProjectId, networkName.Value, EryphConstants.DefaultEnvironmentName)) - .Bind(fr => fr.ToEitherAsync(Error.New($"Network {networkName} not found in environment {environmentName} and default environment."))) - : network.ToEitherAsync(Error.New($"Environment {environmentName}: Network {networkName} not found.")) + .Bind(fr => fr.ToEitherAsync(Error.New($"Network '{networkName}' not found in environment '{environmentName}' and default environment."))) + : network.ToEitherAsync(Error.New($"Network '{networkName}' not found in environment '{environmentName}'.")) from networkProviders in _providerManager.GetCurrentConfiguration() from networkProvider in networkProviders.NetworkProviders.Find(x => x.Name == validNetwork.NetworkProvider) @@ -94,35 +94,33 @@ from networkProvider in networkProviders.NetworkProviders.Find(x => x.Name == va let isFlatNetwork = networkProvider.Type == NetworkProviderType.Flat + let fixedMacAddress = command.Config.NetworkAdapters + .ToSeq() + .Find(x => x.Name == networkConfig.AdapterName) + .Bind(x => Optional(x.MacAddress)) let c1 = new CancellationTokenSource(500000) // TODO update assignment of port to network when the network changed in the config // TODO delete ports which are no longer configured - from networkPort in GetOrAddAdapterPort( + from networkPort in AddOrUpdateAdapterPort( validNetwork, command.CatletId, catletMetadataId, networkConfig.AdapterName, - command.Config.Hostname ?? command.Config.Name, c1.Token) + command.Config.Hostname ?? command.Config.Name, fixedMacAddress, c1.Token) let c2 = new CancellationTokenSource() from floatingPort in isFlatNetwork - ? Prelude.RightAsync>(Option.None) - : GetOrAddFloatingPort( + ? RightAsync>(Option.None) + : UpdateFloatingPort( networkPort, Option.None, "default", "default", "default", c2.Token) .ToAsync() .Map(Option.Some) - let fixedMacAddress = - command.Config.NetworkAdapters.Find(x => x.Name == networkConfig.AdapterName) - .Bind(x => Prelude.Optional(x.MacAddress)) - .IfNone("") - let _ = UpdatePort(networkPort, command.CatletId, networkConfig.AdapterName, fixedMacAddress) - let c3 = new CancellationTokenSource() from ips in isFlatNetwork - ? Prelude.RightAsync([]) + ? RightAsync([]) : _ipManager.ConfigurePortIps( command.ProjectId, command.Config.Environment ?? "default", networkPort, @@ -130,10 +128,10 @@ from ips in isFlatNetwork c3.Token) from floatingIps in isFlatNetwork - ? Prelude.RightAsync([]) + ? RightAsync([]) : floatingPort.ToEither(Error.New("floating port is missing")) .ToAsync() - .Bind(p => _providerIpManager.ConfigureFloatingPortIps(networkProvider, p, c3.Token)) + .Bind(p => _providerIpManager.ConfigureFloatingPortIps(networkProvider.Name, p, c3.Token)) select new MachineNetworkSettings { @@ -150,15 +148,19 @@ from floatingIps in isFlatNetwork ?.ToString(), }; - private EitherAsync GetOrAddAdapterPort( + private EitherAsync AddOrUpdateAdapterPort( VirtualNetwork network, Guid catletId, Guid catletMetadataId, string adapterName, string addressName, + Option fixedMacAddress, CancellationToken cancellationToken) => from _ in RightAsync(unit) let portName = GetPortName(catletId, adapterName) + let macAddress = fixedMacAddress + .Filter(notEmpty) + .IfNone(() => MacAddresses.GenerateMacAddress(portName)) from existingPort in _stateStore.For().IO.GetBySpecAsync( new NetworkPortSpecs.GetByNetworkAndNameForCatlet(network.Id, portName), cancellationToken) from updatedPort in existingPort.Match( @@ -166,10 +168,7 @@ from updatedPort in existingPort.Match( let __ = fun(() => { p.AddressName = addressName; - // TODO use proper fixed MAC address - p.MacAddress = string.IsNullOrEmpty(null) - ? MacAddresses.FormatMacAddress(MacAddresses.GenerateMacAddress(portName)) - : MacAddresses.FormatMacAddress("a"); + p.MacAddress = MacAddresses.FormatMacAddress(macAddress); }) select p, None: () => from _ in RightAsync(unit) @@ -178,13 +177,10 @@ from updatedPort in existingPort.Match( Id = Guid.NewGuid(), CatletMetadataId = catletMetadataId, Name = portName, - // TODO use proper fixed MAC address - MacAddress = string.IsNullOrEmpty(null) - ? MacAddresses.FormatMacAddress(MacAddresses.GenerateMacAddress(portName)) - : MacAddresses.FormatMacAddress("a"), + MacAddress = MacAddresses.FormatMacAddress(macAddress), NetworkId = network.Id, AddressName = addressName, - IpAssignments = new List() + IpAssignments = [], } from addedPort in _stateStore.For().IO.AddAsync(newPort, cancellationToken) select addedPort) @@ -192,10 +188,13 @@ from addedPort in _stateStore.For().IO.AddAsync(newPort, canc - private async Task> GetOrAddFloatingPort(CatletNetworkPort adapterPort, - Option portName, string providerName, string providerSubnetName, string providerPoolName, + private async Task> UpdateFloatingPort( + CatletNetworkPort adapterPort, + Option portName, + string providerName, + string providerSubnetName, + string providerPoolName, CancellationToken cancellationToken) - { await _stateStore.LoadPropertyAsync(adapterPort, x => x.FloatingPort, cancellationToken); @@ -229,28 +228,8 @@ private async Task> GetOrAddFloatingPort(Catl adapterPort.FloatingPort = port; return await _stateStore.For().AddAsync(port, cancellationToken); - - } - - - private static Unit UpdatePort( - CatletNetworkPort networkPort, - Guid catletId, - string adapterName, - string fixedMacAddress) - { - if (!string.IsNullOrEmpty(fixedMacAddress)) - networkPort.MacAddress = MacAddresses.FormatMacAddress(fixedMacAddress); - else - { - networkPort.MacAddress ??= MacAddresses.FormatMacAddress( - MacAddresses.GenerateMacAddress(GetPortName(catletId, adapterName))); - } - - return Unit.Default; } - private static string GetPortName(Guid catletId, string adapterName) => $"{catletId}_{adapterName}"; -} \ No newline at end of file +} diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs index 7ddf63faa..e19077f1c 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs @@ -1,382 +1,458 @@ using Eryph.ConfigModel.Catlets; using Eryph.Core; using Eryph.Modules.Controller.Networks; -using Eryph.Resources; using Eryph.StateDb; using Eryph.StateDb.Model; -using Eryph.StateDb.Sqlite; using Eryph.StateDb.TestBase; -using FluentAssertions; -using FluentAssertions.LanguageExt; -using LanguageExt; -using LanguageExt.Common; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Moq; using SimpleInjector; using SimpleInjector.Integration.ServiceCollection; -using Xunit; -namespace Eryph.Modules.Controller.Tests.Networks +namespace Eryph.Modules.Controller.Tests.Networks; + +public sealed class CatletIpManagerTests : InMemoryStateDbTestBase { - public sealed class CatletIpManagerTests : InMemoryStateDbTestBase + private const string DefaultProjectId = "4B4A3FCF-B5ED-4A9A-AB6E-03852752095E"; + private const string SecondProjectId = "75c27daf-77c8-4b98-a072-a4706dceb422"; + + private const string DefaultNetworkId = "cb58fe00-3f64-4b66-b58e-23fb15df3cac"; + private const string DefaultSubnetId = "ed6697cd-836f-4da7-914b-b09ed1567934"; + private const string SecondSubnetId = "4f976208-613a-40d4-a284-d32cbd4a1b8e"; + + private const string SecondNetworkId = "e480a020-57d0-4443-a973-57aa0c95872e"; + private const string SecondNetworkSubnetId = "27ec11a4-5d6a-47da-9f9f-eb7486db38ea"; + + private const string SecondEnvironmentNetworkId = "81a139e5-ab61-4fe3-b81f-59c11a665d22"; + private const string SecondEnvironmentSubnetId = "dc807357-50e7-4263-8298-0c97ff69f4cf"; + + private const string SecondProjectNetworkId = "c0043e88-8268-4ac0-b027-2fa37ad3168f"; + private const string SecondProjectSubnetId = "0c721846-5e2e-40a9-83d2-f1b75206ef84"; + + private const string CatletMetadataId = "15e2b061-c625-4469-9fe7-7c455058fcc0"; + private static readonly Guid CatletPortId = Guid.NewGuid(); + + [Theory] + [InlineData(DefaultProjectId, "default", DefaultNetworkId, null, null, null, "10.0.0.12")] + [InlineData(DefaultProjectId, "default", DefaultNetworkId, "default", "default", "default", "10.0.0.12")] + [InlineData(DefaultProjectId, "default", DefaultNetworkId, "default", "default", "second-pool", "10.0.1.12")] + [InlineData(DefaultProjectId, "default", DefaultNetworkId, "default", "second-subnet", "default", "10.1.0.12")] + [InlineData(DefaultProjectId, "default", SecondNetworkId, "second-network", "default", "default", "10.5.0.12")] + [InlineData(DefaultProjectId, "second-environment", DefaultNetworkId, "default", "default", "default", "10.10.0.12")] + // When the environment does not have a dedicated network, we should fall back to the default network + [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, null, null, null, "10.0.0.12")] + [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, "default", "default", "default", "10.0.0.12")] + [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, "default", "default", "second-pool", "10.0.1.12")] + [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, "default", "second-subnet", "default", "10.1.0.12")] + [InlineData(DefaultProjectId, "environment-without-network", SecondNetworkId, "second-network", "default", "default", "10.5.0.12")] + [InlineData(SecondProjectId, "default", SecondProjectNetworkId, null, null, null, "10.100.0.12")] + [InlineData(SecondProjectId, "default", SecondProjectNetworkId, "default", "default", "default", "10.100.0.12")] + public async Task ConfigurePortIps_NewPortIsAdded_AssignmentIsCreated( + string projectId, + string environment, + string networkId, + string? networkName, + string? subnetName, + string? poolName, + string expectedIpAddress) { - // ReSharper disable InconsistentNaming - private const string ProjectAId = "96bbd6d7-01f9-4001-8c86-3fba75baa1b5"; - private const string ProjectA_NetworkId = "cb58fe00-3f64-4b66-b58e-23fb15df3cac"; - private const string NetworkA_Default_Subnet = "ed6697cd-836f-4da7-914b-b09ed1567934"; - private const string NetworkA_Other_Subnet = "29fb8b37-4779-427a-bc5c-9a5eccffd5e2"; - - private const string ProjectBId = "75c27daf-77c8-4b98-a072-a4706dceb422"; - private const string NetworkB_Default = "a0ce4b1a-03e6-413b-a048-567079b49b28"; - private const string NetworkB_Env2_Default = "29408ed0-f876-4879-9faa-deb519d1df0a"; - private const string NetworkB_Default_Subnet = "ac451fa5-3364-4593-aa4d-14f95529fd54"; - private const string NetworkB_Env2_Subnet = "91a25d95-f417-482d-9264-e4179f61e379"; - - private const string CatletMetadataId = "15e2b061-c625-4469-9fe7-7c455058fcc0"; - // ReSharper restore InconsistentNaming - - [Theory] - [InlineData(ProjectAId, ProjectA_NetworkId, "default", null, null, null, "192.0.2.1")] - [InlineData(ProjectAId, ProjectA_NetworkId, "default", "default", "other", null, "192.0.2.17")] - [InlineData(ProjectAId, ProjectA_NetworkId, "env2", null, null, null, "192.0.2.1")] - [InlineData(ProjectBId, NetworkB_Default, "default", null, null, null, "192.0.2.33")] - [InlineData(ProjectBId, NetworkB_Default, "default", "default", "default", "other", "192.0.2.49")] - [InlineData(ProjectBId, NetworkB_Env2_Default, "env2", null, null, null, "192.0.2.65")] - public async Task Adds_catlet_network_port_to_expected_pool( - string projectId, - string networkId, - string environment, - string? network, - string? subnet, - string? pool, - string expectedIpAddress) + var networkConfig = new CatletNetworkConfig() { - var networkConfig = new CatletNetworkConfig() - { - Name = network, - SubnetV4 = subnet != null || pool != null - ? new CatletSubnetConfig - { - Name = subnet ?? "default", - IpPool = pool - } - : null, - }; - - await WithScope(async (ipManager, _, stateStore) => - { - var catletPort = new CatletNetworkPort + Name = networkName, + SubnetV4 = subnetName != null || poolName != null + ? new CatletSubnetConfig { - Id = Guid.NewGuid(), - Name = "test-catlet-port", - MacAddress = "00:00:00:00:00:01", - NetworkId = Guid.Parse(networkId), - CatletMetadataId = Guid.Parse(CatletMetadataId), - }; - await stateStore.For().AddAsync(catletPort); - - var result = await ipManager.ConfigurePortIps( - Guid.Parse(projectId), - environment, catletPort, networkConfig, - CancellationToken.None); - - result.Should().BeRight().Which.Should().SatisfyRespectively( - ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); - }); - } + Name = subnetName, + IpPool = poolName + } + : null, + }; - // TODO Parameterize for different subnets - [Fact] - public async Task Deletes_Invalid_Port() + await WithScope(async (ipManager, _, stateStore) => { - var projectId = Guid.Parse(ProjectAId); - var networkConfig = new CatletNetworkConfig() + var catletPort = new CatletNetworkPort { - Name = "default" + Id = CatletPortId, + Name = "test-catlet-port", + MacAddress = "00:00:00:00:00:01", + NetworkId = Guid.Parse(networkId), + CatletMetadataId = Guid.Parse(CatletMetadataId), }; + await stateStore.For().AddAsync(catletPort); - var catletPortId = Guid.NewGuid(); - var ipAssignmentId = Guid.Empty; - await WithScope(async (_, ipPoolManager, stateStore) => - { - var ipAssignmentResult = ipPoolManager.AcquireIp( - Guid.Parse(NetworkA_Other_Subnet), - EryphConstants.DefaultIpPoolName); - var ipAssignment = ipAssignmentResult.Should().BeRight().Subject; - ipAssignmentId = ipAssignment.Id; + var result = await ipManager.ConfigurePortIps( + Guid.Parse(projectId), + environment, catletPort, networkConfig, + CancellationToken.None); - var catletPort = new CatletNetworkPort + result.Should().BeRight().Which.Should().SatisfyRespectively( + ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); + + await stateStore.SaveChangesAsync(); + }); + + await WithScope(async (_, _, stateStore) => + { + var ipAssignments = await stateStore.For().ListAsync(); + ipAssignments.Should().SatisfyRespectively( + ipAssignment => { - Id = catletPortId, - Name = "test-catlet-port", - MacAddress = "00:00:00:00:00:01", - NetworkId = Guid.Parse(ProjectA_NetworkId), - CatletMetadataId = Guid.Parse(CatletMetadataId), - IpAssignments = [ipAssignment], - }; - - await stateStore.For().AddAsync(catletPort); - await stateStore.SaveChangesAsync(); - }); + ipAssignment.NetworkPortId.Should().Be(CatletPortId); + ipAssignment.IpAddress.Should().Be(expectedIpAddress); + }); + }); + } + + [Theory] + [InlineData(DefaultProjectId, "default", DefaultNetworkId, DefaultSubnetId, null, null, null, "10.0.0.12")] + [InlineData(DefaultProjectId, "default", DefaultNetworkId, DefaultSubnetId, "default", "default", "default", "10.0.0.12")] + [InlineData(DefaultProjectId, "default", DefaultNetworkId, DefaultSubnetId, "default", "default", "second-pool", "10.0.1.12")] + [InlineData(DefaultProjectId, "default", DefaultNetworkId, SecondSubnetId, "default", "second-subnet", "default", "10.1.0.12")] + [InlineData(DefaultProjectId, "default", SecondNetworkId, SecondNetworkSubnetId, "second-network", "default", "default", "10.5.0.12")] + [InlineData(DefaultProjectId, "second-environment", SecondEnvironmentNetworkId, SecondEnvironmentSubnetId, "default", "default", "default", "10.10.0.12")] + // When the environment does not have a dedicated network, we should fall back to the default network + [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, DefaultSubnetId, null, null, null, "10.0.0.12")] + [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, DefaultSubnetId, "default", "default", "default", "10.0.0.12")] + [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, DefaultSubnetId, "default", "default", "second-pool", "10.0.1.12")] + [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, SecondSubnetId, "default", "second-subnet", "default", "10.1.0.12")] + [InlineData(DefaultProjectId, "environment-without-network", SecondNetworkId, SecondNetworkSubnetId, "second-network", "default", "default", "10.5.0.12")] + [InlineData(SecondProjectId, "default", SecondProjectNetworkId, SecondProjectSubnetId, null, null, null, "10.100.0.12")] + [InlineData(SecondProjectId, "default", SecondProjectNetworkId, SecondProjectSubnetId, "default", "default", "default", "10.100.0.12")] + public async Task ConfigureFloatingPortIps_AssignmentIsValid_AssignmentIsNotChanged( + string projectId, + string environment, + string networkId, + string subnetId, + string? networkName, + string? subnetName, + string? poolName, + string expectedIpAddress) + { + var ipAssignmentId = Guid.Empty; + await WithScope(async (_, ipPoolManager, stateStore) => + { + var ipAssignmentResult = ipPoolManager.AcquireIp( + Guid.Parse(subnetId), poolName ?? EryphConstants.DefaultIpPoolName); + var ipAssignment = ipAssignmentResult.Should().BeRight().Subject; + ipAssignmentId = ipAssignment.Id; - await WithScope(async (catletIpManager, _, stateStore) => + var catletPort = new CatletNetworkPort { - var catletPort = await stateStore.Read() - .GetByIdAsync(catletPortId); - catletPort.Should().NotBeNull(); - await stateStore.LoadCollectionAsync(catletPort!, p => p.IpAssignments); - catletPort!.IpAssignments.Should().SatisfyRespectively( - ipAssignment => ipAssignment.Id.Should().Be(ipAssignmentId)); + Id = CatletPortId, + Name = "test-catlet-port", + MacAddress = "00:00:00:00:00:01", + NetworkId = Guid.Parse(networkId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + IpAssignments = [ipAssignment], + }; - var result = await catletIpManager.ConfigurePortIps(projectId, - "default", catletPort, networkConfig, - CancellationToken.None); + await stateStore.For().AddAsync(catletPort); + await stateStore.SaveChangesAsync(); + }); - result.Should().BeRight().Which.Should().SatisfyRespectively( - ipAddress => ipAddress.ToString().Should().Be("192.0.2.1")); + var networkConfig = new CatletNetworkConfig() + { + Name = networkName, + SubnetV4 = subnetName != null || poolName != null + ? new CatletSubnetConfig + { + Name = subnetName, + IpPool = poolName + } + : null, + }; - await stateStore.SaveChangesAsync(); - }); + await WithScope(async (catletIpManager, _, stateStore) => + { + var catletPort = await stateStore.Read() + .GetByIdAsync(CatletPortId); + catletPort.Should().NotBeNull(); - await WithScope(async (_, _, stateStore) => - { - var catletPort = await stateStore.Read() - .GetByIdAsync(catletPortId); - catletPort.Should().NotBeNull(); - await stateStore.LoadCollectionAsync(catletPort!, p => p.IpAssignments); + var result = await catletIpManager.ConfigurePortIps( + Guid.Parse(projectId), environment, catletPort!, networkConfig, + CancellationToken.None); - catletPort!.IpAssignments.Should().SatisfyRespectively( - ipAssignment => ipAssignment.IpAddress.Should().Be("192.0.2.1")); - }); - } + result.Should().BeRight().Which.Should().SatisfyRespectively( + ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); - [Fact] - public async Task ExistingAssignmentInDifferentEnvironment_RemovesOldAssignment() + await stateStore.SaveChangesAsync(); + }); + + await WithScope(async (_, _, stateStore) => { - - } + var ipAssignments = await stateStore.For().ListAsync(); + ipAssignments.Should().SatisfyRespectively( + ipAssignment => + { + ipAssignment.NetworkPortId.Should().Be(CatletPortId); + ipAssignment.Id.Should().Be(ipAssignmentId); + ipAssignment.IpAddress.Should().Be(expectedIpAddress); + }); + }); + } - [Fact] - public async Task Keeps_Valid_Port() + [Theory] + [InlineData(DefaultProjectId, "default", "default", "default", "second-pool", "10.0.1.12")] + [InlineData(DefaultProjectId, "default", "default", "second-subnet", "default", "10.1.0.12")] + [InlineData(DefaultProjectId, "default", "second-network", "default", "default", "10.5.0.12")] + [InlineData(DefaultProjectId, "second-environment", "default", "default", "default", "10.10.0.12")] + // When the environment does not have a dedicated network, we should fall back to the default network + [InlineData(DefaultProjectId, "environment-without-network", "default", "default", "second-pool", "10.0.1.12")] + [InlineData(DefaultProjectId, "environment-without-network", "default", "second-subnet", "default", "10.1.0.12")] + [InlineData(DefaultProjectId, "environment-without-network", "second-network", "default", "default", "10.5.0.12")] + [InlineData(SecondProjectId, "default", null, null, null, "10.100.0.12")] + [InlineData(SecondProjectId, "default", "default", "default", "default", "10.100.0.12")] + public async Task ConfigurePortIps_AssignmentIsInvalid_AssignmentIsChanged( + string projectId, + string environment, + string? networkName, + string? subnetName, + string? poolName, + string expectedIpAddress) + { + var ipAssignmentId = Guid.Empty; + await WithScope(async (_, ipPoolManager, stateStore) => { - var projectId = Guid.Parse(ProjectAId); - var networkConfig = new CatletNetworkConfig() + var ipAssignmentResult = ipPoolManager.AcquireIp( + Guid.Parse(DefaultSubnetId), + EryphConstants.DefaultIpPoolName); + var ipAssignment = ipAssignmentResult.Should().BeRight().Subject; + ipAssignmentId = ipAssignment.Id; + + var catletPort = new CatletNetworkPort { - Name = "default" + Id = CatletPortId, + Name = "test-catlet-port", + MacAddress = "00:00:00:00:00:01", + NetworkId = Guid.Parse(DefaultNetworkId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + IpAssignments = [ipAssignment], }; - var catletPortId = Guid.NewGuid(); - var ipAssignmentId = Guid.Empty; - await WithScope(async (_, ipPoolManager, stateStore) => - { - var ipAssignmentResult = ipPoolManager.AcquireIp( - Guid.Parse(NetworkA_Default_Subnet), - EryphConstants.DefaultIpPoolName); - var ipAssignment = ipAssignmentResult.Should().BeRight().Subject; - ipAssignmentId = ipAssignment.Id; + await stateStore.For().AddAsync(catletPort); + await stateStore.SaveChangesAsync(); + }); - var catletPort = new CatletNetworkPort + var networkConfig = new CatletNetworkConfig() + { + Name = networkName, + SubnetV4 = subnetName != null || poolName != null + ? new CatletSubnetConfig { - Id = catletPortId, - Name = "test-catlet-port", - MacAddress = "00:00:00:00:00:01", - NetworkId = Guid.Parse(ProjectA_NetworkId), - CatletMetadataId = Guid.Parse(CatletMetadataId), - IpAssignments = [ipAssignment] - }; - - await stateStore.For().AddAsync(catletPort); - await stateStore.SaveChangesAsync(); - }); + Name = subnetName, + IpPool = poolName + } + : null, + }; - await WithScope(async (catletIpManager, _, stateStore) => - { - var catletPort = await stateStore.For() - .GetByIdAsync(catletPortId); - await stateStore.LoadCollectionAsync(catletPort!, p => p.IpAssignments); - catletPort!.IpAssignments.Should().SatisfyRespectively( - ipAssignment => ipAssignment.Id.Should().Be(ipAssignmentId)); - - var result = await catletIpManager.ConfigurePortIps(projectId, - "default", catletPort, networkConfig, - CancellationToken.None); - - result.Should().BeRight().Which.Should().SatisfyRespectively( - ipAddress => ipAddress.ToString().Should().Be("192.0.2.1")); - - await stateStore.SaveChangesAsync(); - }); + await WithScope(async (catletIpManager, _, stateStore) => + { + var catletPort = await stateStore.Read() + .GetByIdAsync(CatletPortId); + catletPort.Should().NotBeNull(); - await WithScope(async (_, _, stateStore) => - { - var catletPort = await stateStore.Read() - .GetByIdAsync(catletPortId); - catletPort.Should().NotBeNull(); - await stateStore.LoadCollectionAsync(catletPort!, p => p.IpAssignments); + var result = await catletIpManager.ConfigurePortIps( + Guid.Parse(projectId), environment, catletPort!, networkConfig, + CancellationToken.None); - catletPort!.IpAssignments.Should().SatisfyRespectively( - ipAssignment => ipAssignment.Id.Should().Be(ipAssignmentId)); - }); - } + result.Should().BeRight().Which.Should().SatisfyRespectively( + ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); - private async Task WithScope(Func func) - { - await using var scope = CreateScope(); - var catletIpManager = scope.GetInstance(); - var ipPoolManager = scope.GetInstance(); - var stateStore = scope.GetInstance(); - await func(catletIpManager, ipPoolManager, stateStore); - } - - protected override void AddSimpleInjector(SimpleInjectorAddOptions options) - { - // Use the proper IpPoolManager instead of a mock as the code quite - // interdependent as it modifies the same EF Core entities. - options.Container.Register(Lifestyle.Scoped); - options.Container.Register(Lifestyle.Scoped); - } + await stateStore.SaveChangesAsync(); + }); - protected override async Task SeedAsync(IStateStore stateStore) + await WithScope(async (_, _, stateStore) => { - await SeedDefaultTenantAndProject(); + var ipAssignments = await stateStore.For().ListAsync(); + ipAssignments.Should().SatisfyRespectively( + ipAssignment => + { + ipAssignment.NetworkPortId.Should().Be(CatletPortId); + ipAssignment.Id.Should().NotBe(ipAssignmentId); + ipAssignment.IpAddress.Should().Be(expectedIpAddress); + }); + }); + } + + // TODO Add negative test - await stateStore.For().AddAsync(new CatletMetadata - { - Id = Guid.Parse(CatletMetadataId), - }); + private async Task WithScope(Func func) + { + await using var scope = CreateScope(); + var catletIpManager = scope.GetInstance(); + var ipPoolManager = scope.GetInstance(); + var stateStore = scope.GetInstance(); + await func(catletIpManager, ipPoolManager, stateStore); + } - var projectA = new Project() - { - Id = Guid.Parse(ProjectAId), - Name = "project-a", - TenantId = EryphConstants.DefaultTenantId, - }; - await stateStore.For().AddAsync(projectA); + protected override void AddSimpleInjector(SimpleInjectorAddOptions options) + { + // Use the proper IpPoolManager instead of a mock as the code quite + // interdependent as it modifies the same EF Core entities. + options.Container.Register(Lifestyle.Scoped); + options.Container.Register(Lifestyle.Scoped); + } - var projectB = new Project() - { - Id = Guid.Parse(ProjectBId), - Name = "project-b", - TenantId = EryphConstants.DefaultTenantId, - }; - await stateStore.For().AddAsync(projectB); + protected override async Task SeedAsync(IStateStore stateStore) + { + await SeedDefaultTenantAndProject(); - var projectADefaultEnvNetwork = new VirtualNetwork + await stateStore.For().AddAsync(new CatletMetadata + { + Id = Guid.Parse(CatletMetadataId), + }); + + var projectB = new Project() + { + Id = Guid.Parse(SecondProjectId), + Name = "second-project", + TenantId = EryphConstants.DefaultTenantId, + }; + await stateStore.For().AddAsync(projectB); + + await stateStore.For().AddAsync( + new VirtualNetwork { - Id = Guid.Parse(ProjectA_NetworkId), - Project = projectA, + Id = Guid.Parse(DefaultNetworkId), + ProjectId = EryphConstants.DefaultProjectId, Name = EryphConstants.DefaultNetworkName, Environment = EryphConstants.DefaultEnvironmentName, - ResourceType = ResourceType.VirtualNetwork, Subnets = [ new VirtualNetworkSubnet { - Id = Guid.Parse(NetworkA_Default_Subnet), + Id = Guid.Parse(DefaultSubnetId), Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.0.0.0/16", IpPools = [ new IpPool() { Id = Guid.NewGuid(), Name = EryphConstants.DefaultIpPoolName, - IpNetwork = "192.0.2.0/28", - FirstIp = "192.0.2.1", - NextIp = "192.0.2.1", - LastIp = "192.0.2.11", + IpNetwork = "10.0.0.0/16", + FirstIp = "10.0.0.10", + NextIp = "10.0.0.12", + LastIp = "10.0.0.19", + }, + new IpPool() + { + Id = Guid.NewGuid(), + Name = "second-pool", + IpNetwork = "10.0.0.0/16", + FirstIp = "10.0.1.10", + NextIp = "10.0.1.12", + LastIp = "10.0.1.19", } ], }, new VirtualNetworkSubnet { - Id = Guid.Parse(NetworkA_Other_Subnet), - Name = "other", + Id = Guid.Parse(SecondSubnetId), + Name = "second-subnet", + IpNetwork = "10.1.0.0/16", IpPools = [ new IpPool() { Id = Guid.NewGuid(), Name = EryphConstants.DefaultIpPoolName, - IpNetwork = "192.0.2.16/28", - FirstIp = "192.0.2.17", - NextIp = "192.0.2.17", - LastIp = "192.0.2.27", + IpNetwork = "10.1.0.0/16", + FirstIp = "10.1.0.10", + NextIp = "10.1.0.12", + LastIp = "10.1.0.19", } ], }, ], - }; - await stateStore.For().AddAsync(projectADefaultEnvNetwork); + }); - var projectBDefaultEnvNetwork = new VirtualNetwork + await stateStore.For().AddAsync( + new VirtualNetwork { - Id = Guid.Parse(NetworkB_Default), - Project = projectB, - Name = "default", - Environment = "default", - ResourceType = ResourceType.VirtualNetwork, + Id = Guid.Parse(SecondNetworkId), + ProjectId = EryphConstants.DefaultProjectId, + Name = "second-network", + Environment = EryphConstants.DefaultEnvironmentName, Subnets = [ new VirtualNetworkSubnet { - Id = Guid.Parse(NetworkB_Default_Subnet), - Name = "default", + Id = Guid.Parse(SecondNetworkSubnetId), + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.5.0.0/16", IpPools = [ - new IpPool + new IpPool() { Id = Guid.NewGuid(), - Name = "default", - IpNetwork = "192.0.2.32/28", - FirstIp = "192.0.2.33", - NextIp = "192.0.2.33", - LastIp = "192.0.2.43", - }, - new IpPool + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.5.0.0/16", + FirstIp = "10.5.0.10", + NextIp = "10.5.0.12", + LastIp = "10.5.0.19", + } + ], + }, + ], + }); + + await stateStore.For().AddAsync( + new VirtualNetwork + { + Id = Guid.Parse(SecondEnvironmentNetworkId), + ProjectId = EryphConstants.DefaultProjectId, + Name = EryphConstants.DefaultNetworkName, + Environment = "second-environment", + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.Parse(SecondEnvironmentSubnetId), + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.10.0.0/16", + IpPools = + [ + new IpPool() { Id = Guid.NewGuid(), - Name = "other", - IpNetwork = "192.0.2.48/28", - FirstIp = "192.0.2.49", - NextIp = "192.0.2.49", - LastIp = "192.0.2.59", - }, + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.10.0.0/16", + FirstIp = "10.10.0.10", + NextIp = "10.10.0.12", + LastIp = "10.10.0.19", + } ], }, ], - }; - await stateStore.For().AddAsync(projectBDefaultEnvNetwork); - - var projectBOtherEnvNetwork = new VirtualNetwork + }); + + await stateStore.For().AddAsync( + new VirtualNetwork { - Id = Guid.Parse(NetworkB_Env2_Default), - Project = projectB, - Name = "default", - Environment = "env2", - ResourceType = ResourceType.VirtualNetwork, + Id = Guid.Parse(SecondProjectNetworkId), + ProjectId = Guid.Parse(SecondProjectId), + Name = EryphConstants.DefaultNetworkName, + Environment = EryphConstants.DefaultEnvironmentName, Subnets = [ new VirtualNetworkSubnet { - Id = Guid.Parse(NetworkB_Env2_Subnet), - Name = "default", + Id = Guid.Parse(SecondProjectSubnetId), + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.100.0.0/16", IpPools = [ new IpPool() { Id = Guid.NewGuid(), Name = EryphConstants.DefaultIpPoolName, - IpNetwork = "192.0.2.64/28", - FirstIp = "192.0.2.65", - NextIp = "192.0.2.65", - LastIp = "192.0.2.75", + IpNetwork = "10.100.0.0/16", + FirstIp = "10.100.0.10", + NextIp = "10.100.0.12", + LastIp = "10.100.0.19", } ], }, ], - }; - await stateStore.For().AddAsync(projectBOtherEnvNetwork); - } + }); } -} +} \ No newline at end of file diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs new file mode 100644 index 000000000..63ee6b084 --- /dev/null +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs @@ -0,0 +1,297 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Eryph.Core; +using Eryph.Modules.Controller.Networks; +using Eryph.StateDb; +using Eryph.StateDb.Model; +using Eryph.StateDb.TestBase; +using SimpleInjector.Integration.ServiceCollection; +using SimpleInjector; + +namespace Eryph.Modules.Controller.Tests.Networks; + +public class ProviderIpManagerTests : InMemoryStateDbTestBase +{ + private const string DefaultSubnetId = "00bbb738-9b76-4b52-8c9a-89fcb2516f66"; + private const string SecondSubnetId = "edd1c7e0-c8e5-4679-b98d-f7672914d5f7"; + private const string SecondProviderSubnetId = "f712ba4e-ace9-4830-a346-59d17c11b764"; + + private static readonly Guid FloatingPortId = Guid.NewGuid(); + + [Theory] + [InlineData("default", "default", "default", "10.0.0.12")] + [InlineData("default", "default", "second-pool", "10.0.1.12")] + [InlineData("default", "second-subnet", "default", "10.1.0.12")] + [InlineData("second-provider", "default", "default", "10.10.0.12")] + public async Task ConfigureFloatingPortIps_NewPortIsAdded_AssignmentIsCreated( + string providerName, + string subnetName, + string poolName, + string expectedIpAddress) + { + await WithScope(async (providerIpManager, _, stateStore) => + { + var floatingPort = new FloatingNetworkPort + { + Id = FloatingPortId, + Name = "test-floating-port", + MacAddress = "00:00:00:00:00:01", + ProviderName = providerName, + SubnetName = subnetName, + PoolName = poolName, + }; + await stateStore.For().AddAsync(floatingPort); + + var result = await providerIpManager.ConfigureFloatingPortIps( + providerName, + floatingPort, + default); + + result.Should().BeRight().Which.Should().SatisfyRespectively( + ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); + + await stateStore.SaveChangesAsync(); + }); + + await WithScope(async (_, _, stateStore) => + { + var ipAssignments = await stateStore.For().ListAsync(); + ipAssignments.Should().SatisfyRespectively( + ipAssignment => + { + ipAssignment.NetworkPortId.Should().Be(FloatingPortId); + ipAssignment.IpAddress.Should().Be(expectedIpAddress); + }); + }); + } + + // TODO add negative tests + + [Theory] + [InlineData("default", DefaultSubnetId, "default", "default", "10.0.0.12")] + [InlineData("default", DefaultSubnetId, "default", "second-pool", "10.0.1.12")] + [InlineData("default", SecondSubnetId, "second-subnet", "default", "10.1.0.12")] + [InlineData("second-provider", SecondProviderSubnetId, "default", "default", "10.10.0.12")] + public async Task ConfigureFloatingPortIps_AssignmentIsValid_AssignmentIsNotChanged( + string providerName, + string subnetId, + string subnetName, + string poolName, + string expectedIpAddress) + { + var ipAssignmentId = Guid.Empty; + await WithScope(async (_, ipPoolManager, stateStore) => + { + var ipAssignmentResult = ipPoolManager.AcquireIp( + Guid.Parse(subnetId), + poolName); + var ipAssignment = ipAssignmentResult.Should().BeRight().Subject; + ipAssignment.IpAddress.Should().Be(expectedIpAddress); + ipAssignmentId = ipAssignment.Id; + + var floatingPort = new FloatingNetworkPort + { + Id = FloatingPortId, + Name = "test-floating-port", + MacAddress = "00:00:00:00:00:01", + ProviderName = providerName, + SubnetName = subnetName, + PoolName = poolName, + IpAssignments = [ipAssignment] + }; + await stateStore.For().AddAsync(floatingPort); + await stateStore.SaveChangesAsync(); + }); + + await WithScope(async (providerIpManager, _, stateStore) => + { + var floatingPort = await stateStore.Read() + .GetByIdAsync(FloatingPortId); + floatingPort.Should().NotBeNull(); + + var result = await providerIpManager.ConfigureFloatingPortIps( + providerName, floatingPort!, default); + + result.Should().BeRight().Which.Should().SatisfyRespectively( + ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); + + await stateStore.SaveChangesAsync(); + }); + + await WithScope(async (_, _, stateStore) => + { + var ipAssignments = await stateStore.For().ListAsync(); + ipAssignments.Should().SatisfyRespectively( + ipAssignment => + { + ipAssignment.NetworkPortId.Should().Be(FloatingPortId); + ipAssignment.Id.Should().Be(ipAssignmentId); + ipAssignment.IpAddress.Should().Be(expectedIpAddress); + }); + }); + } + + [Theory] + [InlineData("default", "default", "second-pool", "10.0.1.12")] + [InlineData("default", "second-subnet", "default", "10.1.0.12")] + [InlineData("second-provider", "default", "default", "10.10.0.12")] + public async Task ConfigureFloatingPortIps_AssignmentIsInvalid_AssignmentIsChanged( + string providerName, + string subnetName, + string poolName, + string expectedIpAddress) + { + var ipAssignmentId = Guid.Empty; + await WithScope(async (_, ipPoolManager, stateStore) => + { + var ipAssignmentResult = ipPoolManager.AcquireIp( + Guid.Parse(DefaultSubnetId), + "default"); + var ipAssignment = ipAssignmentResult.Should().BeRight().Subject; + ipAssignment.IpAddress.Should().Be("10.0.0.12"); + ipAssignmentId = ipAssignment.Id; + + var floatingPort = new FloatingNetworkPort + { + Id = FloatingPortId, + Name = "test-floating-port", + MacAddress = "00:00:00:00:00:01", + ProviderName = "default", + SubnetName = "default", + PoolName = "default", + IpAssignments = [ipAssignment] + }; + await stateStore.For().AddAsync(floatingPort); + await stateStore.SaveChangesAsync(); + }); + + await WithScope(async (providerIpManager, _, stateStore) => + { + var floatingPort = await stateStore.Read() + .GetByIdAsync(FloatingPortId); + floatingPort.Should().NotBeNull(); + + floatingPort!.ProviderName = providerName; + floatingPort.SubnetName = subnetName; + floatingPort.PoolName = poolName; + + var result = await providerIpManager.ConfigureFloatingPortIps( + providerName, floatingPort, default); + + result.Should().BeRight().Which.Should().SatisfyRespectively( + ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); + + await stateStore.SaveChangesAsync(); + }); + + await WithScope(async (_, _, stateStore) => + { + var ipAssignments = await stateStore.For().ListAsync(); + ipAssignments.Should().SatisfyRespectively( + ipAssignment => + { + ipAssignment.NetworkPortId.Should().Be(FloatingPortId); + ipAssignment.Id.Should().NotBe(ipAssignmentId); + ipAssignment.IpAddress.Should().Be(expectedIpAddress); + + }); + }); + } + + private async Task WithScope(Func func) + { + await using var scope = CreateScope(); + var catletIpManager = scope.GetInstance(); + var ipPoolManager = scope.GetInstance(); + var stateStore = scope.GetInstance(); + await func(catletIpManager, ipPoolManager, stateStore); + } + + protected override void AddSimpleInjector(SimpleInjectorAddOptions options) + { + // Use the proper IpPoolManager instead of a mock as the code quite + // interdependent as it modifies the same EF Core entities. + options.Container.Register(Lifestyle.Scoped); + options.Container.Register(Lifestyle.Scoped); + } + + protected override async Task SeedAsync(IStateStore stateStore) + { + await SeedDefaultTenantAndProject(); + + await stateStore.For().AddAsync( + new ProviderSubnet + { + Id = Guid.Parse(DefaultSubnetId), + ProviderName = EryphConstants.DefaultProviderName, + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.0.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.0.0.0/16", + FirstIp = "10.0.0.10", + NextIp = "10.0.0.12", + LastIp = "10.0.0.19", + }, + new IpPool() + { + Id = Guid.NewGuid(), + Name = "second-pool", + IpNetwork = "10.0.0.0/16", + FirstIp = "10.0.1.10", + NextIp = "10.0.1.12", + LastIp = "10.0.1.19", + } + ] + }); + + await stateStore.For().AddAsync( + new ProviderSubnet + { + Id = Guid.Parse(SecondSubnetId), + ProviderName = EryphConstants.DefaultProviderName, + Name = "second-subnet", + IpNetwork = "10.1.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.1.0.0/16", + FirstIp = "10.1.0.10", + NextIp = "10.1.0.12", + LastIp = "10.1.0.19", + }, + ], + }); + + await stateStore.For().AddAsync( + new ProviderSubnet + { + Id = Guid.Parse(SecondProviderSubnetId), + ProviderName = "second-provider", + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.10.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.10.0.0/16", + FirstIp = "10.10.0.10", + NextIp = "10.10.0.12", + LastIp = "10.10.0.19", + }, + ], + }); + } +} From 2e8a9bc2517c6acf5abd9d19a4d37ebedf31eea2 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Tue, 12 Nov 2024 19:54:06 +0100 Subject: [PATCH 07/30] Simplify network lookup --- .../Specifications/ProviderRouterPortSpecs.cs | 20 +++ .../Networks/CatletIpManager.cs | 144 ++---------------- .../Networks/ICatletIpManager.cs | 18 +-- .../UpdateCatletNetworksCommandHandler.cs | 55 ++++--- .../Networks/CatletIpManagerTests.cs | 45 +++--- ...UpdateCatletNetworksCommandHandlerTests.cs | 34 +++++ 6 files changed, 138 insertions(+), 178 deletions(-) create mode 100644 src/data/src/Eryph.StateDb/Specifications/ProviderRouterPortSpecs.cs create mode 100644 src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs diff --git a/src/data/src/Eryph.StateDb/Specifications/ProviderRouterPortSpecs.cs b/src/data/src/Eryph.StateDb/Specifications/ProviderRouterPortSpecs.cs new file mode 100644 index 000000000..0f2790074 --- /dev/null +++ b/src/data/src/Eryph.StateDb/Specifications/ProviderRouterPortSpecs.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Ardalis.Specification; +using Eryph.StateDb.Model; + +namespace Eryph.StateDb.Specifications; + +public static class ProviderRouterPortSpecs +{ + public sealed class GetByNetworkId : Specification, ISingleResultSpecification + { + public GetByNetworkId(Guid networkId) + { + Query.Where(x => x.NetworkId == networkId); + } + } +} \ No newline at end of file diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs index ee769d021..3daea0634 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs @@ -22,17 +22,11 @@ public class CatletIpManager( : BaseIpManager(stateStore, poolManager), ICatletIpManager { public EitherAsync ConfigurePortIps( - Guid projectId, - string environment, + VirtualNetwork network, CatletNetworkPort port, CatletNetworkConfig networkConfig, CancellationToken cancellationToken) => - from environmentName in EnvironmentName.NewEither(environment) - .ToAsync() - from networkName in Optional(networkConfig.Name) - .Map(EryphNetworkName.NewEither) - .IfNone(EryphNetworkName.New(EryphConstants.DefaultNetworkName)) - .ToAsync() + from _ in RightAsync(unit) let subnetName = Optional(networkConfig.SubnetV4?.Name) .IfNone(EryphConstants.DefaultSubnetName) let ipPoolName = Optional(networkConfig.SubnetV4?.IpPool) @@ -41,16 +35,16 @@ from ipAssignments in _stateStore.For().IO.ListAsync( new IPAssignmentSpecs.GetByPort(port.Id), cancellationToken) let validDirectAssignments = ipAssignments - .Filter(a => a is not IpPoolAssignment && IsValidAssignment(a, networkName, subnetName)) + .Filter(a => a is not IpPoolAssignment && IsValidAssignment(a, network, subnetName)) let validPoolAssignments = ipAssignments .OfType().ToSeq() - .Filter(a => IsValidPoolAssignment(a, networkName, subnetName, ipPoolName)) + .Filter(a => IsValidPoolAssignment(a, network, subnetName, ipPoolName)) let invalidAssignments = ipAssignments.Except(validDirectAssignments).Except(validPoolAssignments) - from _ in invalidAssignments + from __ in invalidAssignments .Map(a => _stateStore.For().IO.DeleteAsync(a)) .SequenceSerial() from newAssignment in validPoolAssignments.IsEmpty - ? from assignment in CreateAssignment(projectId, environmentName.Value, port, networkName, subnetName, ipPoolName, cancellationToken) + ? from assignment in CreateAssignment(network, port, subnetName, ipPoolName, cancellationToken) select Some(assignment) : RightAsync>(None) select validPoolAssignments.Append(newAssignment).Append(validDirectAssignments) @@ -58,149 +52,37 @@ select validPoolAssignments.Append(newAssignment).Append(validDirectAssignments) .ToArray(); private EitherAsync CreateAssignment( - Guid projectId, - string environment, + VirtualNetwork network, CatletNetworkPort port, - EryphNetworkName networkName, string subnetName, string ipPoolName, CancellationToken cancellationToken) => - from network in _stateStore.Read().IO.GetBySpecAsync( - new VirtualNetworkSpecs.GetByName(projectId, networkName.Value, environment), - cancellationToken) - // It is optional to have an environment specific network. Therefore, - // we fall back to the network in default environment. - from validNetwork in network.IsNone && environment != EryphConstants.DefaultEnvironmentName - ? _stateStore.Read().IO.GetBySpecAsync( - new VirtualNetworkSpecs.GetByName(projectId, networkName.Value, EryphConstants.DefaultEnvironmentName), - cancellationToken) - .Bind(fr => fr.ToEitherAsync(Error.New($"Network {networkName} not found in environment {environment} and default environment."))) - : network.ToEitherAsync(Error.New($"Environment {environment}: Network {networkName} not found.")) - from subnet in _stateStore.Read().IO.GetBySpecAsync( - new SubnetSpecs.GetByNetwork(validNetwork.Id, subnetName), + new SubnetSpecs.GetByNetwork(network.Id, subnetName), cancellationToken) from validSubnet in subnet.ToEitherAsync( - Error.New($"Environment {environment}: Subnet {subnetName} not found in network {networkName}.")) + Error.New($"Environment {network.Environment}: Subnet {subnetName} not found in network {network.Name}.")) from assignment in _poolManager.AcquireIp(validSubnet.Id, ipPoolName, cancellationToken) let _ = UpdatePortAssignment(port, assignment) select assignment; - /* - public EitherAsync ConfigurePortIps2( - Guid projectId, - string environment, - CatletNetworkPort port, - CatletNetworkConfig networkConfig, - CancellationToken cancellationToken) - { - // TODO Why does this iterate over all ports? - var portNetworks = networkConfigs.Map(x => - new PortNetwork(x.Name, - x.SubnetV4 == null - ? Option.None - : x.SubnetV4.Name , - x.SubnetV4 == null - ? Option.None - : x.SubnetV4.IpPool)); - - var getPortAssignments = - Prelude.TryAsync(_stateStore.For().ListAsync(new IPAssignmentSpecs.GetByPort(port.Id), - c)) - .ToEither(f => Error.New(f)); - - return - from portAssignments in getPortAssignments - from validAssignments in portAssignments.Map( - assignment => CheckAssignmentConfigured(assignment, networkConfig).ToAsync()) - .TraverseSerial(l => l.AsEnumerable()) - .Map(e => e.Flatten()) - - from validAndNewAssignments in portNetworks.Map(portNetwork => - { - var networkNameString = portNetwork.NetworkName.IfNone("default"); - var subnetNameString = portNetwork.SubnetName.IfNone("default"); - var poolNameString = portNetwork.PoolName.IfNone("default"); - return - from network in _stateStore.Read() - .IO.GetBySpecAsync(new VirtualNetworkSpecs.GetByName(projectId, networkNameString,environment), cancellationToken) - .Bind(r => - // it is optional to have a environment specific network - // therefore fallback to network in default environment if not found - r.IsNone && environment != "default" ? - _stateStore.Read() - .IO.GetBySpecAsync(new VirtualNetworkSpecs.GetByName(projectId, networkNameString, "default"), cancellationToken) - .Bind(fr => fr.ToEitherAsync(Error.New($"Network {networkNameString} not found in environment {environment} and default environment."))) - : r.ToEitherAsync(Error.New($"Environment {environment}: Network {networkNameString} not found."))) - - from subnet in _stateStore.Read().IO - .GetBySpecAsync(new SubnetSpecs.GetByNetwork(network.Id, subnetNameString), cancellationToken) - .Bind(r => r.ToEitherAsync( - Error.New($"Environment {environment}: Subnet {subnetNameString} not found in network {networkNameString}."))) - - let existingAssignment = CheckAssignmentExist(validAssignments, subnet, poolNameString) - - from assignment in existingAssignment.IsSome ? - existingAssignment.ToEitherAsync(Errors.None) - : from newAssignment in _poolManager.AcquireIp(subnet.Id, poolNameString, cancellationToken) - .Map(a => (IpAssignment)a) - let _ = UpdatePortAssignment(port, newAssignment) - select newAssignment - select assignment; - - }).SequenceSerial() - - select validAndNewAssignments - .Select(x => IPAddress.Parse(x.IpAddress)).ToArray(); - - } - */ - /* - private EitherAsync> CheckAssignmentConfigured( - IpAssignment assignment, - EryphNetworkName configuredNetworkName, - string configuredSubnetName, - string configuredPoolName) => - from _ in RightAsync(unit) - let networkName = assignment.Subnet switch - { - VirtualNetworkSubnet subnet => Some(EryphNetworkName.New(subnet.Network.Name)), - _ => None - } - let subnetName = assignment.Subnet.Name - let isValid = networkName.Match( - Some: validNetworkName => assignment switch - { - IpPoolAssignment poolAssignment => validNetworkName == configuredNetworkName - && subnetName == configuredSubnetName - && poolAssignment.Pool.Name == configuredPoolName, - _ => validNetworkName == configuredNetworkName - && subnetName == configuredSubnetName, - }, - None: () => false) - from result in isValid - ? RightAsync>(assignment) - : from _ in _stateStore.For().IO.DeleteAsync(assignment) - select Option.None - select result; - */ // TODO Check environment match // TODO Check IP address is valid private static bool IsValidAssignment( IpAssignment assignment, - EryphNetworkName configuredNetwork, + VirtualNetwork network, string configuredSubnet) => assignment.Subnet is VirtualNetworkSubnet subnet - && EryphNetworkName.New(subnet.Network.Name) == configuredNetwork + && subnet.NetworkId == network.Id && subnet.Name == configuredSubnet; private static bool IsValidPoolAssignment( IpPoolAssignment assignment, - EryphNetworkName configuredNetwork, + VirtualNetwork network, string configuredSubnet, string configuredPool) => - IsValidAssignment(assignment, configuredNetwork, configuredSubnet) + IsValidAssignment(assignment, network, configuredSubnet) && assignment.Pool.Name == configuredPool; } diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/ICatletIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/ICatletIpManager.cs index fee0c4aa6..cf7ba6558 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/ICatletIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/ICatletIpManager.cs @@ -6,15 +6,13 @@ using LanguageExt; using LanguageExt.Common; -namespace Eryph.Modules.Controller.Networks +namespace Eryph.Modules.Controller.Networks; + +public interface ICatletIpManager { - public interface ICatletIpManager - { - public EitherAsync ConfigurePortIps( - Guid projectId, - string environment, - CatletNetworkPort port, - CatletNetworkConfig networkConfig, - CancellationToken cancellationToken); - } + public EitherAsync ConfigurePortIps( + VirtualNetwork network, + CatletNetworkPort port, + CatletNetworkConfig networkConfig, + CancellationToken cancellationToken); } diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs index e9d6c311f..a765378dc 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs @@ -62,6 +62,7 @@ private EitherAsync> UpdateNetworks( UpdateCatletNetworksCommand command) => from catletResult in _stateStore.For().IO.GetByIdAsync(command.CatletId) from catlet in catletResult.ToEitherAsync(Error.New($"Catlet {command.CatletId} not found.")) + // TODO delete ports which are no longer configured from settings in command.Config.Networks .ToSeq() .Map(cfg => UpdateNetwork(catlet.MetadataId, command, cfg)) @@ -73,16 +74,22 @@ private EitherAsync UpdateNetwork( Guid catletMetadataId, UpdateCatletNetworksCommand command, CatletNetworkConfig networkConfig) => - from environmentName in EnvironmentName.NewEither(command.Config.Environment) + from environmentName in EnvironmentName.NewEither( + Optional(command.Config.Environment) + .Filter(notEmpty) + .IfNone(EryphConstants.DefaultEnvironmentName)) .ToAsync() - from networkName in EryphNetworkName.NewEither(networkConfig.Name) + from networkName in EryphNetworkName.NewEither( + Optional(networkConfig.Name) + .Filter(notEmpty) + .IfNone(EryphConstants.DefaultNetworkName)) .ToAsync() - from network in _stateStore.Read().IO.GetBySpecAsync( + from network in _stateStore.For().IO.GetBySpecAsync( new VirtualNetworkSpecs.GetByName(command.ProjectId, networkName.Value, environmentName.Value)) // It is optional to have an environment specific network. Therefore, // we fall back to the network in the default environment. from validNetwork in network.IsNone && environmentName != EnvironmentName.New(EryphConstants.DefaultEnvironmentName) - ? _stateStore.Read().IO.GetBySpecAsync( + ? _stateStore.For().IO.GetBySpecAsync( new VirtualNetworkSpecs.GetByName(command.ProjectId, networkName.Value, EryphConstants.DefaultEnvironmentName)) .Bind(fr => fr.ToEitherAsync(Error.New($"Network '{networkName}' not found in environment '{environmentName}' and default environment."))) : network.ToEitherAsync(Error.New($"Network '{networkName}' not found in environment '{environmentName}'.")) @@ -100,8 +107,7 @@ from networkProvider in networkProviders.NetworkProviders.Find(x => x.Name == va .Bind(x => Optional(x.MacAddress)) let c1 = new CancellationTokenSource(500000) - // TODO update assignment of port to network when the network changed in the config - // TODO delete ports which are no longer configured + from networkPort in AddOrUpdateAdapterPort( validNetwork, command.CatletId, catletMetadataId, networkConfig.AdapterName, @@ -109,21 +115,27 @@ from networkPort in AddOrUpdateAdapterPort( let c2 = new CancellationTokenSource() + let providerSubnetName = validNetwork.Subnets + from floatingPort in isFlatNetwork - ? RightAsync>(Option.None) - : UpdateFloatingPort( - networkPort, Option.None, - "default", "default", - "default", c2.Token) + ? from _ in Optional(networkPort.FloatingPort) + .Map(fp => _stateStore.For().IO.DeleteAsync(fp)) + .Sequence() + select Option.None + : from providerPort in _stateStore.For().IO.GetBySpecAsync( + new ProviderRouterPortSpecs.GetByNetworkId(validNetwork.Id)) + from validProviderPort in providerPort.ToEitherAsync( + Error.New($"The overlay network '{validNetwork.Name}' has no provider port.")) + from fp in UpdateFloatingPort( + networkPort, networkProvider.Name, validProviderPort.SubnetName, validProviderPort.PoolName, c2.Token) .ToAsync() - .Map(Option.Some) + select Some(fp) let c3 = new CancellationTokenSource() from ips in isFlatNetwork ? RightAsync([]) : _ipManager.ConfigurePortIps( - command.ProjectId, - command.Config.Environment ?? "default", networkPort, + validNetwork, networkPort, networkConfig, c3.Token) @@ -169,6 +181,7 @@ from updatedPort in existingPort.Match( { p.AddressName = addressName; p.MacAddress = MacAddresses.FormatMacAddress(macAddress); + p.Network = network; }) select p, None: () => from _ in RightAsync(unit) @@ -178,7 +191,7 @@ from updatedPort in existingPort.Match( CatletMetadataId = catletMetadataId, Name = portName, MacAddress = MacAddresses.FormatMacAddress(macAddress), - NetworkId = network.Id, + Network = network, AddressName = addressName, IpAssignments = [], } @@ -190,7 +203,6 @@ from addedPort in _stateStore.For().IO.AddAsync(newPort, canc private async Task> UpdateFloatingPort( CatletNetworkPort adapterPort, - Option portName, string providerName, string providerSubnetName, string providerPoolName, @@ -205,9 +217,7 @@ private async Task> UpdateFloatingPort( providerSubnetName || floatingPort.PoolName != providerPoolName) { adapterPort.FloatingPort = null; - - if (portName.IsNone) // not a named port, then remove - await _stateStore.For().DeleteAsync(floatingPort, cancellationToken); + await _stateStore.For().DeleteAsync(floatingPort, cancellationToken); } } @@ -218,7 +228,7 @@ private async Task> UpdateFloatingPort( var port = new FloatingNetworkPort { Id = Guid.NewGuid(), - Name = portName.IfNone(adapterPort.Name), + Name = adapterPort.Name, ProviderName = providerName, SubnetName = providerSubnetName, PoolName = providerPoolName, @@ -230,6 +240,11 @@ private async Task> UpdateFloatingPort( return await _stateStore.For().AddAsync(port, cancellationToken); } + // TODO Remove port and attached floating port if necessary + private EitherAsync RemovePort() => Error.New("not implemented"); + + private EitherAsync RemoveFloatingPort() => Error.New("not implemented"); + private static string GetPortName(Guid catletId, string adapterName) => $"{catletId}_{adapterName}"; } diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs index e19077f1c..fc1859270 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs @@ -36,7 +36,7 @@ public sealed class CatletIpManagerTests : InMemoryStateDbTestBase [InlineData(DefaultProjectId, "default", DefaultNetworkId, "default", "default", "second-pool", "10.0.1.12")] [InlineData(DefaultProjectId, "default", DefaultNetworkId, "default", "second-subnet", "default", "10.1.0.12")] [InlineData(DefaultProjectId, "default", SecondNetworkId, "second-network", "default", "default", "10.5.0.12")] - [InlineData(DefaultProjectId, "second-environment", DefaultNetworkId, "default", "default", "default", "10.10.0.12")] + [InlineData(DefaultProjectId, "second-environment", SecondEnvironmentNetworkId, "default", "default", "default", "10.10.0.12")] // When the environment does not have a dedicated network, we should fall back to the default network [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, null, null, null, "10.0.0.12")] [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, "default", "default", "default", "10.0.0.12")] @@ -68,19 +68,22 @@ public async Task ConfigurePortIps_NewPortIsAdded_AssignmentIsCreated( await WithScope(async (ipManager, _, stateStore) => { + var network = await stateStore.For() + .GetByIdAsync(Guid.Parse(networkId)); + network.Should().NotBeNull(); + var catletPort = new CatletNetworkPort { Id = CatletPortId, Name = "test-catlet-port", MacAddress = "00:00:00:00:00:01", - NetworkId = Guid.Parse(networkId), + Network = network!, CatletMetadataId = Guid.Parse(CatletMetadataId), }; await stateStore.For().AddAsync(catletPort); var result = await ipManager.ConfigurePortIps( - Guid.Parse(projectId), - environment, catletPort, networkConfig, + network!, catletPort, networkConfig, CancellationToken.None); result.Should().BeRight().Which.Should().SatisfyRespectively( @@ -116,7 +119,7 @@ await WithScope(async (_, _, stateStore) => [InlineData(DefaultProjectId, "environment-without-network", SecondNetworkId, SecondNetworkSubnetId, "second-network", "default", "default", "10.5.0.12")] [InlineData(SecondProjectId, "default", SecondProjectNetworkId, SecondProjectSubnetId, null, null, null, "10.100.0.12")] [InlineData(SecondProjectId, "default", SecondProjectNetworkId, SecondProjectSubnetId, "default", "default", "default", "10.100.0.12")] - public async Task ConfigureFloatingPortIps_AssignmentIsValid_AssignmentIsNotChanged( + public async Task ConfigurePortIps_AssignmentIsValid_AssignmentIsNotChanged( string projectId, string environment, string networkId, @@ -162,12 +165,15 @@ await WithScope(async (_, ipPoolManager, stateStore) => await WithScope(async (catletIpManager, _, stateStore) => { - var catletPort = await stateStore.Read() + var catletPort = await stateStore.For() .GetByIdAsync(CatletPortId); catletPort.Should().NotBeNull(); + var network = await stateStore.For() + .GetByIdAsync(Guid.Parse(networkId)); + network.Should().NotBeNull(); var result = await catletIpManager.ConfigurePortIps( - Guid.Parse(projectId), environment, catletPort!, networkConfig, + network!, catletPort!, networkConfig, CancellationToken.None); result.Should().BeRight().Which.Should().SatisfyRespectively( @@ -190,19 +196,20 @@ await WithScope(async (_, _, stateStore) => } [Theory] - [InlineData(DefaultProjectId, "default", "default", "default", "second-pool", "10.0.1.12")] - [InlineData(DefaultProjectId, "default", "default", "second-subnet", "default", "10.1.0.12")] - [InlineData(DefaultProjectId, "default", "second-network", "default", "default", "10.5.0.12")] - [InlineData(DefaultProjectId, "second-environment", "default", "default", "default", "10.10.0.12")] + [InlineData(DefaultProjectId, "default", DefaultNetworkId, "default", "default", "second-pool", "10.0.1.12")] + [InlineData(DefaultProjectId, "default", DefaultNetworkId, "default", "second-subnet", "default", "10.1.0.12")] + [InlineData(DefaultProjectId, "default", SecondNetworkId, "second-network", "default", "default", "10.5.0.12")] + [InlineData(DefaultProjectId, "second-environment", SecondEnvironmentNetworkId, "default", "default", "default", "10.10.0.12")] // When the environment does not have a dedicated network, we should fall back to the default network - [InlineData(DefaultProjectId, "environment-without-network", "default", "default", "second-pool", "10.0.1.12")] - [InlineData(DefaultProjectId, "environment-without-network", "default", "second-subnet", "default", "10.1.0.12")] - [InlineData(DefaultProjectId, "environment-without-network", "second-network", "default", "default", "10.5.0.12")] - [InlineData(SecondProjectId, "default", null, null, null, "10.100.0.12")] - [InlineData(SecondProjectId, "default", "default", "default", "default", "10.100.0.12")] + [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, "default", "default", "second-pool", "10.0.1.12")] + [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, "default", "second-subnet", "default", "10.1.0.12")] + [InlineData(DefaultProjectId, "environment-without-network", SecondNetworkId, "second-network", "default", "default", "10.5.0.12")] + [InlineData(SecondProjectId, "default", SecondProjectNetworkId, null, null, null, "10.100.0.12")] + [InlineData(SecondProjectId, "default", SecondProjectNetworkId, "default", "default", "default", "10.100.0.12")] public async Task ConfigurePortIps_AssignmentIsInvalid_AssignmentIsChanged( string projectId, string environment, + string networkId, string? networkName, string? subnetName, string? poolName, @@ -249,8 +256,12 @@ await WithScope(async (catletIpManager, _, stateStore) => .GetByIdAsync(CatletPortId); catletPort.Should().NotBeNull(); + var network = await stateStore.Read() + .GetByIdAsync(Guid.Parse(networkId)); + network.Should().NotBeNull(); + var result = await catletIpManager.ConfigurePortIps( - Guid.Parse(projectId), environment, catletPort!, networkConfig, + network!, catletPort!, networkConfig, CancellationToken.None); result.Should().BeRight().Which.Should().SatisfyRespectively( diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs new file mode 100644 index 000000000..28cc6503a --- /dev/null +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Eryph.StateDb.TestBase; + +namespace Eryph.Modules.Controller.Tests.Networks; + +public class UpdateCatletNetworksCommandHandlerTests : InMemoryStateDbTestBase +{ + [Fact] + public async Task UpdateNetworks_SwitchFromOverlayToFlat_CreatesCorrectNetworkConfig() + { + + // TODO Verify flat network creates a port + // TODO Verify that the floating port and IP assignment were removed + } + + [Fact] + public async Task UpdateNetworks_SwitchFromFlatToOverlay_CreatesCorrectNetworkConfig() + { + // TODO Verify that the floating port and IP assignment are created + } + + [Fact] + public async Task UpdateNetworks_RemoveNetwork_CreatesCorrectNetworkConfig() + { + // TODO Verify that the port and assignment were deleted + } + + // TODO test change of project + // TODO test change of environment +} \ No newline at end of file From 8a3f3f34823c8a3aafdd6f26eaee79c80bdc0635 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Wed, 13 Nov 2024 16:00:06 +0100 Subject: [PATCH 08/30] Add tests --- .../UpdateCatletNetworksCommandHandler.cs | 57 +-- .../NetworkProvidersConfigRealizerTests.cs | 207 +++++++++ ...UpdateCatletNetworksCommandHandlerTests.cs | 425 +++++++++++++++++- 3 files changed, 651 insertions(+), 38 deletions(-) create mode 100644 src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs index a765378dc..537d74163 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs @@ -27,40 +27,29 @@ namespace Eryph.Modules.Controller.Networks; #pragma warning restore 1998 [UsedImplicitly] -public class UpdateCatletNetworksCommandHandler : IHandleMessages> +public class UpdateCatletNetworksCommandHandler( + ITaskMessaging messaging, + ICatletIpManager ipManager, + IStateStore stateStore, + INetworkProviderManager providerManager, + IProviderIpManager providerIpManager) + : IHandleMessages> { - private readonly ITaskMessaging _messaging; - private readonly IStateStore _stateStore; - private readonly ICatletIpManager _ipManager; - private readonly IProviderIpManager _providerIpManager; - private readonly INetworkProviderManager _providerManager; - - - public UpdateCatletNetworksCommandHandler(ITaskMessaging messaging, ICatletIpManager ipManager, - IStateStore stateStore, INetworkProviderManager providerManager, IProviderIpManager providerIpManager) - { - _messaging = messaging; - _ipManager = ipManager; - _stateStore = stateStore; - _providerManager = providerManager; - _providerIpManager = providerIpManager; - } - public async Task Handle(OperationTask message) { - await _messaging.ProgressMessage(message, "Updating Catlet network settings"); + await messaging.ProgressMessage(message, "Updating Catlet network settings"); await UpdateNetworks(message.Command) .Map(settings => new UpdateCatletNetworksCommandResponse { NetworkSettings = settings.ToArray() }) - .FailOrComplete(_messaging, message); + .FailOrComplete(messaging, message); } - private EitherAsync> UpdateNetworks( + public EitherAsync> UpdateNetworks( UpdateCatletNetworksCommand command) => - from catletResult in _stateStore.For().IO.GetByIdAsync(command.CatletId) + from catletResult in stateStore.For().IO.GetByIdAsync(command.CatletId) from catlet in catletResult.ToEitherAsync(Error.New($"Catlet {command.CatletId} not found.")) // TODO delete ports which are no longer configured from settings in command.Config.Networks @@ -84,17 +73,17 @@ from networkName in EryphNetworkName.NewEither( .Filter(notEmpty) .IfNone(EryphConstants.DefaultNetworkName)) .ToAsync() - from network in _stateStore.For().IO.GetBySpecAsync( + from network in stateStore.For().IO.GetBySpecAsync( new VirtualNetworkSpecs.GetByName(command.ProjectId, networkName.Value, environmentName.Value)) // It is optional to have an environment specific network. Therefore, // we fall back to the network in the default environment. from validNetwork in network.IsNone && environmentName != EnvironmentName.New(EryphConstants.DefaultEnvironmentName) - ? _stateStore.For().IO.GetBySpecAsync( + ? stateStore.For().IO.GetBySpecAsync( new VirtualNetworkSpecs.GetByName(command.ProjectId, networkName.Value, EryphConstants.DefaultEnvironmentName)) .Bind(fr => fr.ToEitherAsync(Error.New($"Network '{networkName}' not found in environment '{environmentName}' and default environment."))) : network.ToEitherAsync(Error.New($"Network '{networkName}' not found in environment '{environmentName}'.")) - from networkProviders in _providerManager.GetCurrentConfiguration() + from networkProviders in providerManager.GetCurrentConfiguration() from networkProvider in networkProviders.NetworkProviders.Find(x => x.Name == validNetwork.NetworkProvider) .ToEither(Error.New($"network provider {validNetwork.NetworkProvider} not found.")) .ToAsync() @@ -119,10 +108,10 @@ from networkPort in AddOrUpdateAdapterPort( from floatingPort in isFlatNetwork ? from _ in Optional(networkPort.FloatingPort) - .Map(fp => _stateStore.For().IO.DeleteAsync(fp)) + .Map(fp => stateStore.For().IO.DeleteAsync(fp)) .Sequence() select Option.None - : from providerPort in _stateStore.For().IO.GetBySpecAsync( + : from providerPort in stateStore.For().IO.GetBySpecAsync( new ProviderRouterPortSpecs.GetByNetworkId(validNetwork.Id)) from validProviderPort in providerPort.ToEitherAsync( Error.New($"The overlay network '{validNetwork.Name}' has no provider port.")) @@ -134,7 +123,7 @@ select Some(fp) let c3 = new CancellationTokenSource() from ips in isFlatNetwork ? RightAsync([]) - : _ipManager.ConfigurePortIps( + : ipManager.ConfigurePortIps( validNetwork, networkPort, networkConfig, c3.Token) @@ -143,7 +132,7 @@ from floatingIps in isFlatNetwork ? RightAsync([]) : floatingPort.ToEither(Error.New("floating port is missing")) .ToAsync() - .Bind(p => _providerIpManager.ConfigureFloatingPortIps(networkProvider.Name, p, c3.Token)) + .Bind(p => providerIpManager.ConfigureFloatingPortIps(networkProvider.Name, p, c3.Token)) select new MachineNetworkSettings { @@ -173,7 +162,7 @@ from _ in RightAsync(unit) let macAddress = fixedMacAddress .Filter(notEmpty) .IfNone(() => MacAddresses.GenerateMacAddress(portName)) - from existingPort in _stateStore.For().IO.GetBySpecAsync( + from existingPort in stateStore.For().IO.GetBySpecAsync( new NetworkPortSpecs.GetByNetworkAndNameForCatlet(network.Id, portName), cancellationToken) from updatedPort in existingPort.Match( Some: p => from _ in RightAsync(unit) @@ -195,7 +184,7 @@ from updatedPort in existingPort.Match( AddressName = addressName, IpAssignments = [], } - from addedPort in _stateStore.For().IO.AddAsync(newPort, cancellationToken) + from addedPort in stateStore.For().IO.AddAsync(newPort, cancellationToken) select addedPort) select updatedPort; @@ -208,7 +197,7 @@ private async Task> UpdateFloatingPort( string providerPoolName, CancellationToken cancellationToken) { - await _stateStore.LoadPropertyAsync(adapterPort, x => x.FloatingPort, cancellationToken); + await stateStore.LoadPropertyAsync(adapterPort, x => x.FloatingPort, cancellationToken); if (adapterPort.FloatingPort != null) { @@ -217,7 +206,7 @@ private async Task> UpdateFloatingPort( providerSubnetName || floatingPort.PoolName != providerPoolName) { adapterPort.FloatingPort = null; - await _stateStore.For().DeleteAsync(floatingPort, cancellationToken); + await stateStore.For().DeleteAsync(floatingPort, cancellationToken); } } @@ -237,7 +226,7 @@ private async Task> UpdateFloatingPort( adapterPort.FloatingPort = port; - return await _stateStore.For().AddAsync(port, cancellationToken); + return await stateStore.For().AddAsync(port, cancellationToken); } // TODO Remove port and attached floating port if necessary diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs new file mode 100644 index 000000000..b72d7671a --- /dev/null +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Eryph.Core.Network; +using Eryph.Modules.Controller.Networks; +using Eryph.StateDb; +using Eryph.StateDb.Model; +using Eryph.StateDb.TestBase; +using SimpleInjector.Integration.ServiceCollection; + +namespace Eryph.Modules.Controller.Tests.Networks; + +public class NetworkProvidersConfigRealizerTests : InMemoryStateDbTestBase +{ + private readonly NetworkProvidersConfiguration _config = new() + { + NetworkProviders = + [ + new NetworkProvider + { + Name = "default", + TypeString = "nat_overlay", + BridgeName = "br-nat", + Subnets = + [ + new NetworkProviderSubnet + { + Name = "default", + Network = "10.249.248.0/24", + Gateway = "10.249.248.1", + IpPools = + [ + new NetworkProviderIpPool + { + Name = "default", + FirstIp = "10.249.248.10", + NextIp = "10.249.248.12", + LastIp = "10.249.248.19" + }, + new NetworkProviderIpPool + { + Name = "second-provider-pool", + FirstIp = "10.249.248.20", + NextIp = "10.249.248.22", + LastIp = "10.249.248.29" + }, + ], + }, + new NetworkProviderSubnet + { + Name = "second-provider-subnet", + Network = "10.249.249.0/24", + Gateway = "10.249.249.1", + IpPools = + [ + new NetworkProviderIpPool + { + Name = "default", + FirstIp = "10.249.249.10", + NextIp = "10.249.249.12", + LastIp = "10.249.249.19" + }, + ], + }, + ], + }, + new NetworkProvider + { + Name = "flat-provider", + TypeString = "flat", + }, + ] + }; + + [Fact] + public async Task RealizeConfigAsync_NoExistingSubnets_CreatesCorrectSubnets() + { + await WithScope(async (realizer, _) => + { + await realizer.RealizeConfigAsync(_config, default); + }); + + await WithScope(async (_, stateStore) => + { + var subnets = await stateStore.For().ListAsync(); + subnets.Should().HaveCount(2); + + { + var subnet = subnets.Should().ContainSingle(s => s.Name == "default").Subject; + await stateStore.LoadCollectionAsync(subnet, s => s.IpPools); + + subnet.IpNetwork.Should().Be("10.249.248.0/24"); + subnet.IpPools.Should().HaveCount(2); + + var defaultPool = subnet.IpPools.Should().ContainSingle(p => p.Name == "default").Subject; + defaultPool.FirstIp.Should().Be("10.249.248.10"); + defaultPool.NextIp.Should().Be("10.249.248.12"); + defaultPool.LastIp.Should().Be("10.249.248.19"); + + var secondPool = subnet.IpPools.Should().ContainSingle(p => p.Name == "second-provider-pool").Subject; + secondPool.FirstIp.Should().Be("10.249.248.20"); + secondPool.NextIp.Should().Be("10.249.248.22"); + secondPool.LastIp.Should().Be("10.249.248.29"); + } + + { + var subnet = subnets.Should().ContainSingle(s => s.Name == "second-provider-subnet").Subject; + await stateStore.LoadCollectionAsync(subnet, s => s.IpPools); + + subnet.IpNetwork.Should().Be("10.249.249.0/24"); + subnet.IpPools.Should().HaveCount(1); + + var defaultPool = subnet.IpPools.Should().ContainSingle(p => p.Name == "default").Subject; + defaultPool.FirstIp.Should().Be("10.249.249.10"); + defaultPool.NextIp.Should().Be("10.249.249.12"); + defaultPool.LastIp.Should().Be("10.249.249.19"); + } + }); + } + + [Fact] + public async Task RealizeConfigAsync_ExistingSubnets_RemovesOldSubnets() + { + await WithScope(async (realizer, _) => + { + await realizer.RealizeConfigAsync(_config, default); + }); + + await WithScope(async (_, stateStore) => + { + var subnetCount = await stateStore.For().CountAsync(); + subnetCount.Should().Be(2); + + var poolCount = await stateStore.For().CountAsync(); + poolCount.Should().Be(3); + }); + + await WithScope(async (realizer, _) => + { + var updatedConfig = new NetworkProvidersConfiguration + { + NetworkProviders = + [ + new NetworkProvider + { + Name = "default", + TypeString = "nat_overlay", + BridgeName = "br-nat", + Subnets = + [ + new NetworkProviderSubnet + { + Name = "default", + Network = "10.249.248.0/24", + Gateway = "10.249.248.1", + IpPools = + [ + new NetworkProviderIpPool + { + Name = "default", + FirstIp = "10.249.248.10", + NextIp = "10.249.248.12", + LastIp = "10.249.248.19" + }, + ], + }, + ], + }, + ], + }; + await realizer.RealizeConfigAsync(updatedConfig, default); + + await WithScope(async (_, stateStore) => + { + var subnets = await stateStore.For().ListAsync(); + subnets.Should().HaveCount(1); + + var subnet = subnets.Should().ContainSingle(s => s.Name == "default").Subject; + await stateStore.LoadCollectionAsync(subnet, s => s.IpPools); + + subnet.IpNetwork.Should().Be("10.249.248.0/24"); + subnet.IpPools.Should().HaveCount(1); + + var defaultPool = subnet.IpPools.Should().ContainSingle(p => p.Name == "default").Subject; + defaultPool.FirstIp.Should().Be("10.249.248.10"); + defaultPool.NextIp.Should().Be("10.249.248.12"); + defaultPool.LastIp.Should().Be("10.249.248.19"); + + }); + }); + } + + private async Task WithScope(Func func) + { + await using var scope = CreateScope(); + var catletIpManager = scope.GetInstance(); + var stateStore = scope.GetInstance(); + await func(catletIpManager, stateStore); + } + + protected override void AddSimpleInjector(SimpleInjectorAddOptions options) + { + options.Container.Register(); + } +} diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs index 28cc6503a..19c9e72bb 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs @@ -3,20 +3,98 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Dbosoft.Rebus.Operations; +using Eryph.ConfigModel.Catlets; +using Eryph.Modules.Controller.Networks; +using Eryph.StateDb; using Eryph.StateDb.TestBase; +using Moq; +using SimpleInjector.Integration.ServiceCollection; +using SimpleInjector; +using Eryph.Core; +using Eryph.Core.Network; +using Eryph.Messages.Resources.Catlets.Commands; +using Eryph.StateDb.Model; +using LanguageExt; +using LanguageExt.Common; namespace Eryph.Modules.Controller.Tests.Networks; public class UpdateCatletNetworksCommandHandlerTests : InMemoryStateDbTestBase { + private const string DefaultProjectId = "4b4a3fcf-b5ed-4a9a-ab6e-03852752095e"; + private const string SecondProjectId = "75c27daf-77c8-4b98-a072-a4706dceb422"; + + private const string DefaultNetworkId = "cb58fe00-3f64-4b66-b58e-23fb15df3cac"; + private const string DefaultSubnetId = "ed6697cd-836f-4da7-914b-b09ed1567934"; + private const string SecondSubnetId = "4f976208-613a-40d4-a284-d32cbd4a1b8e"; + + private const string SecondNetworkId = "e480a020-57d0-4443-a973-57aa0c95872e"; + private const string SecondNetworkSubnetId = "27ec11a4-5d6a-47da-9f9f-eb7486db38ea"; + + private const string SecondEnvironmentNetworkId = "81a139e5-ab61-4fe3-b81f-59c11a665d22"; + private const string SecondEnvironmentSubnetId = "dc807357-50e7-4263-8298-0c97ff69f4cf"; + + private const string SecondProjectNetworkId = "c0043e88-8268-4ac0-b027-2fa37ad3168f"; + private const string SecondProjectSubnetId = "0c721846-5e2e-40a9-83d2-f1b75206ef84"; + + private const string FlatNetworkId = "98ff838a-a2c3-464d-8884-f348888ed804"; + + private const string CatletMetadataId = "15e2b061-c625-4469-9fe7-7c455058fcc0"; + private const string CatletId = "de8c6710-172a-44be-bbed-27ba9905ed8f"; + + private readonly Mock _taskMessingMock = new(); + private readonly Mock _networkProviderManagerMock = new(); + + + [Fact] - public async Task UpdateNetworks_SwitchFromOverlayToFlat_CreatesCorrectNetworkConfig() + public async Task UpdateNetworks_CatletIsAddedToOverlayNetwork_CreatesCorrectNetworkConfig() { + await ArrangeCatlet(DefaultProjectId); + + var command = new UpdateCatletNetworksCommand + { + + CatletId = Guid.Parse(CatletId), + ProjectId = Guid.Parse(DefaultProjectId), + Config = new CatletConfig + { + Networks = + [ + new CatletNetworkConfig + { + AdapterName = "eth0", + } + ] + }, + }; - // TODO Verify flat network creates a port - // TODO Verify that the floating port and IP assignment were removed + await WithScope(async (handler, stateStore) => + { + var result = await handler.UpdateNetworks(command); + + result.Should().BeRight().Which.Should().SatisfyRespectively( + settings => + { + settings.NetworkProviderName.Should().Be("default"); + settings.AdapterName.Should().Be("eth0"); + settings.PortName.Should().Be($"{CatletId}_eth0"); + }); + + await stateStore.SaveChangesAsync(); + }); + + await WithScope(async (_, stateStore) => + { + var assignmentCount = await stateStore.For().CountAsync(); + assignmentCount.Should().Be(2); + }); } + // TODO Verify flat network creates a port + // TODO Verify that the floating port and IP assignment were removed + [Fact] public async Task UpdateNetworks_SwitchFromFlatToOverlay_CreatesCorrectNetworkConfig() { @@ -31,4 +109,343 @@ public async Task UpdateNetworks_RemoveNetwork_CreatesCorrectNetworkConfig() // TODO test change of project // TODO test change of environment -} \ No newline at end of file + + private async Task WithScope(Func func) + { + await using var scope = CreateScope(); + var handler = scope.GetInstance(); + var stateStore = scope.GetInstance(); + await func(handler, stateStore); + } + + private async Task ArrangeCatlet(string projectId) + { + await WithScope(async (_, stateStore) => + { + await stateStore.For().AddAsync(new Catlet + { + Id = Guid.Parse(CatletId), + ProjectId = Guid.Parse(projectId), + MetadataId = Guid.Parse(CatletMetadataId), + Name = "test-catlet", + Environment = "default", + DataStore = "default", + }); + + await stateStore.SaveChangesAsync(); + }); + } + + protected override void AddSimpleInjector(SimpleInjectorAddOptions options) + { + options.Container.RegisterInstance(_taskMessingMock.Object); + options.Container.RegisterInstance(_networkProviderManagerMock.Object); + + // Use the proper managers instead of mocks as the code quite + // interdependent as it modifies the same EF Core entities. + options.Container.Register(Lifestyle.Scoped); + options.Container.Register(Lifestyle.Scoped); + options.Container.Register(Lifestyle.Scoped); + + options.Container.Register(Lifestyle.Scoped); + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + var networkProvidersConfig = new NetworkProvidersConfiguration + { + NetworkProviders = + [ + new NetworkProvider + { + Name = "default", + TypeString = "nat_overlay", + BridgeName = "br-nat", + Subnets = + [ + new NetworkProviderSubnet + { + Name = "default", + Network = "10.249.248.0/24", + Gateway = "10.249.248.1", + IpPools = + [ + new NetworkProviderIpPool + { + Name = "default", + FirstIp = "10.249.248.10", + NextIp = "10.249.248.12", + LastIp = "10.249.248.19" + }, + new NetworkProviderIpPool + { + Name = "second-provider-pool", + FirstIp = "10.249.248.20", + NextIp = "10.249.248.22", + LastIp = "10.249.248.29" + }, + ], + }, + new NetworkProviderSubnet + { + Name = "second-provider-subnet", + Network = "10.249.249.0/24", + Gateway = "10.249.249.1", + IpPools = + [ + new NetworkProviderIpPool + { + Name = "default", + FirstIp = "10.249.249.10", + NextIp = "10.249.249.12", + LastIp = "10.249.249.19" + }, + ], + }, + ], + }, + new NetworkProvider + { + Name = "flat-provider", + TypeString = "flat", + }, + ] + }; + + _networkProviderManagerMock + .Setup(m => m.GetCurrentConfiguration()) + .Returns(Prelude.RightAsync( + networkProvidersConfig)); + + await WithScope(async (_, stateStore) => + { + var configRealizer = new NetworkProvidersConfigRealizer(stateStore); + await configRealizer.RealizeConfigAsync(networkProvidersConfig, default); + }); + } + + protected override async Task SeedAsync(IStateStore stateStore) + { + await SeedDefaultTenantAndProject(); + + await stateStore.For().AddAsync(new CatletMetadata + { + Id = Guid.Parse(CatletMetadataId), + }); + + var projectB = new Project() + { + Id = Guid.Parse(SecondProjectId), + Name = "second-project", + TenantId = EryphConstants.DefaultTenantId, + }; + await stateStore.For().AddAsync(projectB); + + await stateStore.For().AddAsync( + new VirtualNetwork + { + Id = Guid.Parse(DefaultNetworkId), + ProjectId = EryphConstants.DefaultProjectId, + Name = EryphConstants.DefaultNetworkName, + Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = EryphConstants.DefaultProviderName, + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.Parse(DefaultSubnetId), + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.0.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.0.0.0/16", + FirstIp = "10.0.0.10", + NextIp = "10.0.0.12", + LastIp = "10.0.0.19", + }, + new IpPool() + { + Id = Guid.NewGuid(), + Name = "second-pool", + IpNetwork = "10.0.0.0/16", + FirstIp = "10.0.1.10", + NextIp = "10.0.1.12", + LastIp = "10.0.1.19", + } + ], + }, + new VirtualNetworkSubnet + { + Id = Guid.Parse(SecondSubnetId), + Name = "second-subnet", + IpNetwork = "10.1.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.1.0.0/16", + FirstIp = "10.1.0.10", + NextIp = "10.1.0.12", + LastIp = "10.1.0.19", + } + ], + }, + ], + NetworkPorts = + [ + new ProviderRouterPort + { + Name = "default", + ProviderName = EryphConstants.DefaultProviderName, + SubnetName = "second-provider-subnet", + PoolName = EryphConstants.DefaultIpPoolName, + MacAddress = "00:00:00:01:00:01", + } + ], + }); + + await stateStore.For().AddAsync( + new VirtualNetwork + { + Id = Guid.Parse(SecondNetworkId), + ProjectId = EryphConstants.DefaultProjectId, + Name = "second-network", + Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = EryphConstants.DefaultProviderName, + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.Parse(SecondNetworkSubnetId), + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.5.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.5.0.0/16", + FirstIp = "10.5.0.10", + NextIp = "10.5.0.12", + LastIp = "10.5.0.19", + } + ], + }, + ], + NetworkPorts = + [ + new ProviderRouterPort + { + Name = "default", + ProviderName = EryphConstants.DefaultProviderName, + SubnetName = "second-provider-subnet", + PoolName = EryphConstants.DefaultIpPoolName, + MacAddress = "00:00:00:01:00:02", + } + ] + }); + + await stateStore.For().AddAsync( + new VirtualNetwork + { + Id = Guid.Parse(SecondEnvironmentNetworkId), + ProjectId = EryphConstants.DefaultProjectId, + Name = EryphConstants.DefaultNetworkName, + Environment = "second-environment", + NetworkProvider = EryphConstants.DefaultProviderName, + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.Parse(SecondEnvironmentSubnetId), + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.10.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.10.0.0/16", + FirstIp = "10.10.0.10", + NextIp = "10.10.0.12", + LastIp = "10.10.0.19", + } + ], + }, + ], + NetworkPorts = + [ + new ProviderRouterPort + { + Name = "default", + ProviderName = EryphConstants.DefaultProviderName, + SubnetName = EryphConstants.DefaultSubnetName, + PoolName = EryphConstants.DefaultIpPoolName, + MacAddress = "00:00:00:01:00:03", + } + ] + }); + + await stateStore.For().AddAsync( + new VirtualNetwork + { + Id = Guid.Parse(SecondProjectNetworkId), + ProjectId = Guid.Parse(SecondProjectId), + Name = EryphConstants.DefaultNetworkName, + Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = EryphConstants.DefaultProviderName, + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.Parse(SecondProjectSubnetId), + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.100.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.100.0.0/16", + FirstIp = "10.100.0.10", + NextIp = "10.100.0.12", + LastIp = "10.100.0.19", + } + ], + }, + ], + NetworkPorts = + [ + new ProviderRouterPort + { + Name = "default", + ProviderName = EryphConstants.DefaultProviderName, + SubnetName = EryphConstants.DefaultSubnetName, + PoolName = EryphConstants.DefaultIpPoolName, + MacAddress = "00:00:00:01:00:04", + } + ] + }); + + await stateStore.For().AddAsync( + new VirtualNetwork + { + Id = Guid.Parse(FlatNetworkId), + ProjectId = Guid.Parse(DefaultProjectId), + Name = "flat-network", + Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = "flat-provider", + }); + } +} From 6880b88d2d2d6c3180978b6665e82133aa63e79f Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Thu, 14 Nov 2024 12:21:43 +0100 Subject: [PATCH 09/30] Cleanup MachineNetworkSettings and improve tests --- .../Machines/MachineNetworkSettings.cs | 17 ++- .../UpdateCatletNetworksCommandHandler.cs | 10 +- ...UpdateCatletNetworksCommandHandlerTests.cs | 144 +++++++++++++++--- 3 files changed, 145 insertions(+), 26 deletions(-) diff --git a/src/core/src/Eryph.VmConfig.Primitives/Resources/Machines/MachineNetworkSettings.cs b/src/core/src/Eryph.VmConfig.Primitives/Resources/Machines/MachineNetworkSettings.cs index 709003883..53e53644a 100644 --- a/src/core/src/Eryph.VmConfig.Primitives/Resources/Machines/MachineNetworkSettings.cs +++ b/src/core/src/Eryph.VmConfig.Primitives/Resources/Machines/MachineNetworkSettings.cs @@ -1,4 +1,6 @@ -namespace Eryph.Resources.Machines; +using System.Collections.Generic; + +namespace Eryph.Resources.Machines; public sealed class MachineNetworkSettings @@ -8,11 +10,16 @@ public sealed class MachineNetworkSettings public string NetworkName { get; set; } public string NetworkProviderName { get; set; } + public string PortName { get; set; } + public string MacAddress { get; set; } - public string AddressesV4 { get; set; } - public string AddressesV6 { get; set; } + + public IReadOnlyList AddressesV4 { get; set; } + + public IReadOnlyList AddressesV6 { get; set; } + public string FloatingAddressV4 { get; set; } - public string FloatingAddressV6 { get; set; } -} \ No newline at end of file + public string FloatingAddressV6 { get; set; } +} diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs index 537d74163..a4a8bad78 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs @@ -17,7 +17,6 @@ using Eryph.StateDb.Specifications; using JetBrains.Annotations; using LanguageExt; -using LanguageExt.ClassInstances; using LanguageExt.Common; using Rebus.Handlers; @@ -25,7 +24,6 @@ namespace Eryph.Modules.Controller.Networks; -#pragma warning restore 1998 [UsedImplicitly] public class UpdateCatletNetworksCommandHandler( ITaskMessaging messaging, @@ -141,10 +139,14 @@ from floatingIps in isFlatNetwork AdapterName = networkConfig.AdapterName, PortName = networkPort.Name, MacAddress = networkPort.MacAddress, - AddressesV4 = string.Join(',', ips.Where(x => x.AddressFamily == AddressFamily.InterNetwork)), + AddressesV4 = ips.Filter(x => x.AddressFamily == AddressFamily.InterNetwork) + .Map(ip => ip.ToString()) + .ToList(), FloatingAddressV4 = floatingIps.FirstOrDefault(x => x.AddressFamily == AddressFamily.InterNetwork) ?.ToString(), - AddressesV6 = string.Join(',', ips.Where(x => x.AddressFamily == AddressFamily.InterNetworkV6)), + AddressesV6 = ips.Filter(x => x.AddressFamily == AddressFamily.InterNetworkV6) + .Map(ip => ip.ToString()) + .ToList(), FloatingAddressV6 = floatingIps.FirstOrDefault(x => x.AddressFamily == AddressFamily.InterNetworkV6) ?.ToString(), }; diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs index 19c9e72bb..41d0d7306 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs @@ -32,6 +32,9 @@ public class UpdateCatletNetworksCommandHandlerTests : InMemoryStateDbTestBase private const string SecondNetworkId = "e480a020-57d0-4443-a973-57aa0c95872e"; private const string SecondNetworkSubnetId = "27ec11a4-5d6a-47da-9f9f-eb7486db38ea"; + private const string ThirdNetworkId = "9016fa5b-e0c7-4626-b1ba-6dc21902d04f"; + private const string ThirdNetworkSubnetId = "106fa5c1-8cf1-4ccd-915a-f9dc230cc299"; + private const string SecondEnvironmentNetworkId = "81a139e5-ab61-4fe3-b81f-59c11a665d22"; private const string SecondEnvironmentSubnetId = "dc807357-50e7-4263-8298-0c97ff69f4cf"; @@ -46,25 +49,72 @@ public class UpdateCatletNetworksCommandHandlerTests : InMemoryStateDbTestBase private readonly Mock _taskMessingMock = new(); private readonly Mock _networkProviderManagerMock = new(); - - - [Fact] - public async Task UpdateNetworks_CatletIsAddedToOverlayNetwork_CreatesCorrectNetworkConfig() + [Theory] + [InlineData(DefaultProjectId, "default", null, null, null, + DefaultNetworkId, "default", "default", "10.0.0.12", "10.249.248.12")] + [InlineData(DefaultProjectId, "default", "default", "default", "default", + DefaultNetworkId, "default", "default", "10.0.0.12", "10.249.248.12")] + [InlineData(DefaultProjectId, "default", "default", "default", "second-pool", + DefaultNetworkId, "default", "default", "10.0.1.12", "10.249.248.12")] + [InlineData(DefaultProjectId, "default", "default", "second-subnet", "default", + DefaultNetworkId, "default", "default", "10.1.0.12", "10.249.248.12")] + [InlineData(DefaultProjectId, "default", "second-network", "default", "default", + SecondNetworkId, "default", "second-provider-pool", "10.5.0.12", "10.249.248.22")] + [InlineData(DefaultProjectId, "default", "third-network", "default", "default", + ThirdNetworkId, "second-provider-subnet", "default", "10.6.0.12", "10.249.249.12")] + [InlineData(DefaultProjectId, "second-environment", "default", "default", "default", + SecondEnvironmentNetworkId, "default", "default", "10.10.0.12", "10.249.248.12")] + // When the environment does not have a dedicated network, we should fall back to the default network + [InlineData(DefaultProjectId, "environment-without-network", null, null, null, + DefaultNetworkId, "default", "default", "10.0.0.12", "10.249.248.12")] + [InlineData(DefaultProjectId, "environment-without-network", "default", "default", "default", + DefaultNetworkId, "default", "default", "10.0.0.12", "10.249.248.12")] + [InlineData(DefaultProjectId, "environment-without-network", "default", "default", "second-pool", + DefaultNetworkId, "default", "default", "10.0.1.12", "10.249.248.12")] + [InlineData(DefaultProjectId, "environment-without-network", "default", "second-subnet", "default", + DefaultNetworkId, "default", "default", "10.1.0.12", "10.249.248.12")] + [InlineData(DefaultProjectId, "environment-without-network", "second-network", "default", "default", + SecondNetworkId, "default", "second-provider-pool", "10.5.0.12", "10.249.248.22")] + [InlineData(DefaultProjectId, "environment-without-network", "third-network", "default", "default", + ThirdNetworkId, "second-provider-subnet", "default", "10.6.0.12", "10.249.249.12")] + [InlineData(SecondProjectId, "default", null, null, null, + SecondProjectNetworkId, "default", "default", "10.100.0.12", "10.249.248.12")] + [InlineData(SecondProjectId, "default", "default", "default", "default", + SecondProjectNetworkId, "default", "default", "10.100.0.12", "10.249.248.12")] + public async Task UpdateNetworks_CatletIsAddedToOverlayNetwork_CreatesCorrectNetworkConfig( + string projectId, + string environment, + string? networkName, + string? subnetName, + string? poolName, + string expectedNetworkId, + string expectedProviderSubnet, + string expectedProviderPool, + string expectedIp, + string expectedFloatingIp) { - await ArrangeCatlet(DefaultProjectId); + await ArrangeCatlet(projectId); var command = new UpdateCatletNetworksCommand { - CatletId = Guid.Parse(CatletId), - ProjectId = Guid.Parse(DefaultProjectId), + ProjectId = Guid.Parse(projectId), Config = new CatletConfig { + Environment = environment, Networks = [ new CatletNetworkConfig { AdapterName = "eth0", + Name = networkName, + SubnetV4 = subnetName is not null || poolName is not null + ? new CatletSubnetConfig + { + Name = subnetName, + IpPool = poolName, + } + : null, } ] }, @@ -80,6 +130,11 @@ await WithScope(async (handler, stateStore) => settings.NetworkProviderName.Should().Be("default"); settings.AdapterName.Should().Be("eth0"); settings.PortName.Should().Be($"{CatletId}_eth0"); + settings.NetworkName.Should().Be(networkName); + settings.AddressesV4.Should().Equal(expectedIp); + settings.FloatingAddressV4.Should().Be(expectedFloatingIp); + settings.AddressesV6.Should().BeEmpty(); + settings.FloatingAddressV6.Should().BeNull(); }); await stateStore.SaveChangesAsync(); @@ -87,8 +142,21 @@ await WithScope(async (handler, stateStore) => await WithScope(async (_, stateStore) => { - var assignmentCount = await stateStore.For().CountAsync(); - assignmentCount.Should().Be(2); + var catletPorts = await stateStore.For().ListAsync(); + var catletPort = catletPorts.Should().ContainSingle().Subject; + catletPort.CatletMetadataId.Should().Be(Guid.Parse(CatletMetadataId)); + catletPort.NetworkId.Should().Be(Guid.Parse(expectedNetworkId)); + + var floatingPorts = await stateStore.For().ListAsync(); + var floatingPort = floatingPorts.Should().ContainSingle().Subject; + floatingPort.ProviderName.Should().Be("default"); + floatingPort.SubnetName.Should().Be(expectedProviderSubnet); + floatingPort.PoolName.Should().Be(expectedProviderPool); + + var assignments = await stateStore.For().ListAsync(); + assignments.Should().Satisfy( + assignment => assignment.IpAddress == expectedIp, + assignment => assignment.IpAddress == expectedFloatingIp); }); } @@ -303,9 +371,9 @@ await stateStore.For().AddAsync( [ new ProviderRouterPort { - Name = "default", + Name = "provider", ProviderName = EryphConstants.DefaultProviderName, - SubnetName = "second-provider-subnet", + SubnetName = EryphConstants.DefaultSubnetName, PoolName = EryphConstants.DefaultIpPoolName, MacAddress = "00:00:00:01:00:01", } @@ -345,11 +413,53 @@ await stateStore.For().AddAsync( [ new ProviderRouterPort { - Name = "default", + Name = "provider", + ProviderName = EryphConstants.DefaultProviderName, + SubnetName = EryphConstants.DefaultSubnetName, + PoolName = "second-provider-pool", + MacAddress = "00:00:00:01:00:02", + } + ] + }); + + await stateStore.For().AddAsync( + new VirtualNetwork + { + Id = Guid.Parse(ThirdNetworkId), + ProjectId = EryphConstants.DefaultProjectId, + Name = "third-network", + Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = EryphConstants.DefaultProviderName, + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.Parse(ThirdNetworkSubnetId), + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.5.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.6.0.0/16", + FirstIp = "10.6.0.10", + NextIp = "10.6.0.12", + LastIp = "10.6.0.19", + } + ], + }, + ], + NetworkPorts = + [ + new ProviderRouterPort + { + Name = "provider", ProviderName = EryphConstants.DefaultProviderName, SubnetName = "second-provider-subnet", PoolName = EryphConstants.DefaultIpPoolName, - MacAddress = "00:00:00:01:00:02", + MacAddress = "00:00:00:01:00:03", } ] }); @@ -387,11 +497,11 @@ await stateStore.For().AddAsync( [ new ProviderRouterPort { - Name = "default", + Name = "provider", ProviderName = EryphConstants.DefaultProviderName, SubnetName = EryphConstants.DefaultSubnetName, PoolName = EryphConstants.DefaultIpPoolName, - MacAddress = "00:00:00:01:00:03", + MacAddress = "00:00:00:01:00:04", } ] }); @@ -429,11 +539,11 @@ await stateStore.For().AddAsync( [ new ProviderRouterPort { - Name = "default", + Name = "provider", ProviderName = EryphConstants.DefaultProviderName, SubnetName = EryphConstants.DefaultSubnetName, PoolName = EryphConstants.DefaultIpPoolName, - MacAddress = "00:00:00:01:00:04", + MacAddress = "00:00:00:01:00:05", } ] }); From 15cc265d2b2cd77d9fd7403c13657dad1cbd965b Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Thu, 14 Nov 2024 14:56:17 +0100 Subject: [PATCH 10/30] Cleanup MAC addresses in tests Improve update tests --- .../Commands/UpdateCatletNetworksCommand.cs | 7 +- .../Compute/UpdateCatletSaga.cs | 9 + .../UpdateCatletNetworksCommandHandler.cs | 4 +- .../NetworkProvidersChangeTrackingTests.cs | 12 +- .../VirtualNetworkChangeTrackingTests.cs | 14 +- .../Networks/CatletIpManagerTests.cs | 6 +- .../Networks/IpPoolManagerTests.cs | 2 +- .../Networks/NetworkConfigRealizerTests.cs | 6 +- .../Networks/NetworkConfigValidatorTests.cs | 8 +- .../Networks/ProviderIpManagerTests.cs | 6 +- ...UpdateCatletNetworksCommandHandlerTests.cs | 503 ++++++++++++++++-- .../StateDbDeleteTests.cs | 6 +- 12 files changed, 504 insertions(+), 79 deletions(-) diff --git a/src/core/src/Eryph.Messages/Resources/Catlets/Commands/UpdateCatletNetworksCommand.cs b/src/core/src/Eryph.Messages/Resources/Catlets/Commands/UpdateCatletNetworksCommand.cs index 8e981b54d..a8aa9d5d3 100644 --- a/src/core/src/Eryph.Messages/Resources/Catlets/Commands/UpdateCatletNetworksCommand.cs +++ b/src/core/src/Eryph.Messages/Resources/Catlets/Commands/UpdateCatletNetworksCommand.cs @@ -8,9 +8,12 @@ namespace Eryph.Messages.Resources.Catlets.Commands; public class UpdateCatletNetworksCommand: IHasResource, IHasProjectId { public Guid ProjectId { get; set; } + public CatletConfig Config { get; set; } + public Guid CatletId { get; set; } - public Resource Resource => new(ResourceType.Catlet, CatletId); + public Guid CatletMetadataId { get; set; } -} \ No newline at end of file + public Resource Resource => new(ResourceType.Catlet, CatletId); +} diff --git a/src/modules/src/Eryph.Modules.Controller/Compute/UpdateCatletSaga.cs b/src/modules/src/Eryph.Modules.Controller/Compute/UpdateCatletSaga.cs index a8c127c91..f5b390279 100644 --- a/src/modules/src/Eryph.Modules.Controller/Compute/UpdateCatletSaga.cs +++ b/src/modules/src/Eryph.Modules.Controller/Compute/UpdateCatletSaga.cs @@ -249,9 +249,18 @@ await bus.SendLocal(new UpdateGenesInventoryCommand private async Task StartUpdateCatlet() { Data.Data.State = UpdateVMState.GenesPrepared; + + var metadata = await GetCatletMetadata(Data.Data.CatletId); + if (metadata.IsNone) + { + await Fail($"The metadata for catlet {Data.Data.CatletId} was not found."); + return; + } + await StartNewTask(new UpdateCatletNetworksCommand { CatletId = Data.Data.CatletId, + CatletMetadataId = metadata.ValueUnsafe().Metadata.Id, Config = Data.Data.BredConfig, ProjectId = Data.Data.ProjectId }); diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs index a4a8bad78..013e84d57 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs @@ -47,12 +47,10 @@ await UpdateNetworks(message.Command) public EitherAsync> UpdateNetworks( UpdateCatletNetworksCommand command) => - from catletResult in stateStore.For().IO.GetByIdAsync(command.CatletId) - from catlet in catletResult.ToEitherAsync(Error.New($"Catlet {command.CatletId} not found.")) // TODO delete ports which are no longer configured from settings in command.Config.Networks .ToSeq() - .Map(cfg => UpdateNetwork(catlet.MetadataId, command, cfg)) + .Map(cfg => UpdateNetwork(command.CatletMetadataId, command, cfg)) .SequenceSerial() select settings; diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/NetworkProvidersChangeTrackingTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/NetworkProvidersChangeTrackingTests.cs index 4abcc5e04..fe6afb442 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/NetworkProvidersChangeTrackingTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/NetworkProvidersChangeTrackingTests.cs @@ -40,7 +40,7 @@ public abstract class NetworkProvidersChangeTrackingTests : ChangeTrackingTestBa ProviderName = "test-provider", SubnetName = "provider-test-subnet", PoolName = "provider-test-pool", - MacAddress = "00:00:00:00:00:01", + MacAddress = "42:00:42:00:00:01", IpAssignments = [ new IpAssignmentConfigModel() @@ -94,7 +94,7 @@ await WithHostScope(async stateStore => await stateStore.For().AddAsync(new FloatingNetworkPort() { Name = "new-floating-port", - MacAddress = "00:00:00:00:00:02", + MacAddress = "42:00:42:00:00:02", ProviderName = "test-provider", SubnetName = "provider-test-subnet", PoolName = "provider-test-pool", @@ -111,7 +111,7 @@ await stateStore.For().AddAsync(new FloatingNetworkPort() new FloatingNetworkPortConfigModel() { Name = "new-floating-port", - MacAddress = "00:00:00:00:00:02", + MacAddress = "42:00:42:00:00:02", ProviderName = "test-provider", SubnetName = "provider-test-subnet", PoolName = "provider-test-pool", @@ -127,14 +127,14 @@ public async Task FloatingPort_update_is_detected() await WithHostScope(async stateStore => { var floatingPort = await stateStore.For().GetByIdAsync(FloatingPortId); - floatingPort!.MacAddress = "00:00:00:00:00:02"; + floatingPort!.MacAddress = "42:00:42:00:00:02"; await stateStore.SaveChangesAsync(); }); _savedProvidersConfig.Should().BeEquivalentTo(GetProvidersConfig()); var portsConfig = await ReadPortsConfig(); - _expectedPortsConfig.FloatingPorts[0].MacAddress = "00:00:00:00:00:02"; + _expectedPortsConfig.FloatingPorts[0].MacAddress = "42:00:42:00:00:02"; portsConfig.Should().BeEquivalentTo(_expectedPortsConfig); } @@ -247,7 +247,7 @@ await stateStore.For().AddAsync(new FloatingNetworkPort() ProviderName = "test-provider", SubnetName = "provider-test-subnet", PoolName = "provider-test-pool", - MacAddress = "00:00:00:00:00:01", + MacAddress = "42:00:42:00:00:01", IpAssignments = [ new IpPoolAssignment() diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs index ae0cd1c38..774f3eaad 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs @@ -80,7 +80,7 @@ public abstract class VirtualNetworkChangeTrackingTests(IDatabaseFixture databas Name = "test-catlet-port", VirtualNetworkName = "virtual-test-network", EnvironmentName = "test-environment", - MacAddress = "00:00:00:00:00:01", + MacAddress = "42:00:42:00:00:01", FloatingNetworkPort = new() { Name = "test-floating-port", @@ -108,7 +108,7 @@ await WithHostScope(async stateStore => await stateStore.For().AddAsync(new CatletNetworkPort() { Name = "new-catlet-port", - MacAddress = "00:00:00:00:00:02", + MacAddress = "42:00:42:00:00:02", CatletMetadataId = CatletMetadataId, NetworkId = VirtualNetworkId, }); @@ -126,7 +126,7 @@ await stateStore.For().AddAsync(new CatletNetworkPort() { CatletMetadataId = CatletMetadataId, Name = "new-catlet-port", - MacAddress = "00:00:00:00:00:02", + MacAddress = "42:00:42:00:00:02", VirtualNetworkName = "virtual-test-network", EnvironmentName = "test-environment", FloatingNetworkPort = null, @@ -143,7 +143,7 @@ await WithHostScope(async stateStore => { var catletPort = await stateStore.For().GetByIdAsync(CatletPortId); catletPort!.AddressName = "test"; - catletPort!.MacAddress = "00:00:00:00:00:02"; + catletPort!.MacAddress = "42:00:42:00:00:02"; await stateStore.SaveChangesAsync(); }); @@ -152,7 +152,7 @@ await WithHostScope(async stateStore => networksConfig.Should().BeEquivalentTo(_expectedNetworksConfig); var portsConfig = await ReadPortsConfig(); _expectedPortsConfig.CatletNetworkPorts[0].AddressName = "test"; - _expectedPortsConfig.CatletNetworkPorts[0].MacAddress = "00:00:00:00:00:02"; + _expectedPortsConfig.CatletNetworkPorts[0].MacAddress = "42:00:42:00:00:02"; portsConfig.Should().BeEquivalentTo(_expectedPortsConfig); } @@ -498,7 +498,7 @@ await stateStore.For().AddAsync(new FloatingNetworkPort() { Id = FloatingPortId, Name = "test-floating-port", - MacAddress = "00:00:00:00:00:10", + MacAddress = "42:00:42:00:00:10", ProviderName = "test-provider", SubnetName = "provider-test-subnet", PoolName = "provider-test-pool", @@ -511,7 +511,7 @@ await stateStore.For().AddAsync(new CatletNetworkPort() CatletMetadataId = CatletMetadataId, NetworkId = VirtualNetworkId, FloatingPortId = FloatingPortId, - MacAddress = "00:00:00:00:00:01", + MacAddress = "42:00:42:00:00:01", IpAssignments = [ new IpPoolAssignment() diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs index fc1859270..75b532880 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs @@ -76,7 +76,7 @@ await WithScope(async (ipManager, _, stateStore) => { Id = CatletPortId, Name = "test-catlet-port", - MacAddress = "00:00:00:00:00:01", + MacAddress = "42:00:42:00:00:01", Network = network!, CatletMetadataId = Guid.Parse(CatletMetadataId), }; @@ -141,7 +141,7 @@ await WithScope(async (_, ipPoolManager, stateStore) => { Id = CatletPortId, Name = "test-catlet-port", - MacAddress = "00:00:00:00:00:01", + MacAddress = "42:00:42:00:00:01", NetworkId = Guid.Parse(networkId), CatletMetadataId = Guid.Parse(CatletMetadataId), IpAssignments = [ipAssignment], @@ -228,7 +228,7 @@ await WithScope(async (_, ipPoolManager, stateStore) => { Id = CatletPortId, Name = "test-catlet-port", - MacAddress = "00:00:00:00:00:01", + MacAddress = "42:00:42:00:00:01", NetworkId = Guid.Parse(DefaultNetworkId), CatletMetadataId = Guid.Parse(CatletMetadataId), IpAssignments = [ipAssignment], diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs index 2170f5687..309ec76cb 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs @@ -235,7 +235,7 @@ await stateStore.For().AddAsync( { Id = NetworkPortId, Name = "test-catlet-port", - MacAddress = "00:00:00:00:00:01", + MacAddress = "42:00:42:00:00:02", CatletMetadataId = CatletMetadataId, NetworkId = NetworkId, }); diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs index e47ffbcbc..5d63fe537 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs @@ -116,7 +116,7 @@ public async Task Existing_ip_pool_is_updated() var routerPort = new NetworkRouterPort() { Name = "default", - MacAddress = "00:00:00:00:10:10", + MacAddress = "42:00:42:00:10:10", IpAssignments = new List { new IpPoolAssignment @@ -153,7 +153,7 @@ public async Task Existing_ip_pool_is_updated() new ProviderRouterPort() { Name = "test-provider-port", - MacAddress = "00:00:00:00:00:10", + MacAddress = "42:00:42:00:00:10", SubnetName = "test-provider-subnet", PoolName = "test-provider-pool", }, @@ -245,7 +245,7 @@ public async Task Cleanup_of_overlay_when_switched_to_flat() var routerPort = new NetworkRouterPort() { Name = "default", - MacAddress = "00:00:00:00:00:10", + MacAddress = "42:00:42:00:00:10", IpAssignments = new List { new IpPoolAssignment diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigValidatorTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigValidatorTests.cs index 57bf61f61..5c76dd94b 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigValidatorTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigValidatorTests.cs @@ -566,13 +566,13 @@ await networkRepo.AddAsync( new CatletNetworkPort() { Name = "test-catlet-port", - MacAddress = "00:00:00:00:00:10", + MacAddress = "42:00:42:00:00:1ß", CatletMetadataId = firstCatletMetadata.Id, }, new ProviderRouterPort() { Name = "provider", - MacAddress = "00:00:00:00:00:01", + MacAddress = "42:00:42:00:00:01", ProviderName = "default", PoolName = "default", SubnetName = "default" @@ -634,14 +634,14 @@ await networkRepo.AddAsync( new CatletNetworkPort() { Id = catletNetworkPortId, - MacAddress = "00:00:00:00:10:10", + MacAddress = "42:00:42:00:10:02", Name = "test-catlet-port", CatletMetadataId = secondCatletMetadata.Id, }, new ProviderRouterPort() { Name = "provider", - MacAddress = "00:00:00:00:10:01", + MacAddress = "42:00:42:00:10:01", ProviderName = "default", PoolName = "default", SubnetName = "default" diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs index 63ee6b084..9567e8a85 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs @@ -38,7 +38,7 @@ await WithScope(async (providerIpManager, _, stateStore) => { Id = FloatingPortId, Name = "test-floating-port", - MacAddress = "00:00:00:00:00:01", + MacAddress = "42:00:42:00:00:01", ProviderName = providerName, SubnetName = subnetName, PoolName = poolName, @@ -96,7 +96,7 @@ await WithScope(async (_, ipPoolManager, stateStore) => { Id = FloatingPortId, Name = "test-floating-port", - MacAddress = "00:00:00:00:00:01", + MacAddress = "42:00:42:00:00:01", ProviderName = providerName, SubnetName = subnetName, PoolName = poolName, @@ -158,7 +158,7 @@ await WithScope(async (_, ipPoolManager, stateStore) => { Id = FloatingPortId, Name = "test-floating-port", - MacAddress = "00:00:00:00:00:01", + MacAddress = "42:00:42:00:00:01", ProviderName = "default", SubnetName = "default", PoolName = "default", diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs index 41d0d7306..e3c2d9173 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs @@ -65,8 +65,6 @@ public class UpdateCatletNetworksCommandHandlerTests : InMemoryStateDbTestBase [InlineData(DefaultProjectId, "second-environment", "default", "default", "default", SecondEnvironmentNetworkId, "default", "default", "10.10.0.12", "10.249.248.12")] // When the environment does not have a dedicated network, we should fall back to the default network - [InlineData(DefaultProjectId, "environment-without-network", null, null, null, - DefaultNetworkId, "default", "default", "10.0.0.12", "10.249.248.12")] [InlineData(DefaultProjectId, "environment-without-network", "default", "default", "default", DefaultNetworkId, "default", "default", "10.0.0.12", "10.249.248.12")] [InlineData(DefaultProjectId, "environment-without-network", "default", "default", "second-pool", @@ -77,8 +75,6 @@ public class UpdateCatletNetworksCommandHandlerTests : InMemoryStateDbTestBase SecondNetworkId, "default", "second-provider-pool", "10.5.0.12", "10.249.248.22")] [InlineData(DefaultProjectId, "environment-without-network", "third-network", "default", "default", ThirdNetworkId, "second-provider-subnet", "default", "10.6.0.12", "10.249.249.12")] - [InlineData(SecondProjectId, "default", null, null, null, - SecondProjectNetworkId, "default", "default", "10.100.0.12", "10.249.248.12")] [InlineData(SecondProjectId, "default", "default", "default", "default", SecondProjectNetworkId, "default", "default", "10.100.0.12", "10.249.248.12")] public async Task UpdateNetworks_CatletIsAddedToOverlayNetwork_CreatesCorrectNetworkConfig( @@ -93,11 +89,10 @@ public async Task UpdateNetworks_CatletIsAddedToOverlayNetwork_CreatesCorrectNet string expectedIp, string expectedFloatingIp) { - await ArrangeCatlet(projectId); - var command = new UpdateCatletNetworksCommand { CatletId = Guid.Parse(CatletId), + CatletMetadataId = Guid.Parse(CatletMetadataId), ProjectId = Guid.Parse(projectId), Config = new CatletConfig { @@ -120,10 +115,372 @@ public async Task UpdateNetworks_CatletIsAddedToOverlayNetwork_CreatesCorrectNet }, }; + await WithScope(async (handler, stateStore) => + { + var result = await handler.UpdateNetworks(command); + result.Should().BeRight().Which.Should().SatisfyRespectively( + settings => + { + settings.NetworkProviderName.Should().Be("default"); + settings.AdapterName.Should().Be("eth0"); + settings.PortName.Should().Be($"{CatletId}_eth0"); + settings.NetworkName.Should().Be(networkName); + settings.AddressesV4.Should().Equal(expectedIp); + settings.FloatingAddressV4.Should().Be(expectedFloatingIp); + settings.AddressesV6.Should().BeEmpty(); + settings.FloatingAddressV6.Should().BeNull(); + }); + + await stateStore.SaveChangesAsync(); + }); + + await ShouldBeOverlayNetworkInDatabase( + expectedNetworkId, + expectedProviderSubnet, expectedProviderPool, + expectedIp, expectedFloatingIp); + } + + [Fact] + public async Task UpdateNetworks_CatletIsAddedToFlatNetwork_CreatesCorrectNetworkConfig() + { + var command = new UpdateCatletNetworksCommand + { + CatletId = Guid.Parse(CatletId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + ProjectId = Guid.Parse(DefaultProjectId), + Config = new CatletConfig + { + Environment = "default", + Networks = + [ + new CatletNetworkConfig + { + AdapterName = "eth0", + Name = "flat-network", + } + ] + }, + }; + await WithScope(async (handler, stateStore) => { var result = await handler.UpdateNetworks(command); + result.Should().BeRight().Which.Should().SatisfyRespectively( + settings => + { + settings.NetworkProviderName.Should().Be("flat-provider"); + settings.AdapterName.Should().Be("eth0"); + settings.PortName.Should().Be($"{CatletId}_eth0"); + settings.NetworkName.Should().Be("flat-network"); + settings.AddressesV4.Should().BeEmpty(); + settings.FloatingAddressV4.Should().BeNull(); + settings.AddressesV6.Should().BeEmpty(); + settings.FloatingAddressV6.Should().BeNull(); + }); + + await stateStore.SaveChangesAsync(); + }); + + await ShouldBeFlatNetworkInDatabase(); + } + + [Fact] + public async Task UpdateNetworks_CatletHasFixedMacAddress_FixedMacAddressIsUsed() + { + var command = new UpdateCatletNetworksCommand + { + CatletId = Guid.Parse(CatletId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + ProjectId = Guid.Parse(DefaultProjectId), + Config = new CatletConfig + { + NetworkAdapters = + [ + new CatletNetworkAdapterConfig + { + Name = "eth0", + MacAddress = "420042004202", + } + ], + Networks = + [ + new CatletNetworkConfig + { + AdapterName = "eth0", + Name = "flat-network", + } + ], + }, + }; + + await WithScope(async (handler, stateStore) => + { + var result = await handler.UpdateNetworks(command); + + result.Should().BeRight().Which.Should().SatisfyRespectively( + settings => + { + settings.AdapterName.Should().Be("eth0"); + settings.PortName.Should().Be($"{CatletId}_eth0"); + settings.MacAddress.Should().Be("42:00:42:00:42:02"); + }); + + await stateStore.SaveChangesAsync(); + }); + + await WithScope(async (_, stateStore) => + { + var catletPorts = await stateStore.For().ListAsync(); + var catletPort = catletPorts.Should().ContainSingle().Subject; + catletPort.CatletMetadataId.Should().Be(Guid.Parse(CatletMetadataId)); + catletPort.MacAddress.Should().Be("42:00:42:00:42:02"); + }); + } + + [Fact] + public async Task UpdateNetworks_MoveCatletFromFlatNetworkToOverlayNetwork_CreatesCorrectNetworkConfig() + { + var command = new UpdateCatletNetworksCommand + { + CatletId = Guid.Parse(CatletId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + ProjectId = Guid.Parse(DefaultProjectId), + Config = new CatletConfig + { + Environment = "default", + Networks = + [ + new CatletNetworkConfig + { + AdapterName = "eth0", + Name = "flat-network", + } + ] + }, + }; + + await WithScope(async (handler, stateStore) => + { + var result = await handler.UpdateNetworks(command); + result.Should().BeRight(); + await stateStore.SaveChangesAsync(); + }); + + await ShouldBeFlatNetworkInDatabase(); + + var updatedConfigCommand = new UpdateCatletNetworksCommand + { + CatletId = Guid.Parse(CatletId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + ProjectId = Guid.Parse(DefaultProjectId), + Config = new CatletConfig + { + Environment = "default", + Networks = + [ + new CatletNetworkConfig + { + AdapterName = "eth0", + Name = "default", + SubnetV4 = new CatletSubnetConfig + { + Name = "default", + IpPool = "default", + }, + } + ] + }, + }; + + await WithScope(async (handler, stateStore) => + { + var result = await handler.UpdateNetworks(updatedConfigCommand); + result.Should().BeRight().Which.Should().SatisfyRespectively( + settings => + { + settings.NetworkProviderName.Should().Be("flat-provider"); + settings.AdapterName.Should().Be("eth0"); + settings.PortName.Should().Be($"{CatletId}_eth0"); + settings.NetworkName.Should().Be("default"); + settings.AddressesV4.Should().Equal("10.0.0.12"); + settings.FloatingAddressV4.Should().Be("10.249.248.12"); + settings.AddressesV6.Should().BeEmpty(); + settings.FloatingAddressV6.Should().BeNull(); + }); + + await stateStore.SaveChangesAsync(); + }); + + await ShouldBeOverlayNetworkInDatabase( + DefaultNetworkId, + "default", "default", + "10.0.0.12", "10.249.248.12"); + } + + [Fact] + public async Task UpdateNetworks_MoveCatletFromOverlayNetworkToFlatNetwork_CreatesCorrectNetworkConfig() + { + var command = new UpdateCatletNetworksCommand + { + CatletId = Guid.Parse(CatletId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + ProjectId = Guid.Parse(DefaultProjectId), + Config = new CatletConfig + { + Networks = + [ + new CatletNetworkConfig + { + AdapterName = "eth0", + } + ] + }, + }; + + await WithScope(async (handler, stateStore) => + { + var result = await handler.UpdateNetworks(command); + result.Should().BeRight(); + await stateStore.SaveChangesAsync(); + }); + + await ShouldBeOverlayNetworkInDatabase( + DefaultNetworkId, + "default", "default", + "10.0.0.12", "10.249.248.12"); + + var updatedConfigCommand = new UpdateCatletNetworksCommand + { + CatletId = Guid.Parse(CatletId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + ProjectId = Guid.Parse(DefaultProjectId), + Config = new CatletConfig + { + Environment = "default", + Networks = + [ + new CatletNetworkConfig + { + AdapterName = "eth0", + Name = "flat-network", + } + ] + }, + }; + + await WithScope(async (handler, stateStore) => + { + var result = await handler.UpdateNetworks(updatedConfigCommand); + result.Should().BeRight().Which.Should().SatisfyRespectively( + settings => + { + settings.NetworkProviderName.Should().Be("flat-provider"); + settings.AdapterName.Should().Be("eth0"); + settings.PortName.Should().Be($"{CatletId}_eth0"); + settings.NetworkName.Should().Be("flat-network"); + settings.AddressesV4.Should().BeEmpty(); + settings.FloatingAddressV4.Should().BeNull(); + settings.AddressesV6.Should().BeEmpty(); + settings.FloatingAddressV6.Should().BeNull(); + }); + + await stateStore.SaveChangesAsync(); + }); + + await ShouldBeFlatNetworkInDatabase(); + } + + [Theory] + [InlineData(DefaultProjectId, "default", "default", "default", "second-pool", + DefaultNetworkId, "default", "default", "10.0.1.12", "10.249.248.12")] + [InlineData(DefaultProjectId, "default", "default", "second-subnet", "default", + DefaultNetworkId, "default", "default", "10.1.0.12", "10.249.248.12")] + [InlineData(DefaultProjectId, "default", "second-network", "default", "default", + SecondNetworkId, "default", "second-provider-pool", "10.5.0.12", "10.249.248.22")] + [InlineData(DefaultProjectId, "default", "third-network", "default", "default", + ThirdNetworkId, "second-provider-subnet", "default", "10.6.0.12", "10.249.249.12")] + [InlineData(DefaultProjectId, "second-environment", "default", "default", "default", + SecondEnvironmentNetworkId, "default", "default", "10.10.0.12", "10.249.248.12")] + // When the environment does not have a dedicated network, we should fall back to the default network + [InlineData(DefaultProjectId, "environment-without-network", "default", "default", "second-pool", + DefaultNetworkId, "default", "default", "10.0.1.12", "10.249.248.12")] + [InlineData(DefaultProjectId, "environment-without-network", "default", "second-subnet", "default", + DefaultNetworkId, "default", "default", "10.1.0.12", "10.249.248.12")] + [InlineData(DefaultProjectId, "environment-without-network", "second-network", "default", "default", + SecondNetworkId, "default", "second-provider-pool", "10.5.0.12", "10.249.248.22")] + [InlineData(DefaultProjectId, "environment-without-network", "third-network", "default", "default", + ThirdNetworkId, "second-provider-subnet", "default", "10.6.0.12", "10.249.249.12")] + [InlineData(SecondProjectId, "default", "default", "default", "default", + SecondProjectNetworkId, "default", "default", "10.100.0.12", "10.249.248.12")] + public async Task UpdateNetworks_CatletIsMovedBetweenOverlayNetworks_CreatesCorrectNetworkConfig( + string projectId, + string environment, + string? networkName, + string? subnetName, + string? poolName, + string expectedNetworkId, + string expectedProviderSubnet, + string expectedProviderPool, + string expectedIp, + string expectedFloatingIp) + { + var command = new UpdateCatletNetworksCommand + { + CatletId = Guid.Parse(CatletId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + ProjectId = Guid.Parse(DefaultProjectId), + Config = new CatletConfig + { + Networks = + [ + new CatletNetworkConfig + { + AdapterName = "eth0", + } + ] + }, + }; + + await WithScope(async (handler, stateStore) => + { + var result = await handler.UpdateNetworks(command); + result.Should().BeRight(); + await stateStore.SaveChangesAsync(); + }); + + await ShouldBeOverlayNetworkInDatabase( + DefaultNetworkId, + "default", "default", + "10.0.0.12", "10.249.248.12"); + + var updatedConfigCommand = new UpdateCatletNetworksCommand + { + CatletId = Guid.Parse(CatletId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + ProjectId = Guid.Parse(projectId), + Config = new CatletConfig + { + Environment = environment, + Networks = + [ + new CatletNetworkConfig + { + AdapterName = "eth0", + Name = networkName, + SubnetV4 = new CatletSubnetConfig + { + Name = subnetName, + IpPool = poolName, + } + } + ] + }, + }; + + await WithScope(async (handler, stateStore) => + { + var result = await handler.UpdateNetworks(updatedConfigCommand); result.Should().BeRight().Which.Should().SatisfyRespectively( settings => { @@ -140,6 +497,83 @@ await WithScope(async (handler, stateStore) => await stateStore.SaveChangesAsync(); }); + await ShouldBeOverlayNetworkInDatabase( + expectedNetworkId, + expectedProviderSubnet, expectedProviderPool, + expectedIp, expectedFloatingIp); + } + + [Fact] + public async Task UpdateNetworks_NetworkIsRemovedFromCatlet_PortsAreDeleted() + { + var command = new UpdateCatletNetworksCommand + { + CatletId = Guid.Parse(CatletId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + ProjectId = Guid.Parse(DefaultProjectId), + Config = new CatletConfig + { + Networks = + [ + new CatletNetworkConfig + { + AdapterName = "eth0", + } + ] + }, + }; + + await WithScope(async (handler, stateStore) => + { + var result = await handler.UpdateNetworks(command); + result.Should().BeRight(); + await stateStore.SaveChangesAsync(); + }); + + await ShouldBeOverlayNetworkInDatabase( + DefaultNetworkId, + "default", "default", + "10.0.0.12", "10.249.248.12"); + + var updatedConfigCommand = new UpdateCatletNetworksCommand + { + CatletId = Guid.Parse(CatletId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + ProjectId = Guid.Parse(DefaultProjectId), + Config = new CatletConfig + { + Networks = [], + }, + }; + + await WithScope(async (handler, stateStore) => + { + var result = await handler.UpdateNetworks(updatedConfigCommand); + result.Should().BeRight().Which.Should().BeEmpty(); + + await stateStore.SaveChangesAsync(); + }); + + await WithScope(async (_, stateStore) => + { + var catletPorts = await stateStore.For().ListAsync(); + catletPorts.Should().BeEmpty(); + + var floatingPorts = await stateStore.For().ListAsync(); + floatingPorts.Should().BeEmpty(); + + var assignments = await stateStore.For().ListAsync(); + assignments.Should().BeEmpty(); + }); + } + + private async Task ShouldBeOverlayNetworkInDatabase( + string expectedNetworkId, + string expectedProviderSubnet, + string expectedProviderPool, + string expectedIp, + string expectedFloatingIp) + { await WithScope(async (_, stateStore) => { var catletPorts = await stateStore.For().ListAsync(); @@ -152,7 +586,7 @@ await WithScope(async (_, stateStore) => floatingPort.ProviderName.Should().Be("default"); floatingPort.SubnetName.Should().Be(expectedProviderSubnet); floatingPort.PoolName.Should().Be(expectedProviderPool); - + var assignments = await stateStore.For().ListAsync(); assignments.Should().Satisfy( assignment => assignment.IpAddress == expectedIp, @@ -160,23 +594,22 @@ await WithScope(async (_, stateStore) => }); } - // TODO Verify flat network creates a port - // TODO Verify that the floating port and IP assignment were removed - - [Fact] - public async Task UpdateNetworks_SwitchFromFlatToOverlay_CreatesCorrectNetworkConfig() + private async Task ShouldBeFlatNetworkInDatabase() { - // TODO Verify that the floating port and IP assignment are created - } + await WithScope(async (_, stateStore) => + { + var catletPorts = await stateStore.For().ListAsync(); + var catletPort = catletPorts.Should().ContainSingle().Subject; + catletPort.CatletMetadataId.Should().Be(Guid.Parse(CatletMetadataId)); + catletPort.NetworkId.Should().Be(Guid.Parse(FlatNetworkId)); - [Fact] - public async Task UpdateNetworks_RemoveNetwork_CreatesCorrectNetworkConfig() - { - // TODO Verify that the port and assignment were deleted - } + var floatingPorts = await stateStore.For().ListAsync(); + floatingPorts.Should().BeEmpty(); - // TODO test change of project - // TODO test change of environment + var assignments = await stateStore.For().ListAsync(); + assignments.Should().BeEmpty(); + }); + } private async Task WithScope(Func func) { @@ -186,24 +619,6 @@ private async Task WithScope(Func - { - await stateStore.For().AddAsync(new Catlet - { - Id = Guid.Parse(CatletId), - ProjectId = Guid.Parse(projectId), - MetadataId = Guid.Parse(CatletMetadataId), - Name = "test-catlet", - Environment = "default", - DataStore = "default", - }); - - await stateStore.SaveChangesAsync(); - }); - } - protected override void AddSimpleInjector(SimpleInjectorAddOptions options) { options.Container.RegisterInstance(_taskMessingMock.Object); @@ -375,7 +790,7 @@ await stateStore.For().AddAsync( ProviderName = EryphConstants.DefaultProviderName, SubnetName = EryphConstants.DefaultSubnetName, PoolName = EryphConstants.DefaultIpPoolName, - MacAddress = "00:00:00:01:00:01", + MacAddress = "42:00:42:00:00:01", } ], }); @@ -417,7 +832,7 @@ await stateStore.For().AddAsync( ProviderName = EryphConstants.DefaultProviderName, SubnetName = EryphConstants.DefaultSubnetName, PoolName = "second-provider-pool", - MacAddress = "00:00:00:01:00:02", + MacAddress = "42:00:42:00:00:02", } ] }); @@ -459,7 +874,7 @@ await stateStore.For().AddAsync( ProviderName = EryphConstants.DefaultProviderName, SubnetName = "second-provider-subnet", PoolName = EryphConstants.DefaultIpPoolName, - MacAddress = "00:00:00:01:00:03", + MacAddress = "42:00:42:00:00:03", } ] }); @@ -501,7 +916,7 @@ await stateStore.For().AddAsync( ProviderName = EryphConstants.DefaultProviderName, SubnetName = EryphConstants.DefaultSubnetName, PoolName = EryphConstants.DefaultIpPoolName, - MacAddress = "00:00:00:01:00:04", + MacAddress = "42:00:42:00:00:04", } ] }); @@ -543,7 +958,7 @@ await stateStore.For().AddAsync( ProviderName = EryphConstants.DefaultProviderName, SubnetName = EryphConstants.DefaultSubnetName, PoolName = EryphConstants.DefaultIpPoolName, - MacAddress = "00:00:00:01:00:05", + MacAddress = "42:00:42:00:00:05", } ] }); diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs index ccf86b8b8..3af047c57 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs @@ -85,7 +85,7 @@ await stateStore.For().AddAsync(new FloatingNetworkPort() { Id = FloatingPortId, Name = "test-floating-port", - MacAddress = "00:00:00:00:00:10", + MacAddress = "42:00:42:00:00:10", ProviderName = "test-provider", SubnetName = "provider-test-subnet", PoolName = "provider-test-pool", @@ -98,7 +98,7 @@ await stateStore.For().AddAsync(new CatletNetworkPort() CatletMetadataId = CatletMetadataId, NetworkId = VirtualNetworkId, FloatingPortId = FloatingPortId, - MacAddress = "00:00:00:00:00:01", + MacAddress = "42:00:42:00:00:01", IpAssignments = [ new IpPoolAssignment() @@ -159,7 +159,7 @@ await stateStore.For().AddAsync(new FloatingNetworkPort() ProviderName = "test-provider", SubnetName = "provider-test-subnet", PoolName = "provider-test-pool", - MacAddress = "00:00:00:00:00:01", + MacAddress = "42:00:42:00:00:01", IpAssignments = [ new IpPoolAssignment() From 4f0d530901387467363098a2ab0647e2bac3cd9d Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Thu, 14 Nov 2024 17:01:44 +0100 Subject: [PATCH 11/30] Fix issues and cleanup --- .../Specifications/CatletNetworkPortSpecs.cs | 21 ++ .../Specifications/NetworkPortSpecs.cs | 13 +- .../Specifications/ProviderRouterPortSpecs.cs | 2 +- .../Networks/CatletIpManager.cs | 20 +- .../Networks/ICatletIpManager.cs | 5 +- .../Networks/IProviderIpManager.cs | 5 +- .../Networks/ProviderIpManager.cs | 19 +- .../UpdateCatletNetworksCommandHandler.cs | 232 +++++++++--------- .../Networks/CatletIpManagerTests.cs | 9 +- .../Networks/ProviderIpManagerTests.cs | 8 +- ...UpdateCatletNetworksCommandHandlerTests.cs | 40 ++- 11 files changed, 201 insertions(+), 173 deletions(-) diff --git a/src/data/src/Eryph.StateDb/Specifications/CatletNetworkPortSpecs.cs b/src/data/src/Eryph.StateDb/Specifications/CatletNetworkPortSpecs.cs index 7f0443a8d..a67ac143b 100644 --- a/src/data/src/Eryph.StateDb/Specifications/CatletNetworkPortSpecs.cs +++ b/src/data/src/Eryph.StateDb/Specifications/CatletNetworkPortSpecs.cs @@ -6,6 +6,7 @@ using Ardalis.Specification; using Eryph.StateDb.Model; using System; +using LanguageExt; namespace Eryph.StateDb.Specifications; @@ -40,4 +41,24 @@ public GetByName(Guid networkId, string name) Query.Where(p => p.Network.Id == networkId && p.Name == name); } } + + public sealed class GetUnused : Specification + { + public GetUnused(Guid catletMetadataId, Seq usedPortNames) + { + var values = usedPortNames.ToArray(); + Query.Where(p => p.CatletMetadataId == catletMetadataId + && !values.Contains(p.Name)) + .Include(p => p.FloatingPort!); + } + } + + public sealed class GetByCatletMetadataIdAndName : Specification, ISingleResultSpecification + { + public GetByCatletMetadataIdAndName(Guid catletMetadataId, string name) + { + Query.Where(p => p.CatletMetadataId == catletMetadataId && p.Name == name) + .Include(p => p.FloatingPort!); + } + } } diff --git a/src/data/src/Eryph.StateDb/Specifications/NetworkPortSpecs.cs b/src/data/src/Eryph.StateDb/Specifications/NetworkPortSpecs.cs index 570894649..13f59b512 100644 --- a/src/data/src/Eryph.StateDb/Specifications/NetworkPortSpecs.cs +++ b/src/data/src/Eryph.StateDb/Specifications/NetworkPortSpecs.cs @@ -23,15 +23,4 @@ public GetByNetworkAndName(Guid networkId, string name) } } - - // TODO fix my naming - public sealed class GetByNetworkAndNameForCatlet : Specification, ISingleResultSpecification - { - public GetByNetworkAndNameForCatlet(Guid networkId, string name) - { - Query.Where(x => x.NetworkId == networkId && x.Name == name); - } - - } - -} \ No newline at end of file +} diff --git a/src/data/src/Eryph.StateDb/Specifications/ProviderRouterPortSpecs.cs b/src/data/src/Eryph.StateDb/Specifications/ProviderRouterPortSpecs.cs index 0f2790074..bcd5a20e5 100644 --- a/src/data/src/Eryph.StateDb/Specifications/ProviderRouterPortSpecs.cs +++ b/src/data/src/Eryph.StateDb/Specifications/ProviderRouterPortSpecs.cs @@ -17,4 +17,4 @@ public GetByNetworkId(Guid networkId) Query.Where(x => x.NetworkId == networkId); } } -} \ No newline at end of file +} diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs index 3daea0634..8af2823a0 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs @@ -21,19 +21,17 @@ public class CatletIpManager( IIpPoolManager poolManager) : BaseIpManager(stateStore, poolManager), ICatletIpManager { - public EitherAsync ConfigurePortIps( + public EitherAsync> ConfigurePortIps( VirtualNetwork network, CatletNetworkPort port, - CatletNetworkConfig networkConfig, - CancellationToken cancellationToken) => + CatletNetworkConfig networkConfig) => from _ in RightAsync(unit) let subnetName = Optional(networkConfig.SubnetV4?.Name) .IfNone(EryphConstants.DefaultSubnetName) let ipPoolName = Optional(networkConfig.SubnetV4?.IpPool) .IfNone(EryphConstants.DefaultIpPoolName) from ipAssignments in _stateStore.For().IO.ListAsync( - new IPAssignmentSpecs.GetByPort(port.Id), - cancellationToken) + new IPAssignmentSpecs.GetByPort(port.Id)) let validDirectAssignments = ipAssignments .Filter(a => a is not IpPoolAssignment && IsValidAssignment(a, network, subnetName)) let validPoolAssignments = ipAssignments @@ -44,25 +42,23 @@ from __ in invalidAssignments .Map(a => _stateStore.For().IO.DeleteAsync(a)) .SequenceSerial() from newAssignment in validPoolAssignments.IsEmpty - ? from assignment in CreateAssignment(network, port, subnetName, ipPoolName, cancellationToken) + ? from assignment in CreateAssignment(network, port, subnetName, ipPoolName) select Some(assignment) : RightAsync>(None) select validPoolAssignments.Append(newAssignment).Append(validDirectAssignments) .Map(a => IPAddress.Parse(a.IpAddress!)) - .ToArray(); + .ToSeq(); private EitherAsync CreateAssignment( VirtualNetwork network, CatletNetworkPort port, string subnetName, - string ipPoolName, - CancellationToken cancellationToken) => + string ipPoolName) => from subnet in _stateStore.Read().IO.GetBySpecAsync( - new SubnetSpecs.GetByNetwork(network.Id, subnetName), - cancellationToken) + new SubnetSpecs.GetByNetwork(network.Id, subnetName)) from validSubnet in subnet.ToEitherAsync( Error.New($"Environment {network.Environment}: Subnet {subnetName} not found in network {network.Name}.")) - from assignment in _poolManager.AcquireIp(validSubnet.Id, ipPoolName, cancellationToken) + from assignment in _poolManager.AcquireIp(validSubnet.Id, ipPoolName) let _ = UpdatePortAssignment(port, assignment) select assignment; diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/ICatletIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/ICatletIpManager.cs index cf7ba6558..abffc78f6 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/ICatletIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/ICatletIpManager.cs @@ -10,9 +10,8 @@ namespace Eryph.Modules.Controller.Networks; public interface ICatletIpManager { - public EitherAsync ConfigurePortIps( + public EitherAsync> ConfigurePortIps( VirtualNetwork network, CatletNetworkPort port, - CatletNetworkConfig networkConfig, - CancellationToken cancellationToken); + CatletNetworkConfig networkConfig); } diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/IProviderIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/IProviderIpManager.cs index 52bdc34e5..3f7d29345 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/IProviderIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/IProviderIpManager.cs @@ -8,8 +8,7 @@ namespace Eryph.Modules.Controller.Networks; public interface IProviderIpManager { - public EitherAsync ConfigureFloatingPortIps( + public EitherAsync> ConfigureFloatingPortIps( string providerName, - FloatingNetworkPort port, - CancellationToken cancellationToken); + FloatingNetworkPort port); } diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs index 226497f36..3429b953b 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs @@ -21,12 +21,11 @@ internal class ProviderIpManager( IIpPoolManager poolManager) : BaseIpManager(stateStore, poolManager), IProviderIpManager { - public EitherAsync ConfigureFloatingPortIps( + public EitherAsync> ConfigureFloatingPortIps( string providerName, - FloatingNetworkPort port, - CancellationToken cancellationToken) => + FloatingNetworkPort port) => from ipAssignments in _stateStore.For().IO.ListAsync( - new IPAssignmentSpecs.GetByPort(port.Id), cancellationToken) + new IPAssignmentSpecs.GetByPort(port.Id)) let validDirectAssignments = ipAssignments .Filter(a => a is not IpPoolAssignment && IsValidAssignment(a, providerName, port.SubnetName)) let validPoolAssignments = ipAssignments @@ -37,25 +36,23 @@ from _ in invalidAssignments .Map(a => _stateStore.For().IO.DeleteAsync(a)) .SequenceSerial() from newAssignment in validPoolAssignments.IsEmpty - ? from assignment in CreateAssignment(port, providerName, port.SubnetName, port.PoolName, cancellationToken) + ? from assignment in CreateAssignment(port, providerName, port.SubnetName, port.PoolName) select Some(assignment) : RightAsync>(None) select validPoolAssignments.Append(newAssignment).Append(validDirectAssignments) .Map(a => IPAddress.Parse(a.IpAddress!)) - .ToArray(); + .ToSeq(); private EitherAsync CreateAssignment( FloatingNetworkPort port, string providerName, string subnetName, - string ipPoolName, - CancellationToken cancellationToken) => + string ipPoolName) => from subnet in _stateStore.Read().IO.GetBySpecAsync( - new SubnetSpecs.GetByProviderName(providerName, subnetName), - cancellationToken) + new SubnetSpecs.GetByProviderName(providerName, subnetName)) from validSubnet in subnet.ToEitherAsync( Error.New($"Subnet {subnetName} not found for provider {providerName}.")) - from assignment in _poolManager.AcquireIp(validSubnet.Id, ipPoolName, cancellationToken) + from assignment in _poolManager.AcquireIp(validSubnet.Id, ipPoolName) let _ = UpdatePortAssignment(port, assignment) select assignment; diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs index 013e84d57..5fb2897cf 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs @@ -35,8 +35,7 @@ public class UpdateCatletNetworksCommandHandler( { public async Task Handle(OperationTask message) { - await messaging.ProgressMessage(message, "Updating Catlet network settings"); - + await messaging.ProgressMessage(message, "Updating catlet network settings"); await UpdateNetworks(message.Command) .Map(settings => new UpdateCatletNetworksCommandResponse { @@ -47,28 +46,33 @@ await UpdateNetworks(message.Command) public EitherAsync> UpdateNetworks( UpdateCatletNetworksCommand command) => - // TODO delete ports which are no longer configured + from _ in RightAsync(unit) + let usedPortNames = command.Config.Networks + .ToSeq() + .Map(cfg => GetPortName(command.CatletId, cfg.AdapterName)) + from unusedPorts in stateStore.For().IO.ListAsync( + new CatletNetworkPortSpecs.GetUnused(command.CatletMetadataId, usedPortNames)) + from __ in unusedPorts.Map(RemovePort) + .SequenceSerial() from settings in command.Config.Networks .ToSeq() .Map(cfg => UpdateNetwork(command.CatletMetadataId, command, cfg)) .SequenceSerial() select settings; - private EitherAsync UpdateNetwork( Guid catletMetadataId, UpdateCatletNetworksCommand command, CatletNetworkConfig networkConfig) => - from environmentName in EnvironmentName.NewEither( - Optional(command.Config.Environment) - .Filter(notEmpty) - .IfNone(EryphConstants.DefaultEnvironmentName)) - .ToAsync() - from networkName in EryphNetworkName.NewEither( - Optional(networkConfig.Name) - .Filter(notEmpty) - .IfNone(EryphConstants.DefaultNetworkName)) - .ToAsync() + from environmentName in Optional(command.Config.Environment) + .Map(EnvironmentName.NewEither) + .Sequence().ToAsync() + .Map(n => n.IfNone(EnvironmentName.New(EryphConstants.DefaultEnvironmentName))) + from networkName in Optional(networkConfig.Name) + .Map(EryphNetworkName.NewEither) + .Sequence().ToAsync() + .Map(n => n.IfNone(EryphNetworkName.New(EryphConstants.DefaultNetworkName))) + from network in stateStore.For().IO.GetBySpecAsync( new VirtualNetworkSpecs.GetByName(command.ProjectId, networkName.Value, environmentName.Value)) // It is optional to have an environment specific network. Therefore, @@ -76,59 +80,51 @@ from network in stateStore.For().IO.GetBySpecAsync( from validNetwork in network.IsNone && environmentName != EnvironmentName.New(EryphConstants.DefaultEnvironmentName) ? stateStore.For().IO.GetBySpecAsync( new VirtualNetworkSpecs.GetByName(command.ProjectId, networkName.Value, EryphConstants.DefaultEnvironmentName)) - .Bind(fr => fr.ToEitherAsync(Error.New($"Network '{networkName}' not found in environment '{environmentName}' and default environment."))) + .Bind(o => o.ToEitherAsync( + Error.New($"Network '{networkName}' not found in environment '{environmentName}' and default environment."))) : network.ToEitherAsync(Error.New($"Network '{networkName}' not found in environment '{environmentName}'.")) from networkProviders in providerManager.GetCurrentConfiguration() - from networkProvider in networkProviders.NetworkProviders.Find(x => x.Name == validNetwork.NetworkProvider) - .ToEither(Error.New($"network provider {validNetwork.NetworkProvider} not found.")) + from networkProvider in networkProviders.NetworkProviders + .Find(x => x.Name == validNetwork.NetworkProvider) + .ToEither(Error.New($"Network provider {validNetwork.NetworkProvider} not found.")) .ToAsync() - let isFlatNetwork = networkProvider.Type == NetworkProviderType.Flat let fixedMacAddress = command.Config.NetworkAdapters .ToSeq() .Find(x => x.Name == networkConfig.AdapterName) .Bind(x => Optional(x.MacAddress)) - let c1 = new CancellationTokenSource(500000) - - + let hostname = Optional(command.Config.Hostname).Filter(notEmpty) + | Optional(command.Config.Name).Filter(notEmpty) from networkPort in AddOrUpdateAdapterPort( - validNetwork, command.CatletId, catletMetadataId, networkConfig.AdapterName, - command.Config.Hostname ?? command.Config.Name, fixedMacAddress, c1.Token) - - let c2 = new CancellationTokenSource() - - let providerSubnetName = validNetwork.Subnets + validNetwork, command.CatletId, catletMetadataId, + networkConfig.AdapterName, hostname.IfNoneUnsafe((string?)null), fixedMacAddress) + + from ips in isFlatNetwork + ? from existingAssignments in stateStore.For().IO.ListAsync( + new IPAssignmentSpecs.GetByPort(networkPort.Id)) + from _ in existingAssignments.Match( + Empty: () => RightAsync(unit), + Seq: a => stateStore.For().IO.DeleteRangeAsync(a)) + select Seq() + : ipManager.ConfigurePortIps(validNetwork, networkPort, networkConfig) - from floatingPort in isFlatNetwork + from floatingIps in isFlatNetwork ? from _ in Optional(networkPort.FloatingPort) - .Map(fp => stateStore.For().IO.DeleteAsync(fp)) - .Sequence() - select Option.None + .Map(fp => stateStore.For().IO.DeleteAsync(fp)) + .Sequence() + select Option>.None : from providerPort in stateStore.For().IO.GetBySpecAsync( - new ProviderRouterPortSpecs.GetByNetworkId(validNetwork.Id)) + new ProviderRouterPortSpecs.GetByNetworkId(validNetwork.Id)) from validProviderPort in providerPort.ToEitherAsync( - Error.New($"The overlay network '{validNetwork.Name}' has no provider port.")) - from fp in UpdateFloatingPort( - networkPort, networkProvider.Name, validProviderPort.SubnetName, validProviderPort.PoolName, c2.Token) - .ToAsync() - select Some(fp) - - let c3 = new CancellationTokenSource() - from ips in isFlatNetwork - ? RightAsync([]) - : ipManager.ConfigurePortIps( - validNetwork, networkPort, - networkConfig, - c3.Token) - - from floatingIps in isFlatNetwork - ? RightAsync([]) - : floatingPort.ToEither(Error.New("floating port is missing")) - .ToAsync() - .Bind(p => providerIpManager.ConfigureFloatingPortIps(networkProvider.Name, p, c3.Token)) + Error.New($"The overlay network '{validNetwork.Name}' has no provider port.")) + let providerSubnetName = validProviderPort.SubnetName + let providerPoolName = validProviderPort.PoolName + from fp in UpdateFloatingPort(networkPort, networkProvider.Name, providerSubnetName, providerPoolName) + from ips in providerIpManager.ConfigureFloatingPortIps(networkProvider.Name, fp) + select Some(ips) select new MachineNetworkSettings { @@ -140,13 +136,17 @@ from floatingIps in isFlatNetwork AddressesV4 = ips.Filter(x => x.AddressFamily == AddressFamily.InterNetwork) .Map(ip => ip.ToString()) .ToList(), - FloatingAddressV4 = floatingIps.FirstOrDefault(x => x.AddressFamily == AddressFamily.InterNetwork) - ?.ToString(), + FloatingAddressV4 = floatingIps.ToSeq().Flatten() + .Find(ip => ip.AddressFamily == AddressFamily.InterNetwork) + .Map(ip => ip.ToString()) + .IfNoneUnsafe((string?)null), AddressesV6 = ips.Filter(x => x.AddressFamily == AddressFamily.InterNetworkV6) .Map(ip => ip.ToString()) .ToList(), - FloatingAddressV6 = floatingIps.FirstOrDefault(x => x.AddressFamily == AddressFamily.InterNetworkV6) - ?.ToString(), + FloatingAddressV6 = floatingIps.ToSeq().Flatten() + .Find(ip => ip.AddressFamily == AddressFamily.InterNetworkV6) + .Map(ip => ip.ToString()) + .IfNoneUnsafe((string?)null), }; private EitherAsync AddOrUpdateAdapterPort( @@ -155,84 +155,78 @@ private EitherAsync AddOrUpdateAdapterPort( Guid catletMetadataId, string adapterName, string addressName, - Option fixedMacAddress, - CancellationToken cancellationToken) => + Option fixedMacAddress) => from _ in RightAsync(unit) let portName = GetPortName(catletId, adapterName) let macAddress = fixedMacAddress .Filter(notEmpty) .IfNone(() => MacAddresses.GenerateMacAddress(portName)) from existingPort in stateStore.For().IO.GetBySpecAsync( - new NetworkPortSpecs.GetByNetworkAndNameForCatlet(network.Id, portName), cancellationToken) + new CatletNetworkPortSpecs.GetByCatletMetadataIdAndName(catletMetadataId, portName)) from updatedPort in existingPort.Match( - Some: p => from _ in RightAsync(unit) - let __ = fun(() => - { - p.AddressName = addressName; - p.MacAddress = MacAddresses.FormatMacAddress(macAddress); - p.Network = network; - }) - select p, - None: () => from _ in RightAsync(unit) - let newPort = new CatletNetworkPort - { - Id = Guid.NewGuid(), - CatletMetadataId = catletMetadataId, - Name = portName, - MacAddress = MacAddresses.FormatMacAddress(macAddress), - Network = network, - AddressName = addressName, - IpAssignments = [], - } - from addedPort in stateStore.For().IO.AddAsync(newPort, cancellationToken) - select addedPort) + Some: p => + from _ in RightAsync(unit) + let __ = fun(() => + { + p.AddressName = addressName; + p.MacAddress = MacAddresses.FormatMacAddress(macAddress); + p.Network = network; + })() + select p, + None: () => + from _ in RightAsync(unit) + let newPort = new CatletNetworkPort + { + Id = Guid.NewGuid(), + CatletMetadataId = catletMetadataId, + Name = portName, + MacAddress = MacAddresses.FormatMacAddress(macAddress), + Network = network, + AddressName = addressName, + IpAssignments = [], + } + from addedPort in stateStore.For().IO.AddAsync(newPort) + select addedPort) select updatedPort; - private async Task> UpdateFloatingPort( + private EitherAsync UpdateFloatingPort( CatletNetworkPort adapterPort, string providerName, string providerSubnetName, - string providerPoolName, - CancellationToken cancellationToken) - { - await stateStore.LoadPropertyAsync(adapterPort, x => x.FloatingPort, cancellationToken); - - if (adapterPort.FloatingPort != null) - { - var floatingPort = adapterPort.FloatingPort; - if (floatingPort.ProviderName != providerName || floatingPort.SubnetName != - providerSubnetName || floatingPort.PoolName != providerPoolName) - { - adapterPort.FloatingPort = null; - await stateStore.For().DeleteAsync(floatingPort, cancellationToken); - } - - } - - if (adapterPort.FloatingPort != null) - return adapterPort.FloatingPort; - - var port = new FloatingNetworkPort - { - Id = Guid.NewGuid(), - Name = adapterPort.Name, - ProviderName = providerName, - SubnetName = providerSubnetName, - PoolName = providerPoolName, - MacAddress = MacAddresses.FormatMacAddress(MacAddresses.GenerateMacAddress(Guid.NewGuid().ToString())) - }; - - adapterPort.FloatingPort = port; - - return await stateStore.For().AddAsync(port, cancellationToken); - } - - // TODO Remove port and attached floating port if necessary - private EitherAsync RemovePort() => Error.New("not implemented"); - - private EitherAsync RemoveFloatingPort() => Error.New("not implemented"); + string providerPoolName) => + from _ in RightAsync(unit) + let existingFloatingPort = Optional(adapterPort.FloatingPort) + from validFloatingPort in existingFloatingPort + .Filter(fp => fp.ProviderName == providerName + && fp.SubnetName == providerSubnetName + && fp.PoolName == providerPoolName) + .Map(RightAsync) + .IfNone(() => + from _ in existingFloatingPort + .Map(fp => stateStore.For().IO.DeleteAsync(fp)) + .Sequence() + let fp = new FloatingNetworkPort + { + Id = Guid.NewGuid(), + Name = adapterPort.Name, + ProviderName = providerName, + SubnetName = providerSubnetName, + PoolName = providerPoolName, + MacAddress = MacAddresses.FormatMacAddress(MacAddresses.GenerateMacAddress(Guid.NewGuid().ToString())) + } + from __ in stateStore.For().IO.AddAsync(fp) + let ___ = fun(() => adapterPort.FloatingPort = fp)() + select fp) + select validFloatingPort; + + private EitherAsync RemovePort(CatletNetworkPort port) => + from _ in Optional(port.FloatingPort) + .Map(fp => stateStore.For().IO.DeleteAsync(fp)) + .Sequence() + from __ in stateStore.For().IO.DeleteAsync(port) + select unit; private static string GetPortName(Guid catletId, string adapterName) => $"{catletId}_{adapterName}"; diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs index 75b532880..72fc3d7f5 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs @@ -83,8 +83,7 @@ await WithScope(async (ipManager, _, stateStore) => await stateStore.For().AddAsync(catletPort); var result = await ipManager.ConfigurePortIps( - network!, catletPort, networkConfig, - CancellationToken.None); + network!, catletPort, networkConfig); result.Should().BeRight().Which.Should().SatisfyRespectively( ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); @@ -173,8 +172,7 @@ await WithScope(async (catletIpManager, _, stateStore) => network.Should().NotBeNull(); var result = await catletIpManager.ConfigurePortIps( - network!, catletPort!, networkConfig, - CancellationToken.None); + network!, catletPort!, networkConfig); result.Should().BeRight().Which.Should().SatisfyRespectively( ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); @@ -261,8 +259,7 @@ await WithScope(async (catletIpManager, _, stateStore) => network.Should().NotBeNull(); var result = await catletIpManager.ConfigurePortIps( - network!, catletPort!, networkConfig, - CancellationToken.None); + network!, catletPort!, networkConfig); result.Should().BeRight().Which.Should().SatisfyRespectively( ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs index 9567e8a85..ae29b114a 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs @@ -46,9 +46,7 @@ await WithScope(async (providerIpManager, _, stateStore) => await stateStore.For().AddAsync(floatingPort); var result = await providerIpManager.ConfigureFloatingPortIps( - providerName, - floatingPort, - default); + providerName, floatingPort); result.Should().BeRight().Which.Should().SatisfyRespectively( ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); @@ -113,7 +111,7 @@ await WithScope(async (providerIpManager, _, stateStore) => floatingPort.Should().NotBeNull(); var result = await providerIpManager.ConfigureFloatingPortIps( - providerName, floatingPort!, default); + providerName, floatingPort!); result.Should().BeRight().Which.Should().SatisfyRespectively( ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); @@ -179,7 +177,7 @@ await WithScope(async (providerIpManager, _, stateStore) => floatingPort.PoolName = poolName; var result = await providerIpManager.ConfigureFloatingPortIps( - providerName, floatingPort, default); + providerName, floatingPort); result.Should().BeRight().Which.Should().SatisfyRespectively( ipAddress => ipAddress.ToString().Should().Be(expectedIpAddress)); diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs index e3c2d9173..125fd038a 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs @@ -238,6 +238,44 @@ await WithScope(async (_, stateStore) => }); } + [Fact] + public async Task UpdateNetworks_CatletHasHostName_HostnameIsUsed() + { + var command = new UpdateCatletNetworksCommand + { + CatletId = Guid.Parse(CatletId), + CatletMetadataId = Guid.Parse(CatletMetadataId), + ProjectId = Guid.Parse(DefaultProjectId), + Config = new CatletConfig + { + Hostname = "test-catlet", + Networks = + [ + new CatletNetworkConfig + { + AdapterName = "eth0", + } + ], + }, + }; + + await WithScope(async (handler, stateStore) => + { + var result = await handler.UpdateNetworks(command); + result.Should().BeRight(); + + await stateStore.SaveChangesAsync(); + }); + + await WithScope(async (_, stateStore) => + { + var catletPorts = await stateStore.For().ListAsync(); + var catletPort = catletPorts.Should().ContainSingle().Subject; + catletPort.CatletMetadataId.Should().Be(Guid.Parse(CatletMetadataId)); + catletPort.AddressName.Should().Be("test-catlet"); + }); + } + [Fact] public async Task UpdateNetworks_MoveCatletFromFlatNetworkToOverlayNetwork_CreatesCorrectNetworkConfig() { @@ -299,7 +337,7 @@ await WithScope(async (handler, stateStore) => result.Should().BeRight().Which.Should().SatisfyRespectively( settings => { - settings.NetworkProviderName.Should().Be("flat-provider"); + settings.NetworkProviderName.Should().Be("default"); settings.AdapterName.Should().Be("eth0"); settings.PortName.Should().Be($"{CatletId}_eth0"); settings.NetworkName.Should().Be("default"); From 3e4ef95b2f5eed508b60c4b25059c318f149c804 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Thu, 14 Nov 2024 17:26:17 +0100 Subject: [PATCH 12/30] Cleanup tests --- .../Networks/CatletIpManagerTests.cs | 183 ++---------------- 1 file changed, 15 insertions(+), 168 deletions(-) diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs index 72fc3d7f5..7e63fb120 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs @@ -11,52 +11,25 @@ namespace Eryph.Modules.Controller.Tests.Networks; public sealed class CatletIpManagerTests : InMemoryStateDbTestBase { - private const string DefaultProjectId = "4B4A3FCF-B5ED-4A9A-AB6E-03852752095E"; - private const string SecondProjectId = "75c27daf-77c8-4b98-a072-a4706dceb422"; - private const string DefaultNetworkId = "cb58fe00-3f64-4b66-b58e-23fb15df3cac"; private const string DefaultSubnetId = "ed6697cd-836f-4da7-914b-b09ed1567934"; private const string SecondSubnetId = "4f976208-613a-40d4-a284-d32cbd4a1b8e"; - private const string SecondNetworkId = "e480a020-57d0-4443-a973-57aa0c95872e"; - private const string SecondNetworkSubnetId = "27ec11a4-5d6a-47da-9f9f-eb7486db38ea"; - - private const string SecondEnvironmentNetworkId = "81a139e5-ab61-4fe3-b81f-59c11a665d22"; - private const string SecondEnvironmentSubnetId = "dc807357-50e7-4263-8298-0c97ff69f4cf"; - - private const string SecondProjectNetworkId = "c0043e88-8268-4ac0-b027-2fa37ad3168f"; - private const string SecondProjectSubnetId = "0c721846-5e2e-40a9-83d2-f1b75206ef84"; - private const string CatletMetadataId = "15e2b061-c625-4469-9fe7-7c455058fcc0"; private static readonly Guid CatletPortId = Guid.NewGuid(); [Theory] - [InlineData(DefaultProjectId, "default", DefaultNetworkId, null, null, null, "10.0.0.12")] - [InlineData(DefaultProjectId, "default", DefaultNetworkId, "default", "default", "default", "10.0.0.12")] - [InlineData(DefaultProjectId, "default", DefaultNetworkId, "default", "default", "second-pool", "10.0.1.12")] - [InlineData(DefaultProjectId, "default", DefaultNetworkId, "default", "second-subnet", "default", "10.1.0.12")] - [InlineData(DefaultProjectId, "default", SecondNetworkId, "second-network", "default", "default", "10.5.0.12")] - [InlineData(DefaultProjectId, "second-environment", SecondEnvironmentNetworkId, "default", "default", "default", "10.10.0.12")] - // When the environment does not have a dedicated network, we should fall back to the default network - [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, null, null, null, "10.0.0.12")] - [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, "default", "default", "default", "10.0.0.12")] - [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, "default", "default", "second-pool", "10.0.1.12")] - [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, "default", "second-subnet", "default", "10.1.0.12")] - [InlineData(DefaultProjectId, "environment-without-network", SecondNetworkId, "second-network", "default", "default", "10.5.0.12")] - [InlineData(SecondProjectId, "default", SecondProjectNetworkId, null, null, null, "10.100.0.12")] - [InlineData(SecondProjectId, "default", SecondProjectNetworkId, "default", "default", "default", "10.100.0.12")] + [InlineData(null, null, "10.0.0.12")] + [InlineData("default", "default", "10.0.0.12")] + [InlineData("default", "second-pool", "10.0.1.12")] + [InlineData("second-subnet", "default", "10.1.0.12")] public async Task ConfigurePortIps_NewPortIsAdded_AssignmentIsCreated( - string projectId, - string environment, - string networkId, - string? networkName, string? subnetName, string? poolName, string expectedIpAddress) { var networkConfig = new CatletNetworkConfig() { - Name = networkName, SubnetV4 = subnetName != null || poolName != null ? new CatletSubnetConfig { @@ -69,7 +42,7 @@ public async Task ConfigurePortIps_NewPortIsAdded_AssignmentIsCreated( await WithScope(async (ipManager, _, stateStore) => { var network = await stateStore.For() - .GetByIdAsync(Guid.Parse(networkId)); + .GetByIdAsync(Guid.Parse(DefaultNetworkId)); network.Should().NotBeNull(); var catletPort = new CatletNetworkPort @@ -104,26 +77,12 @@ await WithScope(async (_, _, stateStore) => } [Theory] - [InlineData(DefaultProjectId, "default", DefaultNetworkId, DefaultSubnetId, null, null, null, "10.0.0.12")] - [InlineData(DefaultProjectId, "default", DefaultNetworkId, DefaultSubnetId, "default", "default", "default", "10.0.0.12")] - [InlineData(DefaultProjectId, "default", DefaultNetworkId, DefaultSubnetId, "default", "default", "second-pool", "10.0.1.12")] - [InlineData(DefaultProjectId, "default", DefaultNetworkId, SecondSubnetId, "default", "second-subnet", "default", "10.1.0.12")] - [InlineData(DefaultProjectId, "default", SecondNetworkId, SecondNetworkSubnetId, "second-network", "default", "default", "10.5.0.12")] - [InlineData(DefaultProjectId, "second-environment", SecondEnvironmentNetworkId, SecondEnvironmentSubnetId, "default", "default", "default", "10.10.0.12")] - // When the environment does not have a dedicated network, we should fall back to the default network - [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, DefaultSubnetId, null, null, null, "10.0.0.12")] - [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, DefaultSubnetId, "default", "default", "default", "10.0.0.12")] - [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, DefaultSubnetId, "default", "default", "second-pool", "10.0.1.12")] - [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, SecondSubnetId, "default", "second-subnet", "default", "10.1.0.12")] - [InlineData(DefaultProjectId, "environment-without-network", SecondNetworkId, SecondNetworkSubnetId, "second-network", "default", "default", "10.5.0.12")] - [InlineData(SecondProjectId, "default", SecondProjectNetworkId, SecondProjectSubnetId, null, null, null, "10.100.0.12")] - [InlineData(SecondProjectId, "default", SecondProjectNetworkId, SecondProjectSubnetId, "default", "default", "default", "10.100.0.12")] + [InlineData(DefaultSubnetId, null, null, "10.0.0.12")] + [InlineData(DefaultSubnetId, "default", "default", "10.0.0.12")] + [InlineData(DefaultSubnetId, "default", "second-pool", "10.0.1.12")] + [InlineData(SecondSubnetId, "second-subnet", "default", "10.1.0.12")] public async Task ConfigurePortIps_AssignmentIsValid_AssignmentIsNotChanged( - string projectId, - string environment, - string networkId, string subnetId, - string? networkName, string? subnetName, string? poolName, string expectedIpAddress) @@ -141,7 +100,7 @@ await WithScope(async (_, ipPoolManager, stateStore) => Id = CatletPortId, Name = "test-catlet-port", MacAddress = "42:00:42:00:00:01", - NetworkId = Guid.Parse(networkId), + NetworkId = Guid.Parse(DefaultNetworkId), CatletMetadataId = Guid.Parse(CatletMetadataId), IpAssignments = [ipAssignment], }; @@ -152,7 +111,6 @@ await WithScope(async (_, ipPoolManager, stateStore) => var networkConfig = new CatletNetworkConfig() { - Name = networkName, SubnetV4 = subnetName != null || poolName != null ? new CatletSubnetConfig { @@ -168,7 +126,7 @@ await WithScope(async (catletIpManager, _, stateStore) => .GetByIdAsync(CatletPortId); catletPort.Should().NotBeNull(); var network = await stateStore.For() - .GetByIdAsync(Guid.Parse(networkId)); + .GetByIdAsync(Guid.Parse(DefaultNetworkId)); network.Should().NotBeNull(); var result = await catletIpManager.ConfigurePortIps( @@ -194,21 +152,9 @@ await WithScope(async (_, _, stateStore) => } [Theory] - [InlineData(DefaultProjectId, "default", DefaultNetworkId, "default", "default", "second-pool", "10.0.1.12")] - [InlineData(DefaultProjectId, "default", DefaultNetworkId, "default", "second-subnet", "default", "10.1.0.12")] - [InlineData(DefaultProjectId, "default", SecondNetworkId, "second-network", "default", "default", "10.5.0.12")] - [InlineData(DefaultProjectId, "second-environment", SecondEnvironmentNetworkId, "default", "default", "default", "10.10.0.12")] - // When the environment does not have a dedicated network, we should fall back to the default network - [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, "default", "default", "second-pool", "10.0.1.12")] - [InlineData(DefaultProjectId, "environment-without-network", DefaultNetworkId, "default", "second-subnet", "default", "10.1.0.12")] - [InlineData(DefaultProjectId, "environment-without-network", SecondNetworkId, "second-network", "default", "default", "10.5.0.12")] - [InlineData(SecondProjectId, "default", SecondProjectNetworkId, null, null, null, "10.100.0.12")] - [InlineData(SecondProjectId, "default", SecondProjectNetworkId, "default", "default", "default", "10.100.0.12")] + [InlineData("default", "second-pool", "10.0.1.12")] + [InlineData("second-subnet", "default", "10.1.0.12")] public async Task ConfigurePortIps_AssignmentIsInvalid_AssignmentIsChanged( - string projectId, - string environment, - string networkId, - string? networkName, string? subnetName, string? poolName, string expectedIpAddress) @@ -238,7 +184,6 @@ await WithScope(async (_, ipPoolManager, stateStore) => var networkConfig = new CatletNetworkConfig() { - Name = networkName, SubnetV4 = subnetName != null || poolName != null ? new CatletSubnetConfig { @@ -255,7 +200,7 @@ await WithScope(async (catletIpManager, _, stateStore) => catletPort.Should().NotBeNull(); var network = await stateStore.Read() - .GetByIdAsync(Guid.Parse(networkId)); + .GetByIdAsync(Guid.Parse(DefaultNetworkId)); network.Should().NotBeNull(); var result = await catletIpManager.ConfigurePortIps( @@ -308,14 +253,6 @@ await stateStore.For().AddAsync(new CatletMetadata Id = Guid.Parse(CatletMetadataId), }); - var projectB = new Project() - { - Id = Guid.Parse(SecondProjectId), - Name = "second-project", - TenantId = EryphConstants.DefaultTenantId, - }; - await stateStore.For().AddAsync(projectB); - await stateStore.For().AddAsync( new VirtualNetwork { @@ -372,95 +309,5 @@ await stateStore.For().AddAsync( }, ], }); - - await stateStore.For().AddAsync( - new VirtualNetwork - { - Id = Guid.Parse(SecondNetworkId), - ProjectId = EryphConstants.DefaultProjectId, - Name = "second-network", - Environment = EryphConstants.DefaultEnvironmentName, - Subnets = - [ - new VirtualNetworkSubnet - { - Id = Guid.Parse(SecondNetworkSubnetId), - Name = EryphConstants.DefaultSubnetName, - IpNetwork = "10.5.0.0/16", - IpPools = - [ - new IpPool() - { - Id = Guid.NewGuid(), - Name = EryphConstants.DefaultIpPoolName, - IpNetwork = "10.5.0.0/16", - FirstIp = "10.5.0.10", - NextIp = "10.5.0.12", - LastIp = "10.5.0.19", - } - ], - }, - ], - }); - - await stateStore.For().AddAsync( - new VirtualNetwork - { - Id = Guid.Parse(SecondEnvironmentNetworkId), - ProjectId = EryphConstants.DefaultProjectId, - Name = EryphConstants.DefaultNetworkName, - Environment = "second-environment", - Subnets = - [ - new VirtualNetworkSubnet - { - Id = Guid.Parse(SecondEnvironmentSubnetId), - Name = EryphConstants.DefaultSubnetName, - IpNetwork = "10.10.0.0/16", - IpPools = - [ - new IpPool() - { - Id = Guid.NewGuid(), - Name = EryphConstants.DefaultIpPoolName, - IpNetwork = "10.10.0.0/16", - FirstIp = "10.10.0.10", - NextIp = "10.10.0.12", - LastIp = "10.10.0.19", - } - ], - }, - ], - }); - - await stateStore.For().AddAsync( - new VirtualNetwork - { - Id = Guid.Parse(SecondProjectNetworkId), - ProjectId = Guid.Parse(SecondProjectId), - Name = EryphConstants.DefaultNetworkName, - Environment = EryphConstants.DefaultEnvironmentName, - Subnets = - [ - new VirtualNetworkSubnet - { - Id = Guid.Parse(SecondProjectSubnetId), - Name = EryphConstants.DefaultSubnetName, - IpNetwork = "10.100.0.0/16", - IpPools = - [ - new IpPool() - { - Id = Guid.NewGuid(), - Name = EryphConstants.DefaultIpPoolName, - IpNetwork = "10.100.0.0/16", - FirstIp = "10.100.0.10", - NextIp = "10.100.0.12", - LastIp = "10.100.0.19", - } - ], - }, - ], - }); } -} \ No newline at end of file +} From 7d173747a08a69fae0e42fd880abc74918e34040 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Thu, 14 Nov 2024 18:06:53 +0100 Subject: [PATCH 13/30] Cleanup --- .../Specifications/IPAssignmentSpecs.cs | 8 ++++++ .../Networks/BaseIpManager.cs | 25 ------------------- .../Networks/CatletIpManager.cs | 18 ++++++------- .../Networks/ProviderIpManager.cs | 14 +++++------ .../VirtualNetworkChangeTrackingTests.cs | 3 --- .../Networks/CatletIpManagerTests.cs | 4 +-- .../Networks/NetworkConfigValidatorTests.cs | 2 +- .../Networks/ProviderIpManagerTests.cs | 2 +- 8 files changed, 26 insertions(+), 50 deletions(-) delete mode 100644 src/modules/src/Eryph.Modules.Controller/Networks/BaseIpManager.cs diff --git a/src/data/src/Eryph.StateDb/Specifications/IPAssignmentSpecs.cs b/src/data/src/Eryph.StateDb/Specifications/IPAssignmentSpecs.cs index 9d24e3e9f..47df9e86d 100644 --- a/src/data/src/Eryph.StateDb/Specifications/IPAssignmentSpecs.cs +++ b/src/data/src/Eryph.StateDb/Specifications/IPAssignmentSpecs.cs @@ -9,6 +9,14 @@ public static class IPAssignmentSpecs public sealed class GetByPort : Specification { public GetByPort(Guid portId) + { + Query.Where(x => x.NetworkPortId == portId); + } + } + + public sealed class GetByPortWithPoolAndSubnet : Specification + { + public GetByPortWithPoolAndSubnet(Guid portId) { Query.Where(x => x.NetworkPortId == portId) .Include(a => ((IpPoolAssignment)a).Pool) diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/BaseIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/BaseIpManager.cs deleted file mode 100644 index f25146d0f..000000000 --- a/src/modules/src/Eryph.Modules.Controller/Networks/BaseIpManager.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using Eryph.StateDb; -using Eryph.StateDb.Model; -using LanguageExt; - -namespace Eryph.Modules.Controller.Networks; - -public abstract class BaseIpManager -{ - protected readonly IStateStore _stateStore; - protected readonly IIpPoolManager _poolManager; - - protected BaseIpManager(IStateStore stateStore, IIpPoolManager poolManager) - { - _stateStore = stateStore; - _poolManager = poolManager; - } - - protected static Unit UpdatePortAssignment(NetworkPort port, IpAssignment newAssignment) - { - newAssignment.NetworkPortId = port.Id; - - return Unit.Default; - } -} diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs index 8af2823a0..bdf43ee49 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs @@ -19,7 +19,7 @@ namespace Eryph.Modules.Controller.Networks; public class CatletIpManager( IStateStore stateStore, IIpPoolManager poolManager) - : BaseIpManager(stateStore, poolManager), ICatletIpManager + : ICatletIpManager { public EitherAsync> ConfigurePortIps( VirtualNetwork network, @@ -30,8 +30,8 @@ from _ in RightAsync(unit) .IfNone(EryphConstants.DefaultSubnetName) let ipPoolName = Optional(networkConfig.SubnetV4?.IpPool) .IfNone(EryphConstants.DefaultIpPoolName) - from ipAssignments in _stateStore.For().IO.ListAsync( - new IPAssignmentSpecs.GetByPort(port.Id)) + from ipAssignments in stateStore.For().IO.ListAsync( + new IPAssignmentSpecs.GetByPortWithPoolAndSubnet(port.Id)) let validDirectAssignments = ipAssignments .Filter(a => a is not IpPoolAssignment && IsValidAssignment(a, network, subnetName)) let validPoolAssignments = ipAssignments @@ -39,7 +39,7 @@ from ipAssignments in _stateStore.For().IO.ListAsync( .Filter(a => IsValidPoolAssignment(a, network, subnetName, ipPoolName)) let invalidAssignments = ipAssignments.Except(validDirectAssignments).Except(validPoolAssignments) from __ in invalidAssignments - .Map(a => _stateStore.For().IO.DeleteAsync(a)) + .Map(a => stateStore.For().IO.DeleteAsync(a)) .SequenceSerial() from newAssignment in validPoolAssignments.IsEmpty ? from assignment in CreateAssignment(network, port, subnetName, ipPoolName) @@ -54,18 +54,14 @@ private EitherAsync CreateAssignment( CatletNetworkPort port, string subnetName, string ipPoolName) => - from subnet in _stateStore.Read().IO.GetBySpecAsync( + from subnet in stateStore.Read().IO.GetBySpecAsync( new SubnetSpecs.GetByNetwork(network.Id, subnetName)) from validSubnet in subnet.ToEitherAsync( Error.New($"Environment {network.Environment}: Subnet {subnetName} not found in network {network.Name}.")) - from assignment in _poolManager.AcquireIp(validSubnet.Id, ipPoolName) - let _ = UpdatePortAssignment(port, assignment) + from assignment in poolManager.AcquireIp(validSubnet.Id, ipPoolName) + let _ = fun(() => { assignment.NetworkPort = port; })() select assignment; - - // TODO Check environment match - // TODO Check IP address is valid - private static bool IsValidAssignment( IpAssignment assignment, VirtualNetwork network, diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs index 3429b953b..ebff6eeb8 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs @@ -19,13 +19,13 @@ namespace Eryph.Modules.Controller.Networks; internal class ProviderIpManager( IStateStore stateStore, IIpPoolManager poolManager) - : BaseIpManager(stateStore, poolManager), IProviderIpManager + : IProviderIpManager { public EitherAsync> ConfigureFloatingPortIps( string providerName, FloatingNetworkPort port) => - from ipAssignments in _stateStore.For().IO.ListAsync( - new IPAssignmentSpecs.GetByPort(port.Id)) + from ipAssignments in stateStore.For().IO.ListAsync( + new IPAssignmentSpecs.GetByPortWithPoolAndSubnet(port.Id)) let validDirectAssignments = ipAssignments .Filter(a => a is not IpPoolAssignment && IsValidAssignment(a, providerName, port.SubnetName)) let validPoolAssignments = ipAssignments @@ -33,7 +33,7 @@ from ipAssignments in _stateStore.For().IO.ListAsync( .Filter(a => IsValidPoolAssignment(a, providerName, port.SubnetName, port.PoolName)) let invalidAssignments = ipAssignments.Except(validDirectAssignments).Except(validPoolAssignments) from _ in invalidAssignments - .Map(a => _stateStore.For().IO.DeleteAsync(a)) + .Map(a => stateStore.For().IO.DeleteAsync(a)) .SequenceSerial() from newAssignment in validPoolAssignments.IsEmpty ? from assignment in CreateAssignment(port, providerName, port.SubnetName, port.PoolName) @@ -48,12 +48,12 @@ private EitherAsync CreateAssignment( string providerName, string subnetName, string ipPoolName) => - from subnet in _stateStore.Read().IO.GetBySpecAsync( + from subnet in stateStore.Read().IO.GetBySpecAsync( new SubnetSpecs.GetByProviderName(providerName, subnetName)) from validSubnet in subnet.ToEitherAsync( Error.New($"Subnet {subnetName} not found for provider {providerName}.")) - from assignment in _poolManager.AcquireIp(validSubnet.Id, ipPoolName) - let _ = UpdatePortAssignment(port, assignment) + from assignment in poolManager.AcquireIp(validSubnet.Id, ipPoolName) + let _ = fun(() => { assignment.NetworkPort = port; })() select assignment; private static bool IsValidAssignment( diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs index 774f3eaad..bdd0f8c33 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs @@ -1,11 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net.Mail; using System.Text; using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; using Eryph.ConfigModel.Json; using Eryph.ConfigModel.Networks; using Eryph.Configuration.Model; diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs index 7e63fb120..cf325a14f 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs @@ -195,11 +195,11 @@ await WithScope(async (_, ipPoolManager, stateStore) => await WithScope(async (catletIpManager, _, stateStore) => { - var catletPort = await stateStore.Read() + var catletPort = await stateStore.For() .GetByIdAsync(CatletPortId); catletPort.Should().NotBeNull(); - var network = await stateStore.Read() + var network = await stateStore.For() .GetByIdAsync(Guid.Parse(DefaultNetworkId)); network.Should().NotBeNull(); diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigValidatorTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigValidatorTests.cs index 5c76dd94b..a1f57647e 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigValidatorTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigValidatorTests.cs @@ -566,7 +566,7 @@ await networkRepo.AddAsync( new CatletNetworkPort() { Name = "test-catlet-port", - MacAddress = "42:00:42:00:00:1ß", + MacAddress = "42:00:42:00:00:10", CatletMetadataId = firstCatletMetadata.Id, }, new ProviderRouterPort() diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs index ae29b114a..6cb1b5719 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs @@ -168,7 +168,7 @@ await WithScope(async (_, ipPoolManager, stateStore) => await WithScope(async (providerIpManager, _, stateStore) => { - var floatingPort = await stateStore.Read() + var floatingPort = await stateStore.For() .GetByIdAsync(FloatingPortId); floatingPort.Should().NotBeNull(); From 2598154872f2df078847a22d4dfe84f4605614b0 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Fri, 15 Nov 2024 15:13:47 +0100 Subject: [PATCH 14/30] Cleanup --- .../Networks/CatletIpManager.cs | 3 +-- .../Networks/ProviderIpManager.cs | 7 ++----- .../UpdateCatletNetworksCommandHandler.cs | 17 +++++++++-------- .../Networks/CatletIpManagerTests.cs | 6 ++---- .../Networks/ProviderIpManagerTests.cs | 6 ++---- .../UpdateCatletNetworksCommandHandlerTests.cs | 7 +++---- 6 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs index bdf43ee49..87d73755e 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/CatletIpManager.cs @@ -3,7 +3,6 @@ using System.Net; using System.Threading; using System.Threading.Tasks; -using Eryph.ConfigModel; using Eryph.ConfigModel.Catlets; using Eryph.Core; using Eryph.StateDb; @@ -54,7 +53,7 @@ private EitherAsync CreateAssignment( CatletNetworkPort port, string subnetName, string ipPoolName) => - from subnet in stateStore.Read().IO.GetBySpecAsync( + from subnet in stateStore.For().IO.GetBySpecAsync( new SubnetSpecs.GetByNetwork(network.Id, subnetName)) from validSubnet in subnet.ToEitherAsync( Error.New($"Environment {network.Environment}: Subnet {subnetName} not found in network {network.Name}.")) diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs b/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs index ebff6eeb8..7bdd4d64c 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/ProviderIpManager.cs @@ -3,9 +3,6 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using Eryph.ConfigModel; -using Eryph.Core; -using Eryph.Core.Network; using Eryph.StateDb; using Eryph.StateDb.Model; using Eryph.StateDb.Specifications; @@ -37,7 +34,7 @@ from _ in invalidAssignments .SequenceSerial() from newAssignment in validPoolAssignments.IsEmpty ? from assignment in CreateAssignment(port, providerName, port.SubnetName, port.PoolName) - select Some(assignment) + select Some(assignment) : RightAsync>(None) select validPoolAssignments.Append(newAssignment).Append(validDirectAssignments) .Map(a => IPAddress.Parse(a.IpAddress!)) @@ -48,7 +45,7 @@ private EitherAsync CreateAssignment( string providerName, string subnetName, string ipPoolName) => - from subnet in stateStore.Read().IO.GetBySpecAsync( + from subnet in stateStore.For().IO.GetBySpecAsync( new SubnetSpecs.GetByProviderName(providerName, subnetName)) from validSubnet in subnet.ToEitherAsync( Error.New($"Subnet {subnetName} not found for provider {providerName}.")) diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs index 5fb2897cf..5153c13a6 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateCatletNetworksCommandHandler.cs @@ -52,7 +52,8 @@ from _ in RightAsync(unit) .Map(cfg => GetPortName(command.CatletId, cfg.AdapterName)) from unusedPorts in stateStore.For().IO.ListAsync( new CatletNetworkPortSpecs.GetUnused(command.CatletMetadataId, usedPortNames)) - from __ in unusedPorts.Map(RemovePort) + from __ in unusedPorts + .Map(RemovePort) .SequenceSerial() from settings in command.Config.Networks .ToSeq() @@ -105,9 +106,9 @@ from networkPort in AddOrUpdateAdapterPort( from ips in isFlatNetwork ? from existingAssignments in stateStore.For().IO.ListAsync( new IPAssignmentSpecs.GetByPort(networkPort.Id)) - from _ in existingAssignments.Match( - Empty: () => RightAsync(unit), - Seq: a => stateStore.For().IO.DeleteRangeAsync(a)) + from _ in existingAssignments + .Map(a => stateStore.For().IO.DeleteAsync(a)) + .SequenceSerial() select Seq() : ipManager.ConfigurePortIps(validNetwork, networkPort, networkConfig) @@ -115,7 +116,7 @@ from floatingIps in isFlatNetwork ? from _ in Optional(networkPort.FloatingPort) .Map(fp => stateStore.For().IO.DeleteAsync(fp)) .Sequence() - select Option>.None + select Seq() : from providerPort in stateStore.For().IO.GetBySpecAsync( new ProviderRouterPortSpecs.GetByNetworkId(validNetwork.Id)) from validProviderPort in providerPort.ToEitherAsync( @@ -124,7 +125,7 @@ from validProviderPort in providerPort.ToEitherAsync( let providerPoolName = validProviderPort.PoolName from fp in UpdateFloatingPort(networkPort, networkProvider.Name, providerSubnetName, providerPoolName) from ips in providerIpManager.ConfigureFloatingPortIps(networkProvider.Name, fp) - select Some(ips) + select ips select new MachineNetworkSettings { @@ -136,14 +137,14 @@ select Some(ips) AddressesV4 = ips.Filter(x => x.AddressFamily == AddressFamily.InterNetwork) .Map(ip => ip.ToString()) .ToList(), - FloatingAddressV4 = floatingIps.ToSeq().Flatten() + FloatingAddressV4 = floatingIps .Find(ip => ip.AddressFamily == AddressFamily.InterNetwork) .Map(ip => ip.ToString()) .IfNoneUnsafe((string?)null), AddressesV6 = ips.Filter(x => x.AddressFamily == AddressFamily.InterNetworkV6) .Map(ip => ip.ToString()) .ToList(), - FloatingAddressV6 = floatingIps.ToSeq().Flatten() + FloatingAddressV6 = floatingIps .Find(ip => ip.AddressFamily == AddressFamily.InterNetworkV6) .Map(ip => ip.ToString()) .IfNoneUnsafe((string?)null), diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs index cf325a14f..189cb3a3f 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs @@ -224,8 +224,6 @@ await WithScope(async (_, _, stateStore) => }); }); } - - // TODO Add negative test private async Task WithScope(Func func) { @@ -238,8 +236,8 @@ private async Task WithScope(Func(Lifestyle.Scoped); options.Container.Register(Lifestyle.Scoped); } diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs index 6cb1b5719..137ddcb2c 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs @@ -66,8 +66,6 @@ await WithScope(async (_, _, stateStore) => }); } - // TODO add negative tests - [Theory] [InlineData("default", DefaultSubnetId, "default", "default", "10.0.0.12")] [InlineData("default", DefaultSubnetId, "default", "second-pool", "10.0.1.12")] @@ -210,8 +208,8 @@ private async Task WithScope(Func(Lifestyle.Scoped); options.Container.Register(Lifestyle.Scoped); } diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs index 125fd038a..8bd2b18ed 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs @@ -662,7 +662,7 @@ protected override void AddSimpleInjector(SimpleInjectorAddOptions options) options.Container.RegisterInstance(_taskMessingMock.Object); options.Container.RegisterInstance(_networkProviderManagerMock.Object); - // Use the proper managers instead of mocks as the code quite + // Use the proper managers instead of mocks.The code is quite // interdependent as it modifies the same EF Core entities. options.Container.Register(Lifestyle.Scoped); options.Container.Register(Lifestyle.Scoped); @@ -756,13 +756,12 @@ await stateStore.For().AddAsync(new CatletMetadata Id = Guid.Parse(CatletMetadataId), }); - var projectB = new Project() + await stateStore.For().AddAsync(new Project() { Id = Guid.Parse(SecondProjectId), Name = "second-project", TenantId = EryphConstants.DefaultTenantId, - }; - await stateStore.For().AddAsync(projectB); + }); await stateStore.For().AddAsync( new VirtualNetwork From 65729c68ce5b816d2266faf3e3fe4a5e37ea210d Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Wed, 20 Nov 2024 20:37:58 +0100 Subject: [PATCH 15/30] Fix incorrect API spec --- .../src/Eryph.Modules.ComputeApi/Model/V1/VirtualNetwork.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/src/Eryph.Modules.ComputeApi/Model/V1/VirtualNetwork.cs b/src/modules/src/Eryph.Modules.ComputeApi/Model/V1/VirtualNetwork.cs index 8cec7b409..f47f2ec78 100644 --- a/src/modules/src/Eryph.Modules.ComputeApi/Model/V1/VirtualNetwork.cs +++ b/src/modules/src/Eryph.Modules.ComputeApi/Model/V1/VirtualNetwork.cs @@ -14,5 +14,5 @@ public class VirtualNetwork public required string ProviderName { get; set; } - public required string IpNetwork { get; set; } + public string? IpNetwork { get; set; } } From 7f26f9d3c8ba42faf0a5132e34720f265fa1684a Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Thu, 21 Nov 2024 10:59:33 +0100 Subject: [PATCH 16/30] Improve externalnet switch configuration --- .../Networks/ProjectNetworkPlanBuilder.cs | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs b/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs index b3c2de264..fee9a8bd0 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs @@ -4,6 +4,7 @@ using System.Net; using System.Threading; using Dbosoft.OVN; +using Dbosoft.OVN.Model.OVN; using Eryph.Core; using Eryph.Core.Network; using Eryph.StateDb; @@ -49,7 +50,7 @@ from floatingPorts in GetFloatingPorts(catletPorts, providerSubnets) let dnsNames = dnsInfo.Select(info => info.CatletMetadataId).ToArray() let p1 = AddProjectRouterAndPorts(networkPlan, networks) - let p2 = AddExternalNetSwitches(p1, providerSubnets, overLayProviders) + from p2 in AddExternalNetSwitches(p1, providerSubnets, overLayProviders) let p3 = AddProviderRouterPorts(p2, providerRouterPorts) let p4 = AddNetworksAsSwitches(p3, networks, dnsNames) let p5 = AddSubnetsAsDhcpOptions(p4, networks) @@ -169,19 +170,51 @@ private static NetworkPlan AddProviderRouterPorts(NetworkPlan networkPlan, Seq

subnets, Seq overlayProviders) + private static EitherAsync AddExternalNetSwitches( + NetworkPlan networkPlan, + Seq subnets, + Seq overlayProviders) => + from plans in subnets.Map(x => x.Subnet.ProviderName).Distinct() + .Map(providerName => AddExternalNetSwitch(networkPlan, providerName, overlayProviders)) + .SequenceSerial() + select JoinPlans(plans, networkPlan); + + private static EitherAsync AddExternalNetSwitch( + NetworkPlan networkPlan, + string providerName, + Seq overlayProvider) => + from provider in overlayProvider.Find(x => x.Name == providerName) + .ToEitherAsync(Error.New($"Network provider {providerName} not found.")) + let switchName = $"externalNet-{networkPlan.Id}-{provider.Name}" + let p1 = networkPlan.AddSwitch(switchName) + let p2 = AddExternalNetworkPortUnique( + p1, switchName, provider.Name, provider.BridgeName, provider.Vlan) + select p2; + + private static NetworkPlan AddExternalNetworkPortUnique( + NetworkPlan plan, + string switchName, + string externalNetwork, + string bridgeName, + int? tag) { - return (from providerNames in subnets.Select(x => x.Subnet.ProviderName).Distinct() - from provider in overlayProviders.Where(p => providerNames.Contains(p.Name)) - let p1 = networkPlan.AddSwitch($"externalNet-{networkPlan.Id}-{provider.Name}") - let p2 = p1.AddExternalNetworkPort($"externalNet-{networkPlan.Id}-{provider.Name}", - provider.Name, provider.Vlan) + var name = $"SN-{switchName}-{externalNetwork}-{bridgeName}"; - select p2).Apply(s => JoinPlans(s, networkPlan)); + return plan with + { + PlannedSwitchPorts = + plan.PlannedSwitchPorts.Add(name, new PlannedSwitchPort(switchName) + { + Name = name, + Type = "localnet", + Options = new Dictionary { { "network_name", externalNetwork } }.ToMap(), + Addresses = new[] { "unknown" }.ToSeq(), + ExternalIds = new Dictionary { { "network_plan", plan.Id } }.ToMap(), + Tag = tag + }) + }; } - private static NetworkPlan AddNetworksAsSwitches(NetworkPlan networkPlan, Seq networks, string[] dnsNames) => networks.Map(network => networkPlan.AddSwitch(network.Id.ToString(), dnsNames)) From 8d635b55074225e9688a3554b9c9e271e8d3eaad Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Thu, 21 Nov 2024 12:39:53 +0100 Subject: [PATCH 17/30] Fix incorrect network definitions in tests --- .../Networks/CatletIpManagerTests.cs | 2 +- .../Networks/ProviderIpManagerTests.cs | 2 +- .../Networks/UpdateCatletNetworksCommandHandlerTests.cs | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs index 189cb3a3f..479ba93fa 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs @@ -264,7 +264,7 @@ await stateStore.For().AddAsync( { Id = Guid.Parse(DefaultSubnetId), Name = EryphConstants.DefaultSubnetName, - IpNetwork = "10.0.0.0/16", + IpNetwork = "10.0.0.0/15", IpPools = [ new IpPool() diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs index 137ddcb2c..346ca5945 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs @@ -224,7 +224,7 @@ await stateStore.For().AddAsync( Id = Guid.Parse(DefaultSubnetId), ProviderName = EryphConstants.DefaultProviderName, Name = EryphConstants.DefaultSubnetName, - IpNetwork = "10.0.0.0/16", + IpNetwork = "10.0.0.0/15", IpPools = [ new IpPool() diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs index 8bd2b18ed..876159008 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs @@ -662,7 +662,7 @@ protected override void AddSimpleInjector(SimpleInjectorAddOptions options) options.Container.RegisterInstance(_taskMessingMock.Object); options.Container.RegisterInstance(_networkProviderManagerMock.Object); - // Use the proper managers instead of mocks.The code is quite + // Use the proper managers instead of mocks. The code is quite // interdependent as it modifies the same EF Core entities. options.Container.Register(Lifestyle.Scoped); options.Container.Register(Lifestyle.Scoped); @@ -777,7 +777,7 @@ await stateStore.For().AddAsync( { Id = Guid.Parse(DefaultSubnetId), Name = EryphConstants.DefaultSubnetName, - IpNetwork = "10.0.0.0/16", + IpNetwork = "10.0.0.0/15", IpPools = [ new IpPool() @@ -840,6 +840,7 @@ await stateStore.For().AddAsync( Name = "second-network", Environment = EryphConstants.DefaultEnvironmentName, NetworkProvider = EryphConstants.DefaultProviderName, + IpNetwork = "10.5.0.0/16", Subnets = [ new VirtualNetworkSubnet @@ -882,6 +883,7 @@ await stateStore.For().AddAsync( Name = "third-network", Environment = EryphConstants.DefaultEnvironmentName, NetworkProvider = EryphConstants.DefaultProviderName, + IpNetwork = "10.6.0.0/16", Subnets = [ new VirtualNetworkSubnet @@ -924,6 +926,7 @@ await stateStore.For().AddAsync( Name = EryphConstants.DefaultNetworkName, Environment = "second-environment", NetworkProvider = EryphConstants.DefaultProviderName, + IpNetwork = "10.10.0.0/16", Subnets = [ new VirtualNetworkSubnet @@ -966,6 +969,7 @@ await stateStore.For().AddAsync( Name = EryphConstants.DefaultNetworkName, Environment = EryphConstants.DefaultEnvironmentName, NetworkProvider = EryphConstants.DefaultProviderName, + IpNetwork = "10.200.0.0/16", Subnets = [ new VirtualNetworkSubnet From b1fc7a805ef34bf9a319f6830beb6ef8e55c267b Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Fri, 22 Nov 2024 14:25:03 +0100 Subject: [PATCH 18/30] Move provider IP acquisition to NetworkConfigRealizer --- .../Specifications/ProviderSubnetSpecs.cs | 8 + .../Specifications/VirtualNetworkSpecs.cs | 2 +- .../Networks/NetworkConfigRealizer.cs | 105 ++++++++----- .../Networks/ProjectNetworkPlanBuilder.cs | 140 +++++++----------- .../Networks/NetworkConfigRealizerTests.cs | 6 +- 5 files changed, 132 insertions(+), 129 deletions(-) diff --git a/src/data/src/Eryph.StateDb/Specifications/ProviderSubnetSpecs.cs b/src/data/src/Eryph.StateDb/Specifications/ProviderSubnetSpecs.cs index 3e4bb51a0..478a2261c 100644 --- a/src/data/src/Eryph.StateDb/Specifications/ProviderSubnetSpecs.cs +++ b/src/data/src/Eryph.StateDb/Specifications/ProviderSubnetSpecs.cs @@ -17,4 +17,12 @@ public GetForChangeTracking() Query.Include(s => s.IpPools); } } + + public sealed class GetByName : Specification, ISingleResultSpecification + { + public GetByName(string providerName, string subnetName) + { + Query.Where(s => s.ProviderName == providerName && s.Name == subnetName); + } + } } diff --git a/src/data/src/Eryph.StateDb/Specifications/VirtualNetworkSpecs.cs b/src/data/src/Eryph.StateDb/Specifications/VirtualNetworkSpecs.cs index 55e513133..6a78c7ec3 100644 --- a/src/data/src/Eryph.StateDb/Specifications/VirtualNetworkSpecs.cs +++ b/src/data/src/Eryph.StateDb/Specifications/VirtualNetworkSpecs.cs @@ -20,7 +20,7 @@ public sealed class GetForProjectConfig : Specification public GetForProjectConfig(Guid projectId) { Query.Where(x => x.ProjectId == projectId) - .Include(x => x.NetworkPorts) + .Include(x => x.NetworkPorts).ThenInclude(x => x.IpAssignments) .Include(x => x.Subnets) .Include(x => x.RouterPort).ThenInclude(x => x!.FloatingPort) .Include(x => x.Subnets) diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs index c41132697..dab5e8663 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs @@ -3,7 +3,9 @@ using System.Linq; using System.Net; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; +using Dbosoft.Rebus; using Eryph.ConfigModel.Networks; using Eryph.Core; using Eryph.Core.Network; @@ -11,21 +13,18 @@ using Eryph.StateDb; using Eryph.StateDb.Model; using Eryph.StateDb.Specifications; +using LanguageExt; +using LanguageExt.UnsafeValueAccess; using Microsoft.Extensions.Logging; namespace Eryph.Modules.Controller.Networks; -public class NetworkConfigRealizer : INetworkConfigRealizer +public class NetworkConfigRealizer( + IStateStore stateStore, + IIpPoolManager ipPoolManager, + ILogger log) + : INetworkConfigRealizer { - private readonly IStateStore _stateStore; - private readonly ILogger _log; - - public NetworkConfigRealizer(IStateStore stateStore, ILogger log) - { - _stateStore = stateStore; - _log = log; - } - private static string GetEnvironmentName(string? environment, string network) { return environment == null @@ -35,7 +34,7 @@ private static string GetEnvironmentName(string? environment, string network) public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, NetworkProvidersConfiguration providerConfig) { - var savedNetworks = await _stateStore + var savedNetworks = await stateStore .For() .ListAsync(new VirtualNetworkSpecs.GetForProjectConfig(projectId)); @@ -43,14 +42,14 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne var networkConfigs = config.Networks?.Where(x=>x.Name!= null).ToArray() ?? Array.Empty(); foreach (var networkConfig in networkConfigs) { - if(networkConfig.Name == null) + if (networkConfig.Name == null) continue; var networkEnvName = GetEnvironmentName(networkConfig.Environment, networkConfig.Name); var savedNetwork = savedNetworks.Find(x => GetEnvironmentName(x.Environment, x.Name) == networkEnvName); if (savedNetwork == null) { - _log.LogDebug("Environment {env}: network {network} not found. Creating new network.", + log.LogDebug("Environment {env}: network {network} not found. Creating new network.", networkConfig.Environment ?? "default", networkConfig.Name); var newNetwork = new VirtualNetwork { @@ -63,7 +62,7 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne }; savedNetworks.Add(newNetwork); - await _stateStore.For().AddAsync(newNetwork); + await stateStore.For().AddAsync(newNetwork); } foundNames.Add(networkEnvName); @@ -72,14 +71,14 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne var removeNetworks = savedNetworks.Where(x => !foundNames.Contains(GetEnvironmentName(x.Environment,x.Name))).ToArray(); if (removeNetworks.Any()) - _log.LogDebug("Removing networks: {@removedNetworks}", (object)removeNetworks); + log.LogDebug("Removing networks: {@removedNetworks}", (object)removeNetworks); - await _stateStore.For().DeleteRangeAsync(removeNetworks); + await stateStore.For().DeleteRangeAsync(removeNetworks); // second pass - update of new or existing records foreach (var networkConfig in networkConfigs.DistinctBy(x => GetEnvironmentName(x.Environment,x.Name!))) { - if(networkConfig.Name == null) // can't happen + if (networkConfig.Name == null) // can't happen continue; var networkEnvName = GetEnvironmentName(networkConfig.Environment, networkConfig.Name); @@ -89,7 +88,7 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne var providerSubnet = networkConfig.Provider?.Subnet ?? "default"; var providerIpPool = networkConfig.Provider?.IpPool ?? "default"; - _log.LogDebug("Environment {env}: Updating network {network}", savedNetwork.Environment ?? "default", savedNetwork.Name); + log.LogDebug("Environment {env}: Updating network {network}", savedNetwork.Environment ?? "default", savedNetwork.Name); var networkProvider = providerConfig.NetworkProviders.FirstOrDefault(x => x.Name == providerName) ?? throw new InconsistentNetworkConfigException($"Network provider {providerName} not found."); @@ -108,14 +107,14 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne if (port is NetworkRouterPort or ProviderRouterPort) savedNetwork.NetworkPorts.Remove(port); - await _stateStore.LoadCollectionAsync(port, x => x.IpAssignments); + await stateStore.LoadCollectionAsync(port, x => x.IpAssignments); port.IpAssignments.Clear(); } continue; } - if(string.IsNullOrWhiteSpace(networkConfig.Address)) + if (string.IsNullOrWhiteSpace(networkConfig.Address)) throw new InconsistentNetworkConfigException($"Network '{networkConfig.Name}': Network address not set."); var networkAddress = IPNetwork2.Parse(networkConfig.Address); @@ -126,9 +125,9 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne //remove all ports if more than one if (providerPorts.Length > 1) { - _log.LogWarning("Found invalid provider port count ({count} provider ports) for {network}. Removing all provider ports.", providerPorts.Length, savedNetwork.Name); + log.LogWarning("Found invalid provider port count ({count} provider ports) for {network}. Removing all provider ports.", providerPorts.Length, savedNetwork.Name); - await _stateStore.For().DeleteRangeAsync(providerPorts); + await stateStore.For().DeleteRangeAsync(providerPorts); providerPorts = Array.Empty(); } @@ -139,7 +138,7 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne providerPort.SubnetName != providerSubnet || providerPort.PoolName != providerIpPool) { - _log.LogInformation("Network {network}: network provider settings changed.", savedNetwork.Name); + log.LogInformation("Network {network}: network provider settings changed.", savedNetwork.Name); savedNetwork.NetworkPorts.Remove(providerPort); providerPort = null; @@ -148,7 +147,7 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne if (providerPort == null) { - savedNetwork.NetworkPorts.Add(new ProviderRouterPort() + providerPort = new ProviderRouterPort() { Name = "provider", SubnetName = providerSubnet, @@ -156,7 +155,16 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne MacAddress = MacAddresses.FormatMacAddress( MacAddresses.GenerateMacAddress(Guid.NewGuid().ToString())), ProviderName = providerName, - }); + IpAssignments = [], + }; + + savedNetwork.NetworkPorts.Add(providerPort); + } + + if (providerPort.IpAssignments.Count == 0) + { + var ipAssignment = await AcquireProviderIp(providerName, providerSubnet, providerIpPool); + providerPort.IpAssignments.Add(ipAssignment); } var routerPorts = savedNetwork.NetworkPorts @@ -165,9 +173,9 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne //remove all ports if more then one if (routerPorts.Length > 1) { - _log.LogWarning("Found invalid router port count ({count} router ports) for {network}. Removing all router ports.", routerPorts.Length, savedNetwork.Name); + log.LogWarning("Found invalid router port count ({count} router ports) for {network}. Removing all router ports.", routerPorts.Length, savedNetwork.Name); - await _stateStore.For().DeleteRangeAsync(routerPorts); + await stateStore.For().DeleteRangeAsync(routerPorts); routerPorts = Array.Empty(); } @@ -182,14 +190,14 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne if (routerPort != null) { - await _stateStore.LoadCollectionAsync(routerPort, x => x.IpAssignments); + await stateStore.LoadCollectionAsync(routerPort, x => x.IpAssignments); var ipAssignment = routerPort.IpAssignments.FirstOrDefault(); if (ipAssignment == null || !IPAddress.TryParse(ipAssignment.IpAddress, out var ipAddress) || !networkAddress.Contains(ipAddress)) { - _log.LogInformation("Environment {env}, Network {network}: network router ip assignment changed to {ipAddress}.", + log.LogInformation("Environment {env}, Network {network}: network router ip assignment changed to {ipAddress}.", savedNetwork.Environment ?? "default", savedNetwork.Name, networkAddress.FirstUsable); savedNetwork.NetworkPorts.Remove(routerPort); @@ -259,7 +267,7 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne subnetConfig.Name); if (savedSubnet == null) { - _log.LogDebug("subnet {network}/{subnet} not found. Creating new subnet.", networkConfig.Name, subnetConfig.Name); + log.LogDebug("subnet {network}/{subnet} not found. Creating new subnet.", networkConfig.Name, subnetConfig.Name); savedNetwork.Subnets.Add(new VirtualNetworkSubnet { @@ -274,16 +282,16 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne .Where(x => !foundNames.Contains(x.Name)).ToArray(); if (removeSubnets.Any()) - _log.LogDebug("Removing subnets: {@removeSubnets}", (object)removeSubnets); + log.LogDebug("Removing subnets: {@removeSubnets}", (object)removeSubnets); - await _stateStore.For().DeleteRangeAsync(removeSubnets); + await stateStore.For().DeleteRangeAsync(removeSubnets); foreach (var subnetConfig in networkConfig.Subnets.DistinctBy(x => x.Name).ToArray()) { var savedSubnet = savedNetwork.Subnets.FirstOrDefault(x => x.Name == subnetConfig.Name) ?? throw new InconsistentNetworkConfigException($"Subnet {subnetConfig} not found in network {savedNetwork.Name}"); - _log.LogDebug("Updating subnet {network}/{subnet}", savedNetwork.Name, savedSubnet.Name); + log.LogDebug("Updating subnet {network}/{subnet}", savedNetwork.Name, savedSubnet.Name); savedSubnet.DhcpLeaseTime = 3600; savedSubnet.MTU = subnetConfig.Mtu.GetValueOrDefault() == 0 ? 1400 : subnetConfig.Mtu.GetValueOrDefault(); @@ -321,7 +329,7 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne // change of ip pool is allowed if validation has passed (enough space in pool or unused) if (savedIpPool != null) { - _log.LogDebug("Updating ip pool {network}/{subnet}/{pool}", savedNetwork.Name, + log.LogDebug("Updating ip pool {network}/{subnet}/{pool}", savedNetwork.Name, savedSubnet.Name, savedIpPool.Name); savedIpPool.IpNetwork = savedSubnet.IpNetwork; @@ -336,7 +344,7 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne if (savedIpPool == null) { - _log.LogDebug("creating new ip pool {network}/{subnet}/{pool}", savedNetwork.Name, + log.LogDebug("creating new ip pool {network}/{subnet}/{pool}", savedNetwork.Name, savedSubnet.Name, ipPoolConfig.Name); savedSubnet.IpPools.Add(new IpPool @@ -351,10 +359,31 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne } var removeIpPools = savedSubnet.IpPools.Where(x => !foundNames.Contains(x.Name)); - await _stateStore.For().DeleteRangeAsync(removeIpPools); + await stateStore.For().DeleteRangeAsync(removeIpPools); } } - } -} \ No newline at end of file + private async Task AcquireProviderIp( + string providerName, + string subnetName, + string poolName) + { + var providerSubnet = await stateStore.Read().GetBySpecAsync( + new ProviderSubnetSpecs.GetByName(providerName, subnetName)); + if (providerSubnet is null) + { + throw new InconsistentNetworkConfigException( + $"Subnet {subnetName} of provider {providerName} not found."); + } + + var result = await ipPoolManager.AcquireIp(providerSubnet.Id, poolName); + if (result.IsLeft) + { + throw new InconsistentNetworkConfigException( + $"Failed to configure IP for provider {providerName}: {result.LeftToSeq().Head.Message}"); + } + + return result.ValueUnsafe(); + } +} diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs b/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs index fee9a8bd0..8b501d989 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs @@ -11,12 +11,13 @@ using Eryph.StateDb.Model; using LanguageExt; using LanguageExt.Common; -using YamlDotNet.Core.Tokens; +using static LanguageExt.Prelude; namespace Eryph.Modules.Controller.Networks; -internal class ProjectNetworkPlanBuilder(INetworkProviderManager networkProviderManager, IStateStore stateStore, - IIpPoolManager ipPoolManager) +internal class ProjectNetworkPlanBuilder( + INetworkProviderManager networkProviderManager, + IStateStore stateStore) : IProjectNetworkPlanBuilder { @@ -30,7 +31,6 @@ private sealed record CatletDnsInfo(string CatletMetadataId, Map public EitherAsync GenerateNetworkPlan(Guid projectId, CancellationToken cancellationToken) { - var networkPlan = new NetworkPlan(projectId.ToString()); return from providerConfig in networkProviderManager.GetCurrentConfiguration() @@ -42,7 +42,7 @@ public EitherAsync GenerateNetworkPlan(Guid projectId, Cance from networks in GetAllOverlayNetworks(projectId, overLayProviders, cancellationToken) from providerSubnets in GetProviderSubnets(overLayProviders, networks, cancellationToken) - from providerRouterPorts in GetProviderRouterPorts(networks, providerSubnets, cancellationToken) + from providerRouterPorts in GetProviderRouterPorts(networks, providerSubnets) let catletPorts = FindPortsOfType(networks) from floatingPorts in GetFloatingPorts(catletPorts, providerSubnets) @@ -51,38 +51,22 @@ from floatingPorts in GetFloatingPorts(catletPorts, providerSubnets) let p1 = AddProjectRouterAndPorts(networkPlan, networks) from p2 in AddExternalNetSwitches(p1, providerSubnets, overLayProviders) - let p3 = AddProviderRouterPorts(p2, providerRouterPorts) + from p3 in AddProviderRouterPorts(p2, providerRouterPorts) let p4 = AddNetworksAsSwitches(p3, networks, dnsNames) let p5 = AddSubnetsAsDhcpOptions(p4, networks) let p6 = AddCatletPorts(p5, catletPorts) let p7 = AddFloatingPorts(p6, floatingPorts) let p8 = AddDnsNames(p7, dnsInfo) select p8; - } private EitherAsync> GetProviderRouterPorts( Seq networks, - Seq providerSubnets, - CancellationToken cancellationToken) => - from portInfos in networks.SelectMany( + Seq providerSubnets) => + from _ in RightAsync(unit) + let portInfos = networks.SelectMany( network => FindPortsOfType(network.NetworkPorts), (network, port) => (Network: network, Port: port)) - // Using Map() and SequenceSerial() here causes failures as EF Core detects - // parallel operations. - .Apply(l => Prelude.TryAsync(async () => - { - var result = new List<(VirtualNetwork Network, ProviderRouterPort Port)>(); - foreach (var (network, port) in l) - { - result.Add(port.IpAssignments.Count == 0 - ? await AcquireRouterPortIpAddress(port, network, providerSubnets, cancellationToken) - .Map(p => (network, p)) - .IfLeft(e => e.ToException().Rethrow<(VirtualNetwork Network, ProviderRouterPort Port)>()) - : (network, port)); - } - return result.ToSeq(); - }).ToEither()) // find and add provider subnet from portInfos2 in portInfos.Map(portInfo => providerSubnets .Find(x => x.Subnet.ProviderName == portInfo.Network.NetworkProvider && x.Subnet.Name == portInfo.Port.SubnetName) @@ -148,27 +132,38 @@ private EitherAsync> GetAllOverlayNetworks(Guid proje s.Filter(x => overLayProviders.Any(p => p.Name == x.NetworkProvider))); - private static NetworkPlan AddProviderRouterPorts(NetworkPlan networkPlan, Seq ports) - { - return ports.Map(portInfo => - { - var externalIpAddress = portInfo.Port.IpAssignments.First().IpAddress.Apply(IPAddress.Parse); - var externalNetwork = IPNetwork2.Parse(portInfo.Subnet.Subnet.IpNetwork); - - return networkPlan.AddRouterPort($"externalNet-{networkPlan.Id}-{portInfo.Network.NetworkProvider}", - $"project-{networkPlan.Id}", - portInfo.Port.MacAddress, externalIpAddress, externalNetwork, "local") - - .AddNATRule($"project-{networkPlan.Id}", "snat", - externalIpAddress, "", portInfo.Network.IpNetwork) - - .AddStaticRoute($"project-{networkPlan.Id}", "0.0.0.0/0", - IPAddress.Parse(portInfo.Subnet.Config.Gateway)); - - }) - .Apply(s => JoinPlans(s, networkPlan)); + private static EitherAsync AddProviderRouterPorts( + NetworkPlan networkPlan, + Seq ports) => + from plans in ports.Map(portInfo => AddProviderRouterPort(networkPlan, portInfo)) + .SequenceSerial() + select JoinPlans(plans, networkPlan); - } + private static EitherAsync AddProviderRouterPort( + NetworkPlan networkPlan, + ProviderRouterPortInfo portInfo) => + from externalIpAssignment in portInfo.Port.IpAssignments + .ToSeq().HeadOrNone() + .ToEitherAsync(Error.New($"The provider port for provider '' has no IP Address assigned.")) + from externalNetwork in Try(() => IPNetwork2.Parse(portInfo.Subnet.Subnet.IpNetwork)) + .ToEither(_ => Error.New($"The provider port for provider '' has an invalid IP Network assigned.")) + .ToAsync() + from parsedExternalIp in Try(() => IPAddress.Parse(externalIpAssignment.IpAddress)) + .ToEither(_ => Error.New($"The provider port for provider '' has an invalid IP Address assigned.")) + .ToAsync() + from gatewayIp in Try(() => IPAddress.Parse(portInfo.Subnet.Config.Gateway)) + .ToEither(_ => Error.New($"The provider port for provider '' has an invalid gateway IP Address assigned.")) + .ToAsync() + let updatedPlan = networkPlan + .AddRouterPort($"externalNet-{networkPlan.Id}-{portInfo.Network.NetworkProvider}", + $"project-{networkPlan.Id}", + portInfo.Port.MacAddress, parsedExternalIp, externalNetwork, "local") + + .AddNATRule($"project-{networkPlan.Id}", "snat", + parsedExternalIp, "", portInfo.Network.IpNetwork) + + .AddStaticRoute($"project-{networkPlan.Id}", "0.0.0.0/0", gatewayIp) + select updatedPlan; private static EitherAsync AddExternalNetSwitches( NetworkPlan networkPlan, @@ -207,9 +202,9 @@ private static NetworkPlan AddExternalNetworkPortUnique( { Name = name, Type = "localnet", - Options = new Dictionary { { "network_name", externalNetwork } }.ToMap(), - Addresses = new[] { "unknown" }.ToSeq(), - ExternalIds = new Dictionary { { "network_plan", plan.Id } }.ToMap(), + Options = Map(("network_name", externalNetwork)), + Addresses = Seq1("unknown"), + ExternalIds = Map(("network_plan", plan.Id)), Tag = tag }) }; @@ -227,19 +222,15 @@ private static NetworkPlan AddSubnetsAsDhcpOptions(NetworkPlan networkPlan, Seq< from subnet in network.Subnets let p1 = networkPlan.AddDHCPOptions( subnet.Id.ToString(), IPNetwork2.Parse(subnet.IpNetwork), - new Map( - new [] - { - ("server_id", networkIp.IpAddress ), - ("server_mac", network.RouterPort.MacAddress ), - ("lease_time", subnet.DhcpLeaseTime == 0 ? "3600" : subnet.DhcpLeaseTime.ToString() ), - ("mtu", subnet.MTU == 0 ? "1400" : subnet.MTU.ToString() ), - ("dns_server", string.IsNullOrWhiteSpace(subnet.DnsServersV4) ? "9.9.9.9" : - $"{{{string.Join(',', subnet.DnsServersV4.Split(','))}}}"), - ("router", networkIp.IpAddress ), - ("domain_name", $"\\\\\\\"{subnet.DnsDomain}\\\\\\\"") - - } + Map( + ("server_id", networkIp.IpAddress ), + ("server_mac", network.RouterPort.MacAddress ), + ("lease_time", subnet.DhcpLeaseTime == 0 ? "3600" : subnet.DhcpLeaseTime.ToString() ), + ("mtu", subnet.MTU == 0 ? "1400" : subnet.MTU.ToString() ), + ("dns_server", string.IsNullOrWhiteSpace(subnet.DnsServersV4) ? "9.9.9.9" : + $"{{{string.Join(',', subnet.DnsServersV4.Split(','))}}}"), + ("router", networkIp.IpAddress ), + ("domain_name", $"\\\\\\\"{subnet.DnsDomain}\\\\\\\"") ) ) select p1) @@ -277,7 +268,7 @@ private static NetworkPlan AddDnsNames(NetworkPlan networkPlan, // DNS requests (A, AAAA, ANY) for these records when it cannot resolve // them itself. This prevents DNS requests from being forwarded when // only IPv4 or IPv6 is configured. - Prelude.Map(("ovn-owned", "true")))) + Map(("ovn-owned", "true")))) .Apply(s => JoinPlans(s, networkPlan)); @@ -297,7 +288,6 @@ private static NetworkPlan AddFloatingPorts(NetworkPlan networkPlan, Seq JoinPlans(s, networkPlan)); - } private static NetworkPlan AddProjectRouterAndPorts(NetworkPlan networkPlan, Seq networks) @@ -315,7 +305,6 @@ private static NetworkPlan AddProjectRouterAndPorts(NetworkPlan networkPlan, Seq $"project-{networkPlan.Id}", network.RouterPort.MacAddress, network.RouterPort.IpAssignments!.First().IpAddress.Apply(IPAddress.Parse), ipNetwork); }).Apply(s => JoinPlans(s, networkPlan)); - } private static Seq FindPortsOfType(Seq networks) where T : VirtualNetworkPort @@ -332,29 +321,6 @@ private static Seq FindPortsOfType(IEnumerable ports) } - private EitherAsync AcquireRouterPortIpAddress(ProviderRouterPort port, VirtualNetwork network, - Seq providerSubnets, CancellationToken cancellationToken) - { - return from providerSubnet in providerSubnets - .Find(x => x.Subnet.ProviderName == network.NetworkProvider && x.Subnet.Name == port.SubnetName) - .ToEitherAsync(Error.New( - $"Network '{network.Name}' configuration error: subnet {port.SubnetName} of network provider {network.NetworkProvider} not found.")) - from ip in ipPoolManager.AcquireIp(providerSubnet.Subnet.Id, port.PoolName, cancellationToken) - let _ = AddIpToPort(port, ip) - select port; - } - - - - private static NetworkPort AddIpToPort(NetworkPort port, IpAssignment ipAssignment) - { - port.IpAssignments ??= new List(); - port.IpAssignments.Add(ipAssignment); - return port; - } - - - private static NetworkPlan JoinPlans(Seq plans, NetworkPlan basePlan) { var result = new NetworkPlan(basePlan.Id); diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs index 5d63fe537..e8639c0e4 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs @@ -86,7 +86,7 @@ public async Task Creates_new_overlay_network() } }; - var realizer = new NetworkConfigRealizer(stateStore.Object, logger); + var realizer = new NetworkConfigRealizer(stateStore.Object, Mock.Of(), logger); await realizer.UpdateNetwork(projectId, networkConfig, networkProviderConfig); addedNetworks.Should().HaveCount(1); @@ -227,7 +227,7 @@ public async Task Existing_ip_pool_is_updated() } }; - var realizer = new NetworkConfigRealizer(stateStore.Object, logger); + var realizer = new NetworkConfigRealizer(stateStore.Object, Mock.Of(), logger); await realizer.UpdateNetwork(projectId, networkConfig, networkProviderConfig); network.Subnets![0].IpPools![0].FirstIp.Should().Be("10.0.0.50"); @@ -353,7 +353,7 @@ public async Task Cleanup_of_overlay_when_switched_to_flat() } }; - var realizer = new NetworkConfigRealizer(stateStore.Object, logger); + var realizer = new NetworkConfigRealizer(stateStore.Object, Mock.Of(), logger); await realizer.UpdateNetwork(projectId, networkConfig, networkProviderConfig); network.Subnets.Should().HaveCount(0); From 55b7d1a617a634f67e4ce071906cba8caa16d9c0 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Fri, 22 Nov 2024 15:41:32 +0100 Subject: [PATCH 19/30] Enable logging to XUnit test output --- .../Eryph.StateDb.TestBase.csproj | 1 + .../InMemoryStateDbTestBase.cs | 4 ++- .../Eryph.StateDb.TestBase/StateDbTestBase.cs | 8 ++++-- .../GeneInventoryQueriesTests.cs | 21 +++++++++----- .../Endpoints/Genes/CleanupGenesTests.cs | 6 +++- .../Endpoints/Genes/GetGeneTests.cs | 7 +++-- .../Endpoints/Genes/ListGenesTests.cs | 6 +++- .../Endpoints/Genes/RemoveGeneTests.cs | 28 +++++++++---------- .../Endpoints/Operations/GetOperationTest.cs | 6 +++- .../Projects/AddProjectMemberTests.cs | 6 +++- .../Endpoints/Projects/CreateProjectTests.cs | 6 +++- .../Endpoints/Projects/GetProjectTests.cs | 6 +++- .../VirtualDisks/CreateVirtualDiskTests.cs | 6 +++- .../VirtualDisks/DeleteVirtualDiskTests.cs | 6 +++- .../CatletMetadataChangeTrackingTests.cs | 21 +++++++++----- .../ChangeTracking/ChangeTrackingTestBase.cs | 7 +++-- .../NetworkProvidersChangeTrackingTests.cs | 19 +++++++++---- .../ProjectChangeTrackingTests.cs | 21 +++++++++----- .../VirtualNetworkChangeTrackingTests.cs | 21 +++++++++----- .../Networks/CatletIpManagerTests.cs | 5 +++- .../Networks/IpPoolManagerTests.cs | 5 +++- .../NetworkProvidersConfigRealizerTests.cs | 5 +++- .../Networks/ProviderIpManagerTests.cs | 5 +++- ...UpdateCatletNetworksCommandHandlerTests.cs | 5 +++- .../StateDbDeleteTests.cs | 18 ++++++++---- 25 files changed, 176 insertions(+), 73 deletions(-) diff --git a/src/data/test/Eryph.StateDb.TestBase/Eryph.StateDb.TestBase.csproj b/src/data/test/Eryph.StateDb.TestBase/Eryph.StateDb.TestBase.csproj index cda42b913..6c7441782 100644 --- a/src/data/test/Eryph.StateDb.TestBase/Eryph.StateDb.TestBase.csproj +++ b/src/data/test/Eryph.StateDb.TestBase/Eryph.StateDb.TestBase.csproj @@ -7,6 +7,7 @@ false + diff --git a/src/data/test/Eryph.StateDb.TestBase/InMemoryStateDbTestBase.cs b/src/data/test/Eryph.StateDb.TestBase/InMemoryStateDbTestBase.cs index 0722ad292..317820107 100644 --- a/src/data/test/Eryph.StateDb.TestBase/InMemoryStateDbTestBase.cs +++ b/src/data/test/Eryph.StateDb.TestBase/InMemoryStateDbTestBase.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using Xunit; +using Xunit.Abstractions; namespace Eryph.StateDb.TestBase; @@ -16,4 +17,5 @@ namespace Eryph.StateDb.TestBase; /// for every test as the fixture does not do anything besides /// providing the connection string. /// -public class InMemoryStateDbTestBase() : StateDbTestBase(new SqliteFixture()); +public class InMemoryStateDbTestBase(ITestOutputHelper outputHelper) + : StateDbTestBase(new SqliteFixture(), outputHelper); diff --git a/src/data/test/Eryph.StateDb.TestBase/StateDbTestBase.cs b/src/data/test/Eryph.StateDb.TestBase/StateDbTestBase.cs index e03bad489..d84e462bc 100644 --- a/src/data/test/Eryph.StateDb.TestBase/StateDbTestBase.cs +++ b/src/data/test/Eryph.StateDb.TestBase/StateDbTestBase.cs @@ -10,10 +10,12 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using SimpleInjector; using SimpleInjector.Integration.ServiceCollection; using SimpleInjector.Lifestyles; using Xunit; +using Xunit.Abstractions; namespace Eryph.StateDb.TestBase; @@ -30,7 +32,9 @@ public abstract class StateDbTestBase : IAsyncLifetime private readonly ServiceProvider _provider; private readonly Container _container; - protected StateDbTestBase(IDatabaseFixture databaseFixture) + protected StateDbTestBase( + IDatabaseFixture databaseFixture, + ITestOutputHelper outputHelper) { _databaseFixture = databaseFixture; _dbConnection = _databaseFixture.GetConnectionString($"test_{DateTime.UtcNow.Ticks}"); @@ -39,7 +43,7 @@ protected StateDbTestBase(IDatabaseFixture databaseFixture) var services = new ServiceCollection(); services.AddSimpleInjector(_container, options => { - options.Services.AddLogging(); + options.Services.AddLogging(loggingBuilder => loggingBuilder.AddXUnit(outputHelper)); ConfigureDatabase(options.Container); RegisterStateStore(options); options.AddLogging(); diff --git a/src/data/test/Eryph.StateDb.Tests/GeneInventoryQueriesTests.cs b/src/data/test/Eryph.StateDb.Tests/GeneInventoryQueriesTests.cs index 70a26ff1b..565809dff 100644 --- a/src/data/test/Eryph.StateDb.Tests/GeneInventoryQueriesTests.cs +++ b/src/data/test/Eryph.StateDb.Tests/GeneInventoryQueriesTests.cs @@ -8,20 +8,27 @@ using Eryph.Core.Genetics; using Eryph.StateDb.Model; using Eryph.StateDb.TestBase; +using Xunit.Abstractions; namespace Eryph.StateDb.Tests; [Trait("Category", "Docker")] [Collection(nameof(MySqlDatabaseCollection))] -public class MySqlGeneInventoryQueriesTests(MySqlFixture databaseFixture) - : GeneInventoryQueriesTests(databaseFixture); +public class MySqlGeneInventoryQueriesTests( + MySqlFixture databaseFixture, + ITestOutputHelper outputHelper) + : GeneInventoryQueriesTests(databaseFixture, outputHelper); [Collection(nameof(SqliteDatabaseCollection))] -public class SqliteGeneInventoryQueriesTests(SqliteFixture databaseFixture) - : GeneInventoryQueriesTests(databaseFixture); - -public abstract class GeneInventoryQueriesTests(IDatabaseFixture databaseFixture) - : StateDbTestBase(databaseFixture) +public class SqliteGeneInventoryQueriesTests( + SqliteFixture databaseFixture, + ITestOutputHelper outputHelper) + : GeneInventoryQueriesTests(databaseFixture, outputHelper); + +public abstract class GeneInventoryQueriesTests( + IDatabaseFixture databaseFixture, + ITestOutputHelper outputHelper) + : StateDbTestBase(databaseFixture, outputHelper) { private const string AgentName = "testhost"; diff --git a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/CleanupGenesTests.cs b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/CleanupGenesTests.cs index a7c8b57b2..8758ebb7f 100644 --- a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/CleanupGenesTests.cs +++ b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/CleanupGenesTests.cs @@ -11,6 +11,7 @@ using FluentAssertions; using System.Net; using Eryph.Messages.Genes.Commands; +using Xunit.Abstractions; namespace Eryph.Modules.ComputeApi.Tests.Integration.Endpoints.Genes; @@ -18,7 +19,10 @@ public class CleanupGenesTests : InMemoryStateDbTestBase, IClassFixture _factory; - public CleanupGenesTests(WebModuleFactory factory) + public CleanupGenesTests( + ITestOutputHelper outputHelper, + WebModuleFactory factory) + : base(outputHelper) { _factory = factory.WithApiHost(ConfigureDatabase); } diff --git a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/GetGeneTests.cs b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/GetGeneTests.cs index c9c4543d8..57325e279 100644 --- a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/GetGeneTests.cs +++ b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/GetGeneTests.cs @@ -16,7 +16,7 @@ using Eryph.StateDb.TestBase; using FluentAssertions; using Xunit; - +using Xunit.Abstractions; using ApiGene = Eryph.Modules.ComputeApi.Model.V1.GeneWithUsage; namespace Eryph.Modules.ComputeApi.Tests.Integration.Endpoints.Genes; @@ -33,7 +33,10 @@ public class GetGeneTests : InMemoryStateDbTestBase, IClassFixture factory) + public GetGeneTests( + ITestOutputHelper outputHelper, + WebModuleFactory factory) + : base(outputHelper) { _factory = factory.WithApiHost(ConfigureDatabase); } diff --git a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/ListGenesTests.cs b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/ListGenesTests.cs index 92dfde51f..9f92ad9a4 100644 --- a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/ListGenesTests.cs +++ b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/ListGenesTests.cs @@ -17,6 +17,7 @@ using Xunit; using ApiGene = Eryph.Modules.ComputeApi.Model.V1.Gene; +using Xunit.Abstractions; namespace Eryph.Modules.ComputeApi.Tests.Integration.Endpoints.Genes; @@ -28,7 +29,10 @@ public class ListGenesTests : InMemoryStateDbTestBase, IClassFixture factory) + public ListGenesTests( + ITestOutputHelper outputHelper, + WebModuleFactory factory) + : base(outputHelper) { _factory = factory.WithApiHost(ConfigureDatabase); } diff --git a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/RemoveGeneTests.cs b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/RemoveGeneTests.cs index d5e3be5e1..5f625a2e5 100644 --- a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/RemoveGeneTests.cs +++ b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Genes/RemoveGeneTests.cs @@ -1,22 +1,19 @@ -using Dbosoft.Hosuto.Modules.Testing; -using Eryph.StateDb.Model; -using Eryph.StateDb; -using Eryph.StateDb.TestBase; -using System; +using System; using System.Collections.Generic; using System.Linq; -using System.Net.Http.Json; using System.Text; +using System.Net; using System.Threading.Tasks; -using Eryph.Core.Genetics; -using Xunit; +using Dbosoft.Hosuto.Modules.Testing; using Eryph.Core; -using System.Text.Json.Serialization; -using System.Text.Json; -using Eryph.Messages.Resources.Catlets.Commands; -using FluentAssertions; -using System.Net; using Eryph.Messages.Genes.Commands; +using Eryph.StateDb.Model; +using Eryph.StateDb; +using Eryph.StateDb.TestBase; +using Eryph.Core.Genetics; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; namespace Eryph.Modules.ComputeApi.Tests.Integration.Endpoints.Genes; @@ -25,7 +22,10 @@ public class RemoveGeneTests : InMemoryStateDbTestBase, IClassFixture _factory; private static readonly Guid GeneId = Guid.NewGuid(); - public RemoveGeneTests(WebModuleFactory factory) + public RemoveGeneTests( + ITestOutputHelper outputHelper, + WebModuleFactory factory) + : base(outputHelper) { _factory = factory.WithApiHost(ConfigureDatabase); } diff --git a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Operations/GetOperationTest.cs b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Operations/GetOperationTest.cs index ce4b56efa..b4d31443f 100644 --- a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Operations/GetOperationTest.cs +++ b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Operations/GetOperationTest.cs @@ -11,6 +11,7 @@ using Eryph.StateDb.TestBase; using FluentAssertions; using Xunit; +using Xunit.Abstractions; using ApiOperation = Eryph.Modules.AspNetCore.ApiProvider.Model.V1.Operation; namespace Eryph.Modules.ComputeApi.Tests.Integration.Endpoints.Operations; @@ -27,7 +28,10 @@ public class GetOperationTest : InMemoryStateDbTestBase, private readonly WebModuleFactory _factory; - public GetOperationTest(WebModuleFactory factory) + public GetOperationTest( + ITestOutputHelper outputHelper, + WebModuleFactory factory) + : base(outputHelper) { _factory = factory.WithApiHost(ConfigureDatabase); } diff --git a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Projects/AddProjectMemberTests.cs b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Projects/AddProjectMemberTests.cs index 644e5a268..a4a1c2a4b 100644 --- a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Projects/AddProjectMemberTests.cs +++ b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Projects/AddProjectMemberTests.cs @@ -13,6 +13,7 @@ using Eryph.StateDb.TestBase; using FluentAssertions; using Xunit; +using Xunit.Abstractions; namespace Eryph.Modules.ComputeApi.Tests.Integration.Endpoints.Projects; @@ -22,7 +23,10 @@ public class AddProjectMemberTests : InMemoryStateDbTestBase, private static readonly Guid UserId = Guid.NewGuid(); private readonly WebModuleFactory _factory; - public AddProjectMemberTests(WebModuleFactory factory) + public AddProjectMemberTests( + ITestOutputHelper outputHelper, + WebModuleFactory factory) + : base(outputHelper) { _factory = factory.WithApiHost(ConfigureDatabase); } diff --git a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Projects/CreateProjectTests.cs b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Projects/CreateProjectTests.cs index 4a1ea399d..2ddac9f27 100644 --- a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Projects/CreateProjectTests.cs +++ b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Projects/CreateProjectTests.cs @@ -13,6 +13,7 @@ using Eryph.StateDb.TestBase; using FluentAssertions; using Xunit; +using Xunit.Abstractions; namespace Eryph.Modules.ComputeApi.Tests.Integration.Endpoints.Projects; @@ -22,7 +23,10 @@ public class CreateProjectTests : InMemoryStateDbTestBase, private static readonly Guid UserId = Guid.NewGuid(); private readonly WebModuleFactory _factory; - public CreateProjectTests(WebModuleFactory factory) + public CreateProjectTests( + ITestOutputHelper outputHelper, + WebModuleFactory factory) + : base(outputHelper) { _factory = factory.WithApiHost(ConfigureDatabase); } diff --git a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Projects/GetProjectTests.cs b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Projects/GetProjectTests.cs index be3c6d6e6..c54af3f3f 100644 --- a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Projects/GetProjectTests.cs +++ b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/Projects/GetProjectTests.cs @@ -11,6 +11,7 @@ using Eryph.StateDb.TestBase; using FluentAssertions; using Xunit; +using Xunit.Abstractions; using ApiProject = Eryph.Modules.AspNetCore.ApiProvider.Model.V1.Project; namespace Eryph.Modules.ComputeApi.Tests.Integration.Endpoints.Projects; @@ -21,7 +22,10 @@ public class GetProjectTests : InMemoryStateDbTestBase, private static readonly Guid UserId = Guid.NewGuid(); private readonly WebModuleFactory _factory; - public GetProjectTests(WebModuleFactory factory) + public GetProjectTests( + ITestOutputHelper outputHelper, + WebModuleFactory factory) + : base(outputHelper) { _factory = factory.WithApiHost(ConfigureDatabase); } diff --git a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/VirtualDisks/CreateVirtualDiskTests.cs b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/VirtualDisks/CreateVirtualDiskTests.cs index 11e7dab74..22cdc119f 100644 --- a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/VirtualDisks/CreateVirtualDiskTests.cs +++ b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/VirtualDisks/CreateVirtualDiskTests.cs @@ -15,6 +15,7 @@ using Eryph.StateDb.TestBase; using FluentAssertions; using Xunit; +using Xunit.Abstractions; namespace Eryph.Modules.ComputeApi.Tests.Integration.Endpoints.VirtualDisks; @@ -28,7 +29,10 @@ public class CreateVirtualDiskTests : InMemoryStateDbTestBase, IClassFixture factory) + public CreateVirtualDiskTests( + ITestOutputHelper outputHelper, + WebModuleFactory factory) + : base(outputHelper) { _factory = factory.WithApiHost(ConfigureDatabase); } diff --git a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/VirtualDisks/DeleteVirtualDiskTests.cs b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/VirtualDisks/DeleteVirtualDiskTests.cs index d75cf1345..9c4700d1b 100644 --- a/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/VirtualDisks/DeleteVirtualDiskTests.cs +++ b/src/modules/test/Eryph.Modules.ComputeApi.Tests/Integration/Endpoints/VirtualDisks/DeleteVirtualDiskTests.cs @@ -15,6 +15,7 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc; using Xunit; +using Xunit.Abstractions; namespace Eryph.Modules.ComputeApi.Tests.Integration.Endpoints.VirtualDisks; @@ -23,7 +24,10 @@ public class DeleteVirtualDiskTests : InMemoryStateDbTestBase, IClassFixture _factory; private static readonly Guid DiskId = Guid.NewGuid(); - public DeleteVirtualDiskTests(WebModuleFactory factory) + public DeleteVirtualDiskTests( + ITestOutputHelper outputHelper, + WebModuleFactory factory) + : base(outputHelper) { _factory = factory.WithApiHost(ConfigureDatabase); } diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/CatletMetadataChangeTrackingTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/CatletMetadataChangeTrackingTests.cs index c22ca46e5..d309f2c0d 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/CatletMetadataChangeTrackingTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/CatletMetadataChangeTrackingTests.cs @@ -7,20 +7,27 @@ using Eryph.StateDb; using Eryph.StateDb.Model; using Eryph.StateDb.TestBase; +using Xunit.Abstractions; namespace Eryph.Modules.Controller.Tests.ChangeTracking; [Trait("Category", "Docker")] [Collection(nameof(MySqlDatabaseCollection))] -public class MySqlCatletMetadataChangeTrackingTests(MySqlFixture databaseFixture) - : CatletMetadataChangeTrackingTests(databaseFixture); +public class MySqlCatletMetadataChangeTrackingTests( + MySqlFixture databaseFixture, + ITestOutputHelper outputHelper) + : CatletMetadataChangeTrackingTests(databaseFixture, outputHelper); [Collection(nameof(SqliteDatabaseCollection))] -public class SqliteCatletMetadataChangeTrackingTests(SqliteFixture databaseFixture) - : CatletMetadataChangeTrackingTests(databaseFixture); - -public abstract class CatletMetadataChangeTrackingTests(IDatabaseFixture databaseFixture) - : ChangeTrackingTestBase(databaseFixture) +public class SqliteCatletMetadataChangeTrackingTests( + SqliteFixture databaseFixture, + ITestOutputHelper outputHelper) + : CatletMetadataChangeTrackingTests(databaseFixture, outputHelper); + +public abstract class CatletMetadataChangeTrackingTests( + IDatabaseFixture databaseFixture, + ITestOutputHelper outputHelper) + : ChangeTrackingTestBase(databaseFixture, outputHelper) { private static readonly Guid MetadataId = Guid.NewGuid(); private static readonly Guid VmId = Guid.NewGuid(); diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/ChangeTrackingTestBase.cs b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/ChangeTrackingTestBase.cs index 5258ab65f..7ae07a616 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/ChangeTrackingTestBase.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/ChangeTrackingTestBase.cs @@ -11,11 +11,14 @@ using Moq; using SimpleInjector; using SimpleInjector.Lifestyles; +using Xunit.Abstractions; namespace Eryph.Modules.Controller.Tests.ChangeTracking; -public abstract class ChangeTrackingTestBase(IDatabaseFixture databaseFixture) - : StateDbTestBase(databaseFixture) +public abstract class ChangeTrackingTestBase( + IDatabaseFixture databaseFixture, + ITestOutputHelper outputHelper) + : StateDbTestBase(databaseFixture, outputHelper) { protected readonly MockFileSystem MockFileSystem = new(); protected readonly Mock MockNetworkProviderManager = new(); diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/NetworkProvidersChangeTrackingTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/NetworkProvidersChangeTrackingTests.cs index fe6afb442..d0821cf74 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/NetworkProvidersChangeTrackingTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/NetworkProvidersChangeTrackingTests.cs @@ -11,19 +11,23 @@ using LanguageExt.Common; using Moq; using SimpleInjector.Integration.ServiceCollection; - +using Xunit.Abstractions; using static LanguageExt.Prelude; namespace Eryph.Modules.Controller.Tests.ChangeTracking; [Trait("Category", "Docker")] [Collection(nameof(MySqlDatabaseCollection))] -public class MySqlNetworkProvidersChangeTrackingTests(MySqlFixture databaseFixture) - : NetworkProvidersChangeTrackingTests(databaseFixture); +public class MySqlNetworkProvidersChangeTrackingTests( + MySqlFixture databaseFixture, + ITestOutputHelper outputHelper) + : NetworkProvidersChangeTrackingTests(databaseFixture, outputHelper); [Collection(nameof(SqliteDatabaseCollection))] -public class SqliteNetworkProvidersChangeTrackingTests(SqliteFixture databaseFixture) - : NetworkProvidersChangeTrackingTests(databaseFixture); +public class SqliteNetworkProvidersChangeTrackingTests( + SqliteFixture databaseFixture, + ITestOutputHelper outputHelper) + : NetworkProvidersChangeTrackingTests(databaseFixture, outputHelper); public abstract class NetworkProvidersChangeTrackingTests : ChangeTrackingTestBase { @@ -56,7 +60,10 @@ public abstract class NetworkProvidersChangeTrackingTests : ChangeTrackingTestBa private NetworkProvidersConfiguration? _savedProvidersConfig; - protected NetworkProvidersChangeTrackingTests(IDatabaseFixture databaseFixture) : base(databaseFixture) + protected NetworkProvidersChangeTrackingTests( + IDatabaseFixture databaseFixture, + ITestOutputHelper outputHelper) + : base(databaseFixture, outputHelper) { MockNetworkProviderManager.Setup(m => m.GetCurrentConfiguration()) .Returns(RightAsync(GetProvidersConfig())); diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/ProjectChangeTrackingTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/ProjectChangeTrackingTests.cs index d8b9a9168..95c5d0123 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/ProjectChangeTrackingTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/ProjectChangeTrackingTests.cs @@ -5,20 +5,27 @@ using Eryph.StateDb; using Eryph.StateDb.Model; using Eryph.StateDb.TestBase; +using Xunit.Abstractions; namespace Eryph.Modules.Controller.Tests.ChangeTracking; [Trait("Category", "Docker")] [Collection(nameof(MySqlDatabaseCollection))] -public class MySqlProjectChangeTrackingTests(MySqlFixture databaseFixture) - : ProjectChangeTrackingTests(databaseFixture); +public class MySqlProjectChangeTrackingTests( + MySqlFixture databaseFixture, + ITestOutputHelper outputHelper) + : ProjectChangeTrackingTests(databaseFixture, outputHelper); [Collection(nameof(SqliteDatabaseCollection))] -public class SqliteProjectChangeTrackingTests(SqliteFixture databaseFixture) - : ProjectChangeTrackingTests(databaseFixture); - -public abstract class ProjectChangeTrackingTests(IDatabaseFixture databaseFixture) - : ChangeTrackingTestBase(databaseFixture) +public class SqliteProjectChangeTrackingTests( + SqliteFixture databaseFixture, + ITestOutputHelper outputHelper) + : ProjectChangeTrackingTests(databaseFixture, outputHelper); + +public abstract class ProjectChangeTrackingTests( + IDatabaseFixture databaseFixture, + ITestOutputHelper outputHelper) + : ChangeTrackingTestBase(databaseFixture, outputHelper) { private static readonly Guid ProjectId = Guid.NewGuid(); private static readonly Guid AssignmentId = Guid.NewGuid(); diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs index bdd0f8c33..3ea7b3a0f 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs @@ -10,20 +10,27 @@ using Eryph.StateDb; using Eryph.StateDb.Model; using Eryph.StateDb.TestBase; +using Xunit.Abstractions; namespace Eryph.Modules.Controller.Tests.ChangeTracking; [Trait("Category", "Docker")] [Collection(nameof(MySqlDatabaseCollection))] -public class MySqlVirtualNetworkChangeTrackingTests(MySqlFixture databaseFixture) - : VirtualNetworkChangeTrackingTests(databaseFixture); +public class MySqlVirtualNetworkChangeTrackingTests( + MySqlFixture databaseFixture, + ITestOutputHelper outputHelper) + : VirtualNetworkChangeTrackingTests(databaseFixture, outputHelper); [Collection(nameof(SqliteDatabaseCollection))] -public class SqliteVirtualNetworkChangeTrackingTests(SqliteFixture databaseFixture) - : VirtualNetworkChangeTrackingTests(databaseFixture); - -public abstract class VirtualNetworkChangeTrackingTests(IDatabaseFixture databaseFixture) - : ChangeTrackingTestBase(databaseFixture) +public class SqliteVirtualNetworkChangeTrackingTests( + SqliteFixture databaseFixture, + ITestOutputHelper outputHelper) + : VirtualNetworkChangeTrackingTests(databaseFixture, outputHelper); + +public abstract class VirtualNetworkChangeTrackingTests( + IDatabaseFixture databaseFixture, + ITestOutputHelper outputHelper) + : ChangeTrackingTestBase(databaseFixture, outputHelper) { private static readonly Guid ProjectId = Guid.NewGuid(); private static readonly Guid VirtualNetworkId = Guid.NewGuid(); diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs index 479ba93fa..b480e4af7 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs @@ -6,10 +6,13 @@ using Eryph.StateDb.TestBase; using SimpleInjector; using SimpleInjector.Integration.ServiceCollection; +using Xunit.Abstractions; namespace Eryph.Modules.Controller.Tests.Networks; -public sealed class CatletIpManagerTests : InMemoryStateDbTestBase +public sealed class CatletIpManagerTests( + ITestOutputHelper outputHelper) + : InMemoryStateDbTestBase(outputHelper) { private const string DefaultNetworkId = "cb58fe00-3f64-4b66-b58e-23fb15df3cac"; private const string DefaultSubnetId = "ed6697cd-836f-4da7-914b-b09ed1567934"; diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs index 309ec76cb..12c28b610 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs @@ -11,10 +11,13 @@ using Eryph.StateDb.TestBase; using SimpleInjector; using SimpleInjector.Integration.ServiceCollection; +using Xunit.Abstractions; namespace Eryph.Modules.Controller.Tests.Networks; -public class IpPoolManagerTests : InMemoryStateDbTestBase +public class IpPoolManagerTests( + ITestOutputHelper outputHelper) + : InMemoryStateDbTestBase(outputHelper) { private static readonly Guid NetworkId = Guid.NewGuid(); private static readonly Guid SubnetId = Guid.NewGuid(); diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs index b72d7671a..0b05483fc 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs @@ -9,10 +9,13 @@ using Eryph.StateDb.Model; using Eryph.StateDb.TestBase; using SimpleInjector.Integration.ServiceCollection; +using Xunit.Abstractions; namespace Eryph.Modules.Controller.Tests.Networks; -public class NetworkProvidersConfigRealizerTests : InMemoryStateDbTestBase +public class NetworkProvidersConfigRealizerTests( + ITestOutputHelper outputHelper) + : InMemoryStateDbTestBase(outputHelper) { private readonly NetworkProvidersConfiguration _config = new() { diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs index 346ca5945..139c51747 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProviderIpManagerTests.cs @@ -10,10 +10,13 @@ using Eryph.StateDb.TestBase; using SimpleInjector.Integration.ServiceCollection; using SimpleInjector; +using Xunit.Abstractions; namespace Eryph.Modules.Controller.Tests.Networks; -public class ProviderIpManagerTests : InMemoryStateDbTestBase +public class ProviderIpManagerTests( + ITestOutputHelper outputHelper) + : InMemoryStateDbTestBase(outputHelper) { private const string DefaultSubnetId = "00bbb738-9b76-4b52-8c9a-89fcb2516f66"; private const string SecondSubnetId = "edd1c7e0-c8e5-4679-b98d-f7672914d5f7"; diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs index 876159008..1585df2a6 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/UpdateCatletNetworksCommandHandlerTests.cs @@ -17,10 +17,13 @@ using Eryph.StateDb.Model; using LanguageExt; using LanguageExt.Common; +using Xunit.Abstractions; namespace Eryph.Modules.Controller.Tests.Networks; -public class UpdateCatletNetworksCommandHandlerTests : InMemoryStateDbTestBase +public class UpdateCatletNetworksCommandHandlerTests( + ITestOutputHelper outputHelper) + : InMemoryStateDbTestBase(outputHelper) { private const string DefaultProjectId = "4b4a3fcf-b5ed-4a9a-ab6e-03852752095e"; private const string SecondProjectId = "75c27daf-77c8-4b98-a072-a4706dceb422"; diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs index 3af047c57..66e172ef3 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs @@ -5,24 +5,32 @@ using Eryph.StateDb.Model; using Eryph.StateDb.Specifications; using Eryph.StateDb.TestBase; +using Xunit.Abstractions; namespace Eryph.Modules.Controller.Tests; [Trait("Category", "Docker")] [Collection(nameof(MySqlDatabaseCollection))] -public class MySqlStateDbTests(MySqlFixture databaseFixture) - : StateDbDeleteTests(databaseFixture); +public class MySqlStateDbTests( + ITestOutputHelper outputHelper, + MySqlFixture databaseFixture) + : StateDbDeleteTests(outputHelper, databaseFixture); [Collection(nameof(SqliteDatabaseCollection))] -public class SqliteStateDbTests(SqliteFixture databaseFixture) - : StateDbDeleteTests(databaseFixture); +public class SqliteStateDbTests( + ITestOutputHelper outputHelper, + SqliteFixture databaseFixture) + : StateDbDeleteTests(outputHelper, databaseFixture); ///

/// This test verifies that deletes cascade as expected in the state database. /// The default behavior can differ significantly depending on the used DBMS /// and the inheritance strategy (TPH, TPC). /// -public abstract class StateDbDeleteTests(IDatabaseFixture databaseFixture) : StateDbTestBase(databaseFixture) +public abstract class StateDbDeleteTests( + ITestOutputHelper outputHelper, + IDatabaseFixture databaseFixture + ) : StateDbTestBase(databaseFixture, outputHelper) { private static readonly Guid ProjectId = Guid.NewGuid(); private static readonly Guid VirtualNetworkId = Guid.NewGuid(); From 5e1d87eb3a2d2193bfb2ed53e942929ad3d9b547 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Fri, 22 Nov 2024 18:01:08 +0100 Subject: [PATCH 20/30] Add first tests --- .../Networks/INetworkConfigRealizer.cs | 1 - .../Networks/NetworkConfigRealizer.cs | 5 +- .../Networks/NetworkConfigRealizerTests.cs | 689 +++++++++++------- .../NetworkProvidersConfigRealizerTests.cs | 2 + .../ProjectNetworkPlanBuilderTests.cs | 503 +++++++++++++ 5 files changed, 947 insertions(+), 253 deletions(-) create mode 100644 src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProjectNetworkPlanBuilderTests.cs diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/INetworkConfigRealizer.cs b/src/modules/src/Eryph.Modules.Controller/Networks/INetworkConfigRealizer.cs index cddfcd991..b3b49886f 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/INetworkConfigRealizer.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/INetworkConfigRealizer.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using Eryph.ConfigModel.Catlets; using Eryph.ConfigModel.Networks; using Eryph.Core.Network; diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs index dab5e8663..d40e15c9a 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs @@ -50,12 +50,13 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne if (savedNetwork == null) { log.LogDebug("Environment {env}: network {network} not found. Creating new network.", - networkConfig.Environment ?? "default", networkConfig.Name); + networkConfig.Environment ?? EryphConstants.DefaultEnvironmentName, + networkConfig.Name); var newNetwork = new VirtualNetwork { Id = Guid.NewGuid(), ProjectId = projectId, - Environment = networkConfig.Environment, + Environment = networkConfig.Environment ?? EryphConstants.DefaultEnvironmentName, Name = networkConfig.Name, Subnets = new List(), NetworkPorts = new List(), diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs index e8639c0e4..be71a7c4a 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs @@ -5,360 +5,549 @@ using Eryph.Modules.Controller.Networks; using Eryph.StateDb; using Eryph.StateDb.Model; +using Eryph.StateDb.TestBase; using FluentAssertions; +using FluentAssertions.Collections; +using FluentAssertions.Primitives; +using JetBrains.Annotations; using MartinCostello.Logging.XUnit; using Moq; +using SimpleInjector; +using SimpleInjector.Integration.ServiceCollection; using Xunit; using Xunit.Abstractions; -namespace Eryph.Modules.Controller.Tests.Networks +namespace Eryph.Modules.Controller.Tests.Networks; + +public class NetworkConfigRealizerTests(ITestOutputHelper outputHelper) + : InMemoryStateDbTestBase(outputHelper) { - public class NetworkConfigRealizerTests + private readonly NetworkProvidersConfiguration _networkProvidersConfig = new() { - private readonly ITestOutputHelper _testOutput; - - public NetworkConfigRealizerTests(ITestOutputHelper testOutput) + NetworkProviders = + [ + new NetworkProvider + { + Name = "default", + TypeString = "nat_overlay", + BridgeName = "br-nat", + Subnets = + [ + new NetworkProviderSubnet + { + Name = "default", + Network = "10.249.248.0/24", + Gateway = "10.249.248.1", + IpPools = + [ + new NetworkProviderIpPool + { + Name = "default", + FirstIp = "10.249.248.10", + NextIp = "10.249.248.12", + LastIp = "10.249.248.19" + }, + new NetworkProviderIpPool + { + Name = "second-provider-pool", + FirstIp = "10.249.248.20", + NextIp = "10.249.248.22", + LastIp = "10.249.248.29" + }, + ], + }, + new NetworkProviderSubnet + { + Name = "second-provider-subnet", + Network = "10.249.249.0/24", + Gateway = "10.249.249.1", + IpPools = + [ + new NetworkProviderIpPool + { + Name = "default", + FirstIp = "10.249.249.10", + NextIp = "10.249.249.12", + LastIp = "10.249.249.19" + }, + ], + }, + ], + }, + new NetworkProvider + { + Name = "second-overlay-provider", + TypeString = "nat_overlay", + BridgeName = "br-second-nat", + Subnets = + [ + new NetworkProviderSubnet + { + Name = "default", + Network = "10.249.250.0/24", + Gateway = "10.249.250.1", + IpPools = + [ + new NetworkProviderIpPool + { + Name = "default", + FirstIp = "10.249.250.10", + NextIp = "10.249.250.12", + LastIp = "10.249.250.19" + }, + ], + }, + ], + }, + new NetworkProvider + { + Name = "flat-provider", + TypeString = "flat", + }, + ] + }; + + private readonly ITestOutputHelper _testOutput = outputHelper; + + [Theory] + [InlineData("default", "default", "default", "10.249.248.12")] + [InlineData("default", "default", "second-provider-pool", "10.249.248.22")] + [InlineData("default", "second-provider-subnet", "default", "10.249.249.12")] + [InlineData("second-overlay-provider", "default", "default", "10.249.250.12")] + public async Task UpdateNetwork_NewNetworkWithOverlayProvider_UsesCorrectProvider( + string providerName, + string providerSubnetName, + string providerPoolName, + string expectedIpAddress) + { + var networkConfig = new ProjectNetworksConfig() { - _testOutput = testOutput; - } + Networks = + [ + new NetworkConfig + { + Name = "test", + Address = "10.0.100.0/22", + Provider = new ProviderConfig() + { + Name = providerName, + Subnet = providerSubnetName, + IpPool = providerPoolName, + }, + }, + ], + }; - [Fact] - public async Task Creates_new_overlay_network() + await WithScope(async (realizer, stateStore) => { - var logger = new XUnitLogger("log", _testOutput, new XUnitLoggerOptions()); - - var addedNetworks = new List(); - var stateStore = new Mock(); - var networkRepo = new Mock>(); - networkRepo.Setup(x => x.AddAsync( - It.IsAny(), It.IsAny())) - .Returns((VirtualNetwork n, CancellationToken _) => - { - addedNetworks.Add(n); - return Task.FromResult(n); - }); + await realizer.UpdateNetwork(EryphConstants.DefaultProjectId, networkConfig, _networkProvidersConfig); + await stateStore.SaveChangesAsync(); + }); - networkRepo.Setup(x => x.ListAsync(It.IsAny>(), - It.IsAny())) - .ReturnsAsync(Array.Empty().ToList); + await WithScope(async (_, stateStore) => + { + var networks = await stateStore.For().ListAsync(new GetAllNetworks()); - var subnetRepo = new Mock>(); - var ipPoolRepo = new Mock>(); + networks.Should().SatisfyRespectively( + network => + { + network.Name.Should().Be("test"); + network.Subnets.Should().SatisfyRespectively( + subnet => + { + subnet.Name.Should().Be("default"); + subnet.IpNetwork.Should().Be("10.0.100.0/22"); - stateStore.Setup(x => x.For()).Returns(networkRepo.Object); - stateStore.Setup(x => x.For()).Returns(subnetRepo.Object); - stateStore.Setup(x => x.For()).Returns(ipPoolRepo.Object); + subnet.IpPools.Should().SatisfyRespectively( + pool => + { + pool.Name.Should().Be("default"); + pool.IpNetwork.Should().Be("10.0.100.0/22"); + pool.FirstIp.Should().Be("10.0.100.2"); + pool.NextIp.Should().Be("10.0.100.2"); + pool.LastIp.Should().Be("10.0.103.254"); + }); + }); + + network.NetworkPorts.Should().HaveCount(2); + network.NetworkPorts.OfType().Should().SatisfyRespectively( + providerPort => + { + providerPort.Name.Should().Be("provider"); + providerPort.ProviderName.Should().Be(providerName); + providerPort.SubnetName.Should().Be(providerSubnetName); + providerPort.PoolName.Should().Be(providerPoolName); + providerPort.IpAssignments.Should().SatisfyRespectively( + ipAssignment => ipAssignment.IpAddress.Should().Be(expectedIpAddress)); + }); + network.NetworkPorts.OfType().Should().SatisfyRespectively( + routerPort => + { + routerPort.Name.Should().Be("default"); + routerPort.IpAssignments.Should().SatisfyRespectively( + ipAssignment => ipAssignment.IpAddress.Should().Be("10.0.100.1")); + }); + }); + }); + } - var projectId = new Guid(); - var networkConfig = new ProjectNetworksConfig() - { - Networks = new[] + [Fact] + public async Task UpdateNetwork_NewNetworkWithFlatProvider_UsesCorrectProvider() + { + var networkConfig = new ProjectNetworksConfig() + { + Networks = + [ + new NetworkConfig { - new NetworkConfig + Name = "test", + Address = "10.0.100.0/22", + Provider = new ProviderConfig() { - Name = "test", - Address = "10.0.0.0/22" - - } - } - }; + Name = "flat-provider", + }, + }, + ], + }; - var networkProviderConfig = new NetworkProvidersConfiguration() - { - NetworkProviders = new NetworkProvider[] - { - new() - { - Name = "default", - Subnets = - new[] - { - new NetworkProviderSubnet - { - Name = "default", - Network = "10.0.0.0/24", - Gateway = "10.0.0.1" + await WithScope(async (realizer, stateStore) => + { + await realizer.UpdateNetwork(EryphConstants.DefaultProjectId, networkConfig, _networkProvidersConfig); + await stateStore.SaveChangesAsync(); + }); - } - } - } - } - }; + await WithScope(async (_, stateStore) => + { + var networks = await stateStore.For().ListAsync(new GetAllNetworks()); - var realizer = new NetworkConfigRealizer(stateStore.Object, Mock.Of(), logger); - await realizer.UpdateNetwork(projectId, networkConfig, networkProviderConfig); + networks.Should().SatisfyRespectively( + network => + { + network.Name.Should().Be("test"); - addedNetworks.Should().HaveCount(1); - addedNetworks[0].Name.Should().Be("test"); - addedNetworks[0].Subnets.Should().HaveCount(1); - addedNetworks[0].Subnets[0].Name.Should().Be("default"); - addedNetworks[0].Subnets[0].IpNetwork.Should().Be("10.0.0.0/22"); - addedNetworks[0].Subnets[0].IpPools.Should().HaveCount(1); - addedNetworks[0].Subnets[0].IpPools[0].Name.Should().Be("default"); - addedNetworks[0].Subnets[0].IpPools[0].IpNetwork.Should().Be("10.0.0.0/22"); - addedNetworks[0].Subnets[0].IpPools[0].FirstIp.Should().Be("10.0.0.2"); + network.Subnets.Should().SatisfyRespectively( + subnet => + { + subnet.Name.Should().Be("default"); + subnet.IpNetwork.Should().Be("10.0.100.0/22"); - addedNetworks[0].NetworkPorts.Should().HaveCount(2); - addedNetworks[0].NetworkPorts[0].Should().BeOfType(); - addedNetworks[0].NetworkPorts[1].Should().BeOfType(); + subnet.IpPools.Should().SatisfyRespectively( + pool => + { + pool.Name.Should().Be("default"); + pool.IpNetwork.Should().Be("10.0.100.0/22"); + pool.FirstIp.Should().Be("10.0.100.2"); + pool.NextIp.Should().Be("10.0.100.2"); + pool.LastIp.Should().Be("10.0.103.254"); + }); + }); + + // TODO Is this correct or do we want to have a provider port? + network.NetworkPorts.Should().BeEmpty(); + }); + }); + } - } + [Fact] + public async Task Existing_ip_pool_is_updated() + { + var logger = new XUnitLogger("log", _testOutput, new XUnitLoggerOptions()); - [Fact] - public async Task Existing_ip_pool_is_updated() + var routerPort = new NetworkRouterPort() { - - var logger = new XUnitLogger("log", _testOutput, new XUnitLoggerOptions()); - - var routerPort = new NetworkRouterPort() + Name = "default", + MacAddress = "42:00:42:00:10:10", + IpAssignments = new List { - Name = "default", - MacAddress = "42:00:42:00:10:10", - IpAssignments = new List + new IpPoolAssignment { - new IpPoolAssignment - { - IpAddress = "192.168.0.10" - } + IpAddress = "192.168.0.10" } - }; + } + }; - var network = new VirtualNetwork() + var network = new VirtualNetwork() + { + Id = new Guid(), + Name = "test", + Environment = EryphConstants.DefaultEnvironmentName, + Subnets = new List { - Id = new Guid(), - Name = "test", - Environment = EryphConstants.DefaultEnvironmentName, - Subnets = new List + new() { - new() + Name = "default", + IpNetwork = "", + IpPools = new List { - Name = "default", - IpNetwork = "", - IpPools = new List + new() { - new() - { - Name = "default", - IpNetwork = "10.0.0.0/22" - } + Name = "default", + IpNetwork = "10.0.0.0/22" } } - }, - RouterPort = routerPort, - NetworkPorts = new List - { - new ProviderRouterPort() - { - Name = "test-provider-port", - MacAddress = "42:00:42:00:00:10", - SubnetName = "test-provider-subnet", - PoolName = "test-provider-pool", - }, - routerPort } - }; + }, + RouterPort = routerPort, + NetworkPorts = new List + { + new ProviderRouterPort() + { + Name = "test-provider-port", + MacAddress = "42:00:42:00:00:10", + SubnetName = "test-provider-subnet", + PoolName = "test-provider-pool", + }, + routerPort + } + }; - var stateStore = new Mock(); - var networkRepo = new Mock>(); + var stateStore = new Mock(); + var networkRepo = new Mock>(); - networkRepo.Setup(x => x.ListAsync(It.IsAny>(), - It.IsAny())) - .ReturnsAsync(new[] { network }.ToList); + networkRepo.Setup(x => x.ListAsync(It.IsAny>(), + It.IsAny())) + .ReturnsAsync(new[] { network }.ToList); - var subnetRepo = new Mock>(); - var ipPoolRepo = new Mock>(); + var subnetRepo = new Mock>(); + var ipPoolRepo = new Mock>(); - stateStore.Setup(x => x.For()).Returns(networkRepo.Object); - stateStore.Setup(x => x.For()).Returns(subnetRepo.Object); - stateStore.Setup(x => x.For()).Returns(ipPoolRepo.Object); + stateStore.Setup(x => x.For()).Returns(networkRepo.Object); + stateStore.Setup(x => x.For()).Returns(subnetRepo.Object); + stateStore.Setup(x => x.For()).Returns(ipPoolRepo.Object); - var projectId = new Guid(); - var networkConfig = new ProjectNetworksConfig() + var projectId = new Guid(); + var networkConfig = new ProjectNetworksConfig() + { + Networks = new[] { - Networks = new[] + new NetworkConfig { - new NetworkConfig + Name = "test", + Address = "10.0.0.0/22", + Subnets = new[] { - Name = "test", - Address = "10.0.0.0/22", - Subnets = new[] + new NetworkSubnetConfig { - new NetworkSubnetConfig + Name = "default", + IpPools = new[] { - Name = "default", - IpPools = new[] + new IpPoolConfig { - new IpPoolConfig - { - Name = "default", - FirstIp = "10.0.0.50", - LastIp = "10.0.0.200", - } + Name = "default", + FirstIp = "10.0.0.50", + LastIp = "10.0.0.200", } } } } } - }; + } + }; - var networkProviderConfig = new NetworkProvidersConfiguration() + var networkProviderConfig = new NetworkProvidersConfiguration() + { + NetworkProviders = new NetworkProvider[] { - NetworkProviders = new NetworkProvider[] + new() { - new() - { - Name = "default", - Subnets = - new[] + Name = "default", + Subnets = + new[] + { + new NetworkProviderSubnet { - new NetworkProviderSubnet - { - Name = "default", - Network = "10.0.0.0/24", - Gateway = "10.0.0.1" + Name = "default", + Network = "10.0.0.0/24", + Gateway = "10.0.0.1" - } } - } + } } - }; + } + }; - var realizer = new NetworkConfigRealizer(stateStore.Object, Mock.Of(), logger); - await realizer.UpdateNetwork(projectId, networkConfig, networkProviderConfig); + var realizer = new NetworkConfigRealizer(stateStore.Object, Mock.Of(), logger); + await realizer.UpdateNetwork(projectId, networkConfig, networkProviderConfig); - network.Subnets![0].IpPools![0].FirstIp.Should().Be("10.0.0.50"); - network.Subnets![0].IpPools![0].LastIp.Should().Be("10.0.0.200"); + network.Subnets![0].IpPools![0].FirstIp.Should().Be("10.0.0.50"); + network.Subnets![0].IpPools![0].LastIp.Should().Be("10.0.0.200"); - } + } - [Fact] - public async Task Cleanup_of_overlay_when_switched_to_flat() - { + [Fact] + public async Task Cleanup_of_overlay_when_switched_to_flat() + { - var logger = new XUnitLogger("log", _testOutput, new XUnitLoggerOptions()); + var logger = new XUnitLogger("log", _testOutput, new XUnitLoggerOptions()); - var routerPort = new NetworkRouterPort() + var routerPort = new NetworkRouterPort() + { + Name = "default", + MacAddress = "42:00:42:00:00:10", + IpAssignments = new List { - Name = "default", - MacAddress = "42:00:42:00:00:10", - IpAssignments = new List + new IpPoolAssignment { - new IpPoolAssignment - { - IpAddress = "192.168.0.10" - } + IpAddress = "192.168.0.10" } - }; + } + }; - var network = new VirtualNetwork() + var network = new VirtualNetwork() + { + Id = new Guid(), + Name = "test", + Environment = EryphConstants.DefaultEnvironmentName, + Subnets = new List { - Id = new Guid(), - Name = "test", - Environment = EryphConstants.DefaultEnvironmentName, - Subnets = new List + new() { - new() + Name = "default", + IpNetwork = "", + IpPools = new List { - Name = "default", - IpNetwork = "", - IpPools = new List + new() { - new() - { - Name = "default", - IpNetwork = "10.0.0.0/22" - } + Name = "default", + IpNetwork = "10.0.0.0/22" } } - }, - RouterPort = routerPort, - NetworkPorts = new List - { - new ProviderRouterPort - { - Name = "test-provider-port", - MacAddress = "00:00:00:00:10:01", - SubnetName = "test-provider-subnet", - PoolName = "test-provider-pool", - IpAssignments = - [ - new IpPoolAssignment - { - IpAddress = "192.168.0.10" - } - ] - }, - routerPort } - }; + }, + RouterPort = routerPort, + NetworkPorts = new List + { + new ProviderRouterPort + { + Name = "test-provider-port", + MacAddress = "00:00:00:00:10:01", + SubnetName = "test-provider-subnet", + PoolName = "test-provider-pool", + IpAssignments = + [ + new IpPoolAssignment + { + IpAddress = "192.168.0.10" + } + ] + }, + routerPort + } + }; - var stateStore = new Mock(); - var networkRepo = new Mock>(); + var stateStore = new Mock(); + var networkRepo = new Mock>(); - networkRepo.Setup(x => x.ListAsync(It.IsAny>(), - It.IsAny())) - .ReturnsAsync(new[] { network }.ToList); + networkRepo.Setup(x => x.ListAsync(It.IsAny>(), + It.IsAny())) + .ReturnsAsync(new[] { network }.ToList); - var subnetRepo = new Mock>(); - var ipPoolRepo = new Mock>(); + var subnetRepo = new Mock>(); + var ipPoolRepo = new Mock>(); - stateStore.Setup(x => x.For()).Returns(networkRepo.Object); - stateStore.Setup(x => x.For()).Returns(subnetRepo.Object); - stateStore.Setup(x => x.For()).Returns(ipPoolRepo.Object); + stateStore.Setup(x => x.For()).Returns(networkRepo.Object); + stateStore.Setup(x => x.For()).Returns(subnetRepo.Object); + stateStore.Setup(x => x.For()).Returns(ipPoolRepo.Object); - var projectId = new Guid(); - var networkConfig = new ProjectNetworksConfig() + var projectId = new Guid(); + var networkConfig = new ProjectNetworksConfig() + { + Networks = new[] { - Networks = new[] + new NetworkConfig { - new NetworkConfig + Name = "test", + Address = "10.0.0.0/22", + Subnets = new[] { - Name = "test", - Address = "10.0.0.0/22", - Subnets = new[] + new NetworkSubnetConfig { - new NetworkSubnetConfig + Name = "default", + IpPools = new[] { - Name = "default", - IpPools = new[] + new IpPoolConfig { - new IpPoolConfig - { - Name = "default", - FirstIp = "10.0.0.50", - LastIp = "10.0.0.200", - } + Name = "default", + FirstIp = "10.0.0.50", + LastIp = "10.0.0.200", } } } } } - }; + } + }; - var networkProviderConfig = new NetworkProvidersConfiguration() + var networkProviderConfig = new NetworkProvidersConfiguration() + { + NetworkProviders = new NetworkProvider[] { - NetworkProviders = new NetworkProvider[] + new() { - new() - { - Name = "default", - TypeString = "flat" - } + Name = "default", + TypeString = "flat" } - }; + } + }; + + var realizer = new NetworkConfigRealizer(stateStore.Object, Mock.Of(), logger); + await realizer.UpdateNetwork(projectId, networkConfig, networkProviderConfig); + + network.Subnets.Should().HaveCount(0); + routerPort.IpAssignments.Should().HaveCount(0); + + } + + private async Task WithScope(Func func) + { + await using var scope = CreateScope(); + var realizer = scope.GetInstance(); + var stateStore = scope.GetInstance(); + await func(realizer, stateStore); + } + + protected override void AddSimpleInjector(SimpleInjectorAddOptions options) + { + // Use the proper manager instead of a mock. The code is quite + // interdependent as it modifies the same EF Core entities. + options.Container.Register(Lifestyle.Scoped); - var realizer = new NetworkConfigRealizer(stateStore.Object, Mock.Of(), logger); - await realizer.UpdateNetwork(projectId, networkConfig, networkProviderConfig); + options.Container.Register(Lifestyle.Scoped); + } - network.Subnets.Should().HaveCount(0); - routerPort.IpAssignments.Should().HaveCount(0); + protected override async Task SeedAsync(IStateStore stateStore) + { + await SeedDefaultTenantAndProject(); + + var configRealizer = new NetworkProvidersConfigRealizer(stateStore); + await configRealizer.RealizeConfigAsync(_networkProvidersConfig, default); + } + private sealed class GetAllNetworks : Specification + { + public GetAllNetworks() + { + Query.Include(x => x.NetworkPorts) + .ThenInclude(p => p.FloatingPort) + .Include(n => n.NetworkPorts) + .ThenInclude(p => p.IpAssignments) + .ThenInclude(a => ((IpPoolAssignment)a).Pool) + .Include(n => n.NetworkPorts) + .ThenInclude(p => p.IpAssignments) + .ThenInclude(a => a.Subnet) + .Include(x => x.Subnets) + .Include(x => x.RouterPort) + .ThenInclude(x => x!.FloatingPort) + .Include(x => x.Subnets) + .ThenInclude(x => x.IpPools); } } -} \ No newline at end of file + + +} diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs index 0b05483fc..dabfb431c 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs @@ -195,6 +195,8 @@ await WithScope(async (_, stateStore) => }); } + // TODO Add test keeps existing subnet and IP pool when updating + private async Task WithScope(Func func) { await using var scope = CreateScope(); diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProjectNetworkPlanBuilderTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProjectNetworkPlanBuilderTests.cs new file mode 100644 index 000000000..a223c5e8e --- /dev/null +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProjectNetworkPlanBuilderTests.cs @@ -0,0 +1,503 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dbosoft.Rebus.Operations; +using Eryph.Core; +using Eryph.Core.Network; +using Eryph.Modules.Controller.Networks; +using Eryph.StateDb.Model; +using Eryph.StateDb; +using Eryph.StateDb.TestBase; +using LanguageExt; +using LanguageExt.Common; +using Moq; +using SimpleInjector.Integration.ServiceCollection; +using SimpleInjector; +using Xunit.Abstractions; + + +namespace Eryph.Modules.Controller.Tests.Networks; + +public class ProjectNetworkPlanBuilderTests( + ITestOutputHelper outputHelper) + : InMemoryStateDbTestBase(outputHelper) +{ + private const string DefaultProjectId = "4b4a3fcf-b5ed-4a9a-ab6e-03852752095e"; + private const string SecondProjectId = "75c27daf-77c8-4b98-a072-a4706dceb422"; + + private const string DefaultNetworkId = "cb58fe00-3f64-4b66-b58e-23fb15df3cac"; + private const string DefaultSubnetId = "ed6697cd-836f-4da7-914b-b09ed1567934"; + private const string SecondSubnetId = "4f976208-613a-40d4-a284-d32cbd4a1b8e"; + + private const string SecondNetworkId = "e480a020-57d0-4443-a973-57aa0c95872e"; + private const string SecondNetworkSubnetId = "27ec11a4-5d6a-47da-9f9f-eb7486db38ea"; + + private const string ThirdNetworkId = "9016fa5b-e0c7-4626-b1ba-6dc21902d04f"; + private const string ThirdNetworkSubnetId = "106fa5c1-8cf1-4ccd-915a-f9dc230cc299"; + + private const string SecondEnvironmentNetworkId = "81a139e5-ab61-4fe3-b81f-59c11a665d22"; + private const string SecondEnvironmentSubnetId = "dc807357-50e7-4263-8298-0c97ff69f4cf"; + + private const string SecondProjectNetworkId = "c0043e88-8268-4ac0-b027-2fa37ad3168f"; + private const string SecondProjectSubnetId = "0c721846-5e2e-40a9-83d2-f1b75206ef84"; + + private const string FlatNetworkId = "98ff838a-a2c3-464d-8884-f348888ed804"; + + private const string CatletMetadataId = "15e2b061-c625-4469-9fe7-7c455058fcc0"; + + private readonly Mock _networkProviderManagerMock = new(); + + [Fact] + public async Task BuildPLan() + { + await WithScope(async (builder, _) => + { + var result = await builder.GenerateNetworkPlan(Guid.Parse(DefaultProjectId), default); + + var networkPlan = result.Should().BeRight().Subject; + + networkPlan.Id.Should().Be($"project-{DefaultProjectId}"); + }); + } + + private async Task WithScope(Func func) + { + await using var scope = CreateScope(); + var builder = scope.GetInstance(); + var stateStore = scope.GetInstance(); + await func(builder, stateStore); + } + + protected override void AddSimpleInjector(SimpleInjectorAddOptions options) + { + options.Container.RegisterInstance(_networkProviderManagerMock.Object); + + // Use the proper manager instead of a mock. The code is quite + // interdependent as it modifies the same EF Core entities. + options.Container.Register(Lifestyle.Scoped); + + options.Container.Register(Lifestyle.Scoped); + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + var networkProvidersConfig = new NetworkProvidersConfiguration + { + NetworkProviders = + [ + new NetworkProvider + { + Name = "default", + TypeString = "nat_overlay", + BridgeName = "br-nat", + Subnets = + [ + new NetworkProviderSubnet + { + Name = "default", + Network = "10.249.248.0/24", + Gateway = "10.249.248.1", + IpPools = + [ + new NetworkProviderIpPool + { + Name = "default", + FirstIp = "10.249.248.10", + NextIp = "10.249.248.12", + LastIp = "10.249.248.19" + }, + new NetworkProviderIpPool + { + Name = "second-provider-pool", + FirstIp = "10.249.248.20", + NextIp = "10.249.248.22", + LastIp = "10.249.248.29" + }, + ], + }, + new NetworkProviderSubnet + { + Name = "second-provider-subnet", + Network = "10.249.249.0/24", + Gateway = "10.249.249.1", + IpPools = + [ + new NetworkProviderIpPool + { + Name = "default", + FirstIp = "10.249.249.10", + NextIp = "10.249.249.12", + LastIp = "10.249.249.19" + }, + ], + }, + ], + }, + new NetworkProvider + { + Name = "second-overlay-provider", + TypeString = "overlay", + BridgeName = "br-second-nat", + Subnets = + [ + new NetworkProviderSubnet + { + Name = "default", + Network = "10.249.248.0/24", + Gateway = "10.249.248.1", + IpPools = + [ + new NetworkProviderIpPool + { + Name = "default", + FirstIp = "10.249.248.10", + NextIp = "10.249.248.12", + LastIp = "10.249.248.19" + }, + new NetworkProviderIpPool + { + Name = "second-provider-pool", + FirstIp = "10.249.248.20", + NextIp = "10.249.248.22", + LastIp = "10.249.248.29" + }, + ], + }, + ], + }, + new NetworkProvider + { + Name = "flat-provider", + TypeString = "flat", + }, + ] + }; + + _networkProviderManagerMock + .Setup(m => m.GetCurrentConfiguration()) + .Returns(Prelude.RightAsync( + networkProvidersConfig)); + + await WithScope(async (_, stateStore) => + { + var configRealizer = new NetworkProvidersConfigRealizer(stateStore); + await configRealizer.RealizeConfigAsync(networkProvidersConfig, default); + }); + } + + protected override async Task SeedAsync(IStateStore stateStore) + { + await SeedDefaultTenantAndProject(); + + await stateStore.For().AddAsync(new CatletMetadata + { + Id = Guid.Parse(CatletMetadataId), + }); + + await stateStore.For().AddAsync(new Project() + { + Id = Guid.Parse(SecondProjectId), + Name = "second-project", + TenantId = EryphConstants.DefaultTenantId, + }); + + await stateStore.For().AddAsync( + new VirtualNetwork + { + Id = Guid.Parse(DefaultNetworkId), + ProjectId = EryphConstants.DefaultProjectId, + Name = EryphConstants.DefaultNetworkName, + Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = EryphConstants.DefaultProviderName, + IpNetwork = "10.0.0.0/15", + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.Parse(DefaultSubnetId), + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.0.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.0.0.0/16", + FirstIp = "10.0.0.10", + NextIp = "10.0.0.12", + LastIp = "10.0.0.19", + }, + new IpPool() + { + Id = Guid.NewGuid(), + Name = "second-pool", + IpNetwork = "10.0.0.0/16", + FirstIp = "10.0.1.10", + NextIp = "10.0.1.12", + LastIp = "10.0.1.19", + } + ], + }, + new VirtualNetworkSubnet + { + Id = Guid.Parse(SecondSubnetId), + Name = "second-subnet", + IpNetwork = "10.1.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.1.0.0/16", + FirstIp = "10.1.0.10", + NextIp = "10.1.0.12", + LastIp = "10.1.0.19", + } + ], + }, + ], + NetworkPorts = + [ + new ProviderRouterPort + { + Name = "provider", + ProviderName = EryphConstants.DefaultProviderName, + SubnetName = EryphConstants.DefaultSubnetName, + PoolName = EryphConstants.DefaultIpPoolName, + MacAddress = "42:00:42:00:00:01", + } + ], + }); + + await stateStore.For().AddAsync( + new VirtualNetwork + { + Id = Guid.Parse(SecondNetworkId), + ProjectId = EryphConstants.DefaultProjectId, + Name = "second-network", + Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = EryphConstants.DefaultProviderName, + IpNetwork = "10.5.0.0/16", + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.Parse(SecondNetworkSubnetId), + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.5.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.5.0.0/16", + FirstIp = "10.5.0.10", + NextIp = "10.5.0.12", + LastIp = "10.5.0.19", + } + ], + }, + ], + NetworkPorts = + [ + new ProviderRouterPort + { + Name = "provider", + ProviderName = EryphConstants.DefaultProviderName, + SubnetName = EryphConstants.DefaultSubnetName, + PoolName = "second-provider-pool", + MacAddress = "42:00:42:00:00:02", + } + ] + }); + + await stateStore.For().AddAsync( + new VirtualNetwork + { + Id = Guid.Parse(ThirdNetworkId), + ProjectId = EryphConstants.DefaultProjectId, + Name = "third-network", + Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = EryphConstants.DefaultProviderName, + IpNetwork = "10.6.0.0/16", + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.Parse(ThirdNetworkSubnetId), + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.6.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.6.0.0/16", + FirstIp = "10.6.0.10", + NextIp = "10.6.0.12", + LastIp = "10.6.0.19", + } + ], + }, + ], + NetworkPorts = + [ + new ProviderRouterPort + { + Name = "provider", + ProviderName = EryphConstants.DefaultProviderName, + SubnetName = "second-provider-subnet", + PoolName = EryphConstants.DefaultIpPoolName, + MacAddress = "42:00:42:00:00:03", + } + ] + }); + + await stateStore.For().AddAsync( + new VirtualNetwork + { + Id = Guid.Parse(SecondEnvironmentNetworkId), + ProjectId = EryphConstants.DefaultProjectId, + Name = EryphConstants.DefaultNetworkName, + Environment = "second-environment", + NetworkProvider = EryphConstants.DefaultProviderName, + IpNetwork = "10.10.0.0/16", + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.Parse(SecondEnvironmentSubnetId), + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.10.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.10.0.0/16", + FirstIp = "10.10.0.10", + NextIp = "10.10.0.12", + LastIp = "10.10.0.19", + } + ], + }, + ], + NetworkPorts = + [ + new ProviderRouterPort + { + Name = "provider", + ProviderName = EryphConstants.DefaultProviderName, + SubnetName = EryphConstants.DefaultSubnetName, + PoolName = EryphConstants.DefaultIpPoolName, + MacAddress = "42:00:42:00:00:04", + } + ] + }); + + await stateStore.For().AddAsync( + new VirtualNetwork + { + Id = Guid.NewGuid(), + ProjectId = EryphConstants.DefaultProjectId, + Name = "second-provider", + Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = "second-overlay-provider", + IpNetwork = "10.200.0.0/16", + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.200.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.200.0.0/16", + FirstIp = "10.200.0.10", + NextIp = "10.200.0.12", + LastIp = "10.200.0.19", + } + ], + }, + ], + NetworkPorts = + [ + new ProviderRouterPort + { + Name = "provider", + ProviderName = "second-overlay-provider", + SubnetName = EryphConstants.DefaultSubnetName, + PoolName = EryphConstants.DefaultIpPoolName, + MacAddress = "42:00:42:00:00:06", + } + ] + }); + + await stateStore.For().AddAsync( + new VirtualNetwork + { + Id = Guid.Parse(SecondProjectNetworkId), + ProjectId = Guid.Parse(SecondProjectId), + Name = EryphConstants.DefaultNetworkName, + Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = EryphConstants.DefaultProviderName, + IpNetwork = "10.100.0.0/16", + Subnets = + [ + new VirtualNetworkSubnet + { + Id = Guid.Parse(SecondProjectSubnetId), + Name = EryphConstants.DefaultSubnetName, + IpNetwork = "10.100.0.0/16", + IpPools = + [ + new IpPool() + { + Id = Guid.NewGuid(), + Name = EryphConstants.DefaultIpPoolName, + IpNetwork = "10.100.0.0/16", + FirstIp = "10.100.0.10", + NextIp = "10.100.0.12", + LastIp = "10.100.0.19", + } + ], + }, + ], + NetworkPorts = + [ + new ProviderRouterPort + { + Name = "provider", + ProviderName = EryphConstants.DefaultProviderName, + SubnetName = EryphConstants.DefaultSubnetName, + PoolName = EryphConstants.DefaultIpPoolName, + MacAddress = "42:00:42:00:00:05", + } + ] + }); + + await stateStore.For().AddAsync( + new VirtualNetwork + { + Id = Guid.Parse(FlatNetworkId), + ProjectId = Guid.Parse(DefaultProjectId), + Name = "flat-network", + Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = "flat-provider", + }); + } +} \ No newline at end of file From df9dbe088f671a3532d0b46a2acbf87d29e27911 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Mon, 25 Nov 2024 15:56:25 +0100 Subject: [PATCH 21/30] Fix management of NetNat --- .../Networks/NetworkChangeOperationBuilder.cs | 112 +++++++++--------- .../Networks/NetworkChangeOperationNames.cs | 2 +- .../Networks/ProviderNetworkUpdate.cs | 8 +- .../ProviderNetworkConsoleTests.cs | 41 ++++++- 4 files changed, 101 insertions(+), 62 deletions(-) diff --git a/src/modules/src/Eryph.Modules.VmHostAgent/Networks/NetworkChangeOperationBuilder.cs b/src/modules/src/Eryph.Modules.VmHostAgent/Networks/NetworkChangeOperationBuilder.cs index 15c528423..26d84271c 100644 --- a/src/modules/src/Eryph.Modules.VmHostAgent/Networks/NetworkChangeOperationBuilder.cs +++ b/src/modules/src/Eryph.Modules.VmHostAgent/Networks/NetworkChangeOperationBuilder.cs @@ -365,59 +365,42 @@ from uEnable in } } - public Aff RemoveUnusedNat( + public Aff> RemoveInvalidNats( Seq netNat, NetworkProvidersConfiguration newConfig, - Seq newBridges) - { - using (_logger.BeginScope("Method: {method}", nameof(RemoveUnusedNat))) - { - - foreach (var nat in netNat.Filter(x => x.Name.StartsWith("eryph_"))) - { - _logger.LogTrace("Checking host nat {nat}", nat.Name); - - - var providerName = nat.Name["eryph_".Length..]; - newConfig.NetworkProviders.Find(x => - x.Name == providerName && x.Type == NetworkProviderType.NatOverLay) - .Match( - None: () => - { - _logger.LogDebug("Removing invalid host nat {nat}", nat.Name); - - AddOperation( - () => default(RT).HostNetworkCommands.Bind(c => c - .RemoveNetNat(nat.Name)), - NetworkChangeOperation.RemoveNetNat, nat.Name); - - netNat = netNat.Filter(x => x.Name != nat.Name); - }, - Some: provider => - { - var network = IPNetwork2.Parse(nat.InternalIPInterfaceAddressPrefix); - - newBridges.Find(x => x.BridgeName == provider.BridgeName).IfSome(bridge => - { - if (!network.Equals(bridge.Network)) - { - _logger.LogDebug("Removing invalid host nat {nat}", nat.Name); - - AddOperation( - () => default(RT).HostNetworkCommands.Bind(c => c - .RemoveNetNat(nat.Name)), NetworkChangeOperation.RemoveNetNat); - - netNat = netNat.Filter(x => x.Name != nat.Name); - } - }); - - } - ); - } + Seq newBridges) => + from _ in unitAff + from result in use( + Eff(fun(() => _logger.BeginScope("Method: {method}", nameof(RemoveInvalidNats)))).ToAff(), + _ => netNat.Filter(n => n.Name.StartsWith("eryph_")) + .Map(n => RemoveInvalidNat(n, newConfig, newBridges)) + .SequenceSerial()) + // Force enumeration + select result.Somes().ToArray().ToSeq(); + + private Aff> RemoveInvalidNat( + NetNat nat, + NetworkProvidersConfiguration newConfig, + Seq newBridges) => + from _ in unitAff + let providerConfig = newConfig.NetworkProviders + .Find(p => GetNetNatName(p.Name) == nat.Name && p.Type == NetworkProviderType.NatOverLay) + // When the prefix of the NetNat is invalid, we will just recreate the NetNat. + let natPrefix = Try(() => IPNetwork2.Parse(nat.InternalIPInterfaceAddressPrefix)) + .ToOption() + let bridge = providerConfig.Bind(p => newBridges.Find(b => b.BridgeName == p.BridgeName)) + let isNatValid = bridge.Map(b => b.Network == natPrefix).IfNone(false) + from result in isNatValid + ? SuccessAff(Option.None) + : from _1 in unitAff + from _2 in Eff(fun(() => _logger.LogDebug("Removing invalid host NAT '{Nat}'", nat.Name))) + let _3 = AddOperation( + () => default(RT).HostNetworkCommands.Bind(c => c.RemoveNetNat(nat.Name)), + NetworkChangeOperation.RemoveNetNat, + nat.Name) + select Some(nat.Name) + select result; - return unitAff; - } - } public Aff RemoveAdapterPortsOnNatOverlays( NetworkProvidersConfiguration newConfig, @@ -467,7 +450,8 @@ public Aff ConfigureNatAdapters( NetworkProvidersConfiguration newConfig, Seq netNat, Seq createdBridges, - Seq newBridges) + Seq newBridges, + Seq removedNats) { using (_logger.BeginScope("Method: {method}", nameof(ConfigureNatAdapters))) { @@ -481,6 +465,7 @@ from newBridge in newBridges .Find(x => x.BridgeName == networkProvider.BridgeName) let isNewCreatedBridge = createdBridges.Contains(newBridge.BridgeName) + let newNatName = GetNetNatName(networkProvider.Name) select from updateBridgeAdapter in !isNewCreatedBridge ? c.GetAdapterIpV4Address(newBridge.BridgeName) @@ -515,16 +500,28 @@ select from updateBridgeAdapter in !isNewCreatedBridge newBridge.BridgeName) : unit - let __ = netNat.Find(n => n.Name == $"eryph_{networkProvider.Name}") + let __ = netNat.Find(n => n.Name == newNatName) .IfNone(() => { AddOperation( () => default(RT).HostNetworkCommands.Bind(cc => cc - .AddNetNat($"eryph_{networkProvider.Name}", newBridge.Network)), + .AddNetNat(newNatName, newBridge.Network)), + _ => true, + () => default(RT).HostNetworkCommands.Bind(cc => cc + .RemoveNetNat(newNatName)), + NetworkChangeOperation.AddNetNat, newNatName, newBridge.Network); + }) + + let ___ = removedNats.Find(n => n == newNatName) + .IfSome(_ => + { + AddOperation( + () => default(RT).HostNetworkCommands.Bind(cc => cc + .AddNetNat(newNatName, newBridge.Network)), _ => true, () => default(RT).HostNetworkCommands.Bind(cc => cc - .RemoveNetNat($"eryph_{networkProvider.Name}")), - NetworkChangeOperation.AddNetNat, newBridge.Network); + .RemoveNetNat(newNatName)), + NetworkChangeOperation.AddNetNat, newNatName, newBridge.Network); }) select unit; @@ -747,6 +744,11 @@ from ovsTable in ovs.GetOVSTable(cancelSource.Token).ToAff(l => l) } } + private static string GetNetNatName(string providerName) + // The pattern for the NetNat name should be "eryph_{providerName}_{subnetName}". + // At the moment, we only support a single provider subnet which must be named + // 'default'. Hence, we hardcode the subnet part for now. + => $"eryph_{providerName}_default"; public NetworkChanges Build() { diff --git a/src/modules/src/Eryph.Modules.VmHostAgent/Networks/NetworkChangeOperationNames.cs b/src/modules/src/Eryph.Modules.VmHostAgent/Networks/NetworkChangeOperationNames.cs index 4eccc135a..1f186e669 100644 --- a/src/modules/src/Eryph.Modules.VmHostAgent/Networks/NetworkChangeOperationNames.cs +++ b/src/modules/src/Eryph.Modules.VmHostAgent/Networks/NetworkChangeOperationNames.cs @@ -22,7 +22,7 @@ public string this[NetworkChangeOperation key]{ NetworkChangeOperation.RemoveBridge => "Remove bridge '{0}'", NetworkChangeOperation.RemoveUnusedBridge => "Remove unused bridge '{0}'", NetworkChangeOperation.AddBridge => "Add bridge '{0}'", - NetworkChangeOperation.AddNetNat => "Add host NAT for provider '{0}'", + NetworkChangeOperation.AddNetNat => "Add host NAT for provider '{0}' with prefix '{1}'", NetworkChangeOperation.RemoveNetNat => "Remove host NAT for provider {0}", NetworkChangeOperation.RemoveAdapterPort => "Remove adapter '{0}' from bridge '{1}'", NetworkChangeOperation.AddAdapterPort => "Add adapter '{0}' to bridge '{1}'", diff --git a/src/modules/src/Eryph.Modules.VmHostAgent/Networks/ProviderNetworkUpdate.cs b/src/modules/src/Eryph.Modules.VmHostAgent/Networks/ProviderNetworkUpdate.cs index ee889c843..e8731c30a 100644 --- a/src/modules/src/Eryph.Modules.VmHostAgent/Networks/ProviderNetworkUpdate.cs +++ b/src/modules/src/Eryph.Modules.VmHostAgent/Networks/ProviderNetworkUpdate.cs @@ -145,8 +145,8 @@ from createdBridges in changeBuilder.AddMissingBridges( from updateBridgePorts in changeBuilder.UpdateBridgePorts( newConfig, createdBridges, ovsBridges3) - // remove no longer needed network nat(s) - from uRemoveNat in changeBuilder.RemoveUnusedNat( + // remove NATs which are no longer needed or need to be recreated + from removedNats in changeBuilder.RemoveInvalidNats( hostState.NetNat, newConfig, newBridges) // remove any adapter on nat overlays (happens if type is changed to nat_overlay) @@ -155,9 +155,9 @@ from ovsBridges4 in changeBuilder.RemoveAdapterPortsOnNatOverlays( // configure ip settings and nat for nat_overlay adapters from uNatAdapter in changeBuilder.ConfigureNatAdapters( - newConfig, hostState.NetNat, createdBridges, newBridges) + newConfig, hostState.NetNat, createdBridges, newBridges, removedNats) - // create ports for adapters in overlay bridges + // create ports for adapters in overlay bridges from uCreatePorts in changeBuilder.CreateOverlayAdapterPorts( newConfig, ovsBridges4) diff --git a/src/modules/test/Eryph.Modules.VmHostAgent.Test/ProviderNetworkConsoleTests.cs b/src/modules/test/Eryph.Modules.VmHostAgent.Test/ProviderNetworkConsoleTests.cs index 17d5f84e1..2fac8efbc 100644 --- a/src/modules/test/Eryph.Modules.VmHostAgent.Test/ProviderNetworkConsoleTests.cs +++ b/src/modules/test/Eryph.Modules.VmHostAgent.Test/ProviderNetworkConsoleTests.cs @@ -122,6 +122,43 @@ static HostState CreateHostStateWithMultipleSwitches() op => op.Operation.Should().Be(NetworkChangeOperation.UpdateBridgeMapping)); } + [Fact] + public async Task GenerateChanges_invalid_NetNat_recreates_NetNat() + { + static HostState CreateHostStateWithMultipleSwitches() + { + var hostState = CreateHostState(); + return hostState with + { + NetNat = Prelude.Seq( + [ + ..hostState.NetNat, + new NetNat() + { + Name = "eryph_default_default", + InternalIPInterfaceAddressPrefix = "10.249.248.0/28", + } + ]), + }; + } + + var runtime = TestRuntime.New(); + var hostState = CreateHostStateWithMultipleSwitches(); + AddMocks(runtime, hostState); + + var res = await importConfig(NetworkProvidersConfiguration.DefaultConfig) + .Bind(c => generateChanges(hostState, c)) + .Run(runtime); + + var operations = res.Should().BeSuccess().Which.Operations; + operations.Should().SatisfyRespectively( + op => op.Operation.Should().Be(NetworkChangeOperation.AddBridge), + op => op.Operation.Should().Be(NetworkChangeOperation.RemoveNetNat), + op => op.Operation.Should().Be(NetworkChangeOperation.ConfigureNatIp), + op => op.Operation.Should().Be(NetworkChangeOperation.AddNetNat), + op => op.Operation.Should().Be(NetworkChangeOperation.UpdateBridgeMapping)); + } + [Fact] public async Task Sync_Before_new_config_happy_path() { @@ -214,7 +251,7 @@ public async Task Sync_Before_new_config_with_rollback() generatedText.Iter(_testOutput.WriteLine); generatedText.Should().HaveCount(22); - generatedText[4].Should().Be("- Add host NAT for provider '{0}'"); + generatedText[4].Should().Be("- Add host NAT for provider '{0}' with prefix '{1}'"); generatedText[17].Should().Be("rollback of: Add bridge '{0}'"); } @@ -450,7 +487,7 @@ private static void AddMocks(TestRuntime runtime, It.IsAny(), It.IsAny())) .Returns(Prelude.unitAff); - hostCommandsMock.Setup(x => x.AddNetNat("eryph_default", + hostCommandsMock.Setup(x => x.AddNetNat("eryph_default_default", It.IsAny())) .Returns(Prelude.unitAff); From a3c364d4e9711794fdf7beb2df5cd59f97f4c5cc Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Mon, 25 Nov 2024 16:15:15 +0100 Subject: [PATCH 22/30] Mark NetworkProvider as required --- ...er.cs => 20241125150528_InitialCreate.Designer.cs} | 11 ++++++----- ...itialCreate.cs => 20241125150528_InitialCreate.cs} | 2 +- .../Migrations/MySqlStateStoreContextModelSnapshot.cs | 9 +++++---- ...er.cs => 20241125150524_InitialCreate.Designer.cs} | 11 ++++++----- ...itialCreate.cs => 20241125150524_InitialCreate.cs} | 2 +- .../SqliteStateStoreContextModelSnapshot.cs | 9 +++++---- src/data/src/Eryph.StateDb/Model/VirtualNetwork.cs | 2 +- .../Networks/NetworkConfigRealizer.cs | 1 + .../VirtualNetworkChangeTrackingTests.cs | 2 ++ .../Networks/CatletIpManagerTests.cs | 1 + .../Networks/IpPoolManagerTests.cs | 1 + .../Networks/NetworkConfigRealizerTests.cs | 2 ++ .../StateDbDeleteTests.cs | 1 + 13 files changed, 33 insertions(+), 21 deletions(-) rename src/data/src/Eryph.StateDb.MySql/Migrations/{20241111192857_InitialCreate.Designer.cs => 20241125150528_InitialCreate.Designer.cs} (99%) rename src/data/src/Eryph.StateDb.MySql/Migrations/{20241111192857_InitialCreate.cs => 20241125150528_InitialCreate.cs} (99%) rename src/data/src/Eryph.StateDb.Sqlite/Migrations/{20241111192853_InitialCreate.Designer.cs => 20241125150524_InitialCreate.Designer.cs} (99%) rename src/data/src/Eryph.StateDb.Sqlite/Migrations/{20241111192853_InitialCreate.cs => 20241125150524_InitialCreate.cs} (99%) diff --git a/src/data/src/Eryph.StateDb.MySql/Migrations/20241111192857_InitialCreate.Designer.cs b/src/data/src/Eryph.StateDb.MySql/Migrations/20241125150528_InitialCreate.Designer.cs similarity index 99% rename from src/data/src/Eryph.StateDb.MySql/Migrations/20241111192857_InitialCreate.Designer.cs rename to src/data/src/Eryph.StateDb.MySql/Migrations/20241125150528_InitialCreate.Designer.cs index ec093d04c..61a05a1c9 100644 --- a/src/data/src/Eryph.StateDb.MySql/Migrations/20241111192857_InitialCreate.Designer.cs +++ b/src/data/src/Eryph.StateDb.MySql/Migrations/20241125150528_InitialCreate.Designer.cs @@ -12,7 +12,7 @@ namespace Eryph.StateDb.MySql.Migrations { [DbContext(typeof(MySqlStateStoreContext))] - [Migration("20241111192857_InitialCreate")] + [Migration("20241125150528_InitialCreate")] partial class InitialCreate { /// @@ -20,7 +20,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -191,7 +191,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("IpAssignment"); - b.HasDiscriminator("Discriminator").HasValue("IpAssignment"); + b.HasDiscriminator().HasValue("IpAssignment"); b.UseTphMappingStrategy(); }); @@ -261,7 +261,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("NetworkPorts"); - b.HasDiscriminator("Discriminator").HasValue("NetworkPort"); + b.HasDiscriminator().HasValue("NetworkPort"); b.UseTphMappingStrategy(); }); @@ -548,7 +548,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Subnet"); - b.HasDiscriminator("Discriminator").HasValue("Subnet"); + b.HasDiscriminator().HasValue("Subnet"); b.UseTphMappingStrategy(); }); @@ -782,6 +782,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("longtext"); b.Property("NetworkProvider") + .IsRequired() .HasColumnType("longtext"); b.ToTable("VirtualNetworks"); diff --git a/src/data/src/Eryph.StateDb.MySql/Migrations/20241111192857_InitialCreate.cs b/src/data/src/Eryph.StateDb.MySql/Migrations/20241125150528_InitialCreate.cs similarity index 99% rename from src/data/src/Eryph.StateDb.MySql/Migrations/20241111192857_InitialCreate.cs rename to src/data/src/Eryph.StateDb.MySql/Migrations/20241125150528_InitialCreate.cs index 98744b5d9..e74386e3d 100644 --- a/src/data/src/Eryph.StateDb.MySql/Migrations/20241111192857_InitialCreate.cs +++ b/src/data/src/Eryph.StateDb.MySql/Migrations/20241125150528_InitialCreate.cs @@ -332,7 +332,7 @@ protected override void Up(MigrationBuilder migrationBuilder) .Annotation("MySql:CharSet", "utf8mb4"), Environment = table.Column(type: "longtext", nullable: false) .Annotation("MySql:CharSet", "utf8mb4"), - NetworkProvider = table.Column(type: "longtext", nullable: true) + NetworkProvider = table.Column(type: "longtext", nullable: false) .Annotation("MySql:CharSet", "utf8mb4"), IpNetwork = table.Column(type: "longtext", nullable: true) .Annotation("MySql:CharSet", "utf8mb4") diff --git a/src/data/src/Eryph.StateDb.MySql/Migrations/MySqlStateStoreContextModelSnapshot.cs b/src/data/src/Eryph.StateDb.MySql/Migrations/MySqlStateStoreContextModelSnapshot.cs index ca1db2b0b..e8e90f4a9 100644 --- a/src/data/src/Eryph.StateDb.MySql/Migrations/MySqlStateStoreContextModelSnapshot.cs +++ b/src/data/src/Eryph.StateDb.MySql/Migrations/MySqlStateStoreContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -188,7 +188,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("IpAssignment"); - b.HasDiscriminator("Discriminator").HasValue("IpAssignment"); + b.HasDiscriminator().HasValue("IpAssignment"); b.UseTphMappingStrategy(); }); @@ -258,7 +258,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("NetworkPorts"); - b.HasDiscriminator("Discriminator").HasValue("NetworkPort"); + b.HasDiscriminator().HasValue("NetworkPort"); b.UseTphMappingStrategy(); }); @@ -545,7 +545,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Subnet"); - b.HasDiscriminator("Discriminator").HasValue("Subnet"); + b.HasDiscriminator().HasValue("Subnet"); b.UseTphMappingStrategy(); }); @@ -779,6 +779,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("longtext"); b.Property("NetworkProvider") + .IsRequired() .HasColumnType("longtext"); b.ToTable("VirtualNetworks"); diff --git a/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241111192853_InitialCreate.Designer.cs b/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241125150524_InitialCreate.Designer.cs similarity index 99% rename from src/data/src/Eryph.StateDb.Sqlite/Migrations/20241111192853_InitialCreate.Designer.cs rename to src/data/src/Eryph.StateDb.Sqlite/Migrations/20241125150524_InitialCreate.Designer.cs index 06d288497..0e47bf680 100644 --- a/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241111192853_InitialCreate.Designer.cs +++ b/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241125150524_InitialCreate.Designer.cs @@ -11,14 +11,14 @@ namespace Eryph.StateDb.Sqlite.Migrations { [DbContext(typeof(SqliteStateStoreContext))] - [Migration("20241111192853_InitialCreate")] + [Migration("20241125150524_InitialCreate")] partial class InitialCreate { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.11"); modelBuilder.Entity("Eryph.StateDb.Model.CatletDrive", b => { @@ -186,7 +186,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("IpAssignment"); - b.HasDiscriminator("Discriminator").HasValue("IpAssignment"); + b.HasDiscriminator().HasValue("IpAssignment"); b.UseTphMappingStrategy(); }); @@ -256,7 +256,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("NetworkPorts"); - b.HasDiscriminator("Discriminator").HasValue("NetworkPort"); + b.HasDiscriminator().HasValue("NetworkPort"); b.UseTphMappingStrategy(); }); @@ -543,7 +543,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Subnet"); - b.HasDiscriminator("Discriminator").HasValue("Subnet"); + b.HasDiscriminator().HasValue("Subnet"); b.UseTphMappingStrategy(); }); @@ -777,6 +777,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("NetworkProvider") + .IsRequired() .HasColumnType("TEXT"); b.ToTable("VirtualNetworks"); diff --git a/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241111192853_InitialCreate.cs b/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241125150524_InitialCreate.cs similarity index 99% rename from src/data/src/Eryph.StateDb.Sqlite/Migrations/20241111192853_InitialCreate.cs rename to src/data/src/Eryph.StateDb.Sqlite/Migrations/20241125150524_InitialCreate.cs index 605250a27..36c3b369f 100644 --- a/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241111192853_InitialCreate.cs +++ b/src/data/src/Eryph.StateDb.Sqlite/Migrations/20241125150524_InitialCreate.cs @@ -282,7 +282,7 @@ protected override void Up(MigrationBuilder migrationBuilder) ResourceType = table.Column(type: "INTEGER", nullable: false), Name = table.Column(type: "TEXT", nullable: false), Environment = table.Column(type: "TEXT", nullable: false), - NetworkProvider = table.Column(type: "TEXT", nullable: true), + NetworkProvider = table.Column(type: "TEXT", nullable: false), IpNetwork = table.Column(type: "TEXT", nullable: true) }, constraints: table => diff --git a/src/data/src/Eryph.StateDb.Sqlite/Migrations/SqliteStateStoreContextModelSnapshot.cs b/src/data/src/Eryph.StateDb.Sqlite/Migrations/SqliteStateStoreContextModelSnapshot.cs index ab60f2c3a..3e279c1da 100644 --- a/src/data/src/Eryph.StateDb.Sqlite/Migrations/SqliteStateStoreContextModelSnapshot.cs +++ b/src/data/src/Eryph.StateDb.Sqlite/Migrations/SqliteStateStoreContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class SqliteStateStoreContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.11"); modelBuilder.Entity("Eryph.StateDb.Model.CatletDrive", b => { @@ -183,7 +183,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("IpAssignment"); - b.HasDiscriminator("Discriminator").HasValue("IpAssignment"); + b.HasDiscriminator().HasValue("IpAssignment"); b.UseTphMappingStrategy(); }); @@ -253,7 +253,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("NetworkPorts"); - b.HasDiscriminator("Discriminator").HasValue("NetworkPort"); + b.HasDiscriminator().HasValue("NetworkPort"); b.UseTphMappingStrategy(); }); @@ -540,7 +540,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Subnet"); - b.HasDiscriminator("Discriminator").HasValue("Subnet"); + b.HasDiscriminator().HasValue("Subnet"); b.UseTphMappingStrategy(); }); @@ -774,6 +774,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("NetworkProvider") + .IsRequired() .HasColumnType("TEXT"); b.ToTable("VirtualNetworks"); diff --git a/src/data/src/Eryph.StateDb/Model/VirtualNetwork.cs b/src/data/src/Eryph.StateDb/Model/VirtualNetwork.cs index ac3855a8c..aef449488 100644 --- a/src/data/src/Eryph.StateDb/Model/VirtualNetwork.cs +++ b/src/data/src/Eryph.StateDb/Model/VirtualNetwork.cs @@ -12,7 +12,7 @@ public VirtualNetwork() ResourceType = ResourceType.VirtualNetwork; } - public string? NetworkProvider { get; set; } + public required string NetworkProvider { get; set; } public string? IpNetwork { get; set; } diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs index d40e15c9a..3db4e28ca 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs @@ -58,6 +58,7 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne ProjectId = projectId, Environment = networkConfig.Environment ?? EryphConstants.DefaultEnvironmentName, Name = networkConfig.Name, + NetworkProvider = networkConfig.Provider?.Name ?? EryphConstants.DefaultProviderName, Subnets = new List(), NetworkPorts = new List(), }; diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs index 3ea7b3a0f..f309bd566 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/ChangeTracking/VirtualNetworkChangeTrackingTests.cs @@ -257,6 +257,7 @@ await WithHostScope(async stateStore => Name = "new-network", ProjectId = ProjectId, Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = EryphConstants.DefaultProviderName, IpNetwork = "10.1.0.0/20", }; await stateStore.For().AddAsync(network); @@ -474,6 +475,7 @@ await stateStore.For().AddAsync(new VirtualNetwork() Id = VirtualNetworkId, Name = "virtual-test-network", Environment = "test-environment", + NetworkProvider = EryphConstants.DefaultProviderName, ProjectId = ProjectId, Subnets = [ diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs index b480e4af7..2611842da 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/CatletIpManagerTests.cs @@ -261,6 +261,7 @@ await stateStore.For().AddAsync( ProjectId = EryphConstants.DefaultProjectId, Name = EryphConstants.DefaultNetworkName, Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = EryphConstants.DefaultProviderName, Subnets = [ new VirtualNetworkSubnet diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs index 12c28b610..d6db2671e 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/IpPoolManagerTests.cs @@ -205,6 +205,7 @@ await stateStore.For().AddAsync( Name = "test-network", ProjectId = EryphConstants.DefaultProjectId, Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = EryphConstants.DefaultProviderName, Subnets = [ new VirtualNetworkSubnet() diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs index be71a7c4a..44383df20 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs @@ -274,6 +274,7 @@ public async Task Existing_ip_pool_is_updated() Id = new Guid(), Name = "test", Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = EryphConstants.DefaultProviderName, Subnets = new List { new() @@ -403,6 +404,7 @@ public async Task Cleanup_of_overlay_when_switched_to_flat() Id = new Guid(), Name = "test", Environment = EryphConstants.DefaultEnvironmentName, + NetworkProvider = EryphConstants.DefaultProviderName, Subnets = new List { new() diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs index 66e172ef3..b775dac13 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/StateDbDeleteTests.cs @@ -65,6 +65,7 @@ await stateStore.For().AddAsync(new VirtualNetwork() Id = VirtualNetworkId, Name = "virtual-test-network", Environment = "test-environment", + NetworkProvider = EryphConstants.DefaultProviderName, ProjectId = ProjectId, Subnets = [ From 683b199a329a390c2c6093906df57116e14972d7 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Mon, 25 Nov 2024 17:27:11 +0100 Subject: [PATCH 23/30] Update unit tests --- .../Networks/NetworkConfigRealizerTests.cs | 539 +++++++++--------- 1 file changed, 279 insertions(+), 260 deletions(-) diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs index 44383df20..adcb9328e 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs @@ -6,15 +6,8 @@ using Eryph.StateDb; using Eryph.StateDb.Model; using Eryph.StateDb.TestBase; -using FluentAssertions; -using FluentAssertions.Collections; -using FluentAssertions.Primitives; -using JetBrains.Annotations; -using MartinCostello.Logging.XUnit; -using Moq; using SimpleInjector; using SimpleInjector.Integration.ServiceCollection; -using Xunit; using Xunit.Abstractions; namespace Eryph.Modules.Controller.Tests.Networks; @@ -149,46 +142,12 @@ await WithScope(async (_, stateStore) => var networks = await stateStore.For().ListAsync(new GetAllNetworks()); networks.Should().SatisfyRespectively( - network => - { - network.Name.Should().Be("test"); - - network.Subnets.Should().SatisfyRespectively( - subnet => - { - subnet.Name.Should().Be("default"); - subnet.IpNetwork.Should().Be("10.0.100.0/22"); - - subnet.IpPools.Should().SatisfyRespectively( - pool => - { - pool.Name.Should().Be("default"); - pool.IpNetwork.Should().Be("10.0.100.0/22"); - pool.FirstIp.Should().Be("10.0.100.2"); - pool.NextIp.Should().Be("10.0.100.2"); - pool.LastIp.Should().Be("10.0.103.254"); - }); - }); - - network.NetworkPorts.Should().HaveCount(2); - network.NetworkPorts.OfType().Should().SatisfyRespectively( - providerPort => - { - providerPort.Name.Should().Be("provider"); - providerPort.ProviderName.Should().Be(providerName); - providerPort.SubnetName.Should().Be(providerSubnetName); - providerPort.PoolName.Should().Be(providerPoolName); - providerPort.IpAssignments.Should().SatisfyRespectively( - ipAssignment => ipAssignment.IpAddress.Should().Be(expectedIpAddress)); - }); - network.NetworkPorts.OfType().Should().SatisfyRespectively( - routerPort => - { - routerPort.Name.Should().Be("default"); - routerPort.IpAssignments.Should().SatisfyRespectively( - ipAssignment => ipAssignment.IpAddress.Should().Be("10.0.100.1")); - }); - }); + network => AssertOverlayNetwork( + network, + providerName, + providerSubnetName, + providerPoolName, + expectedIpAddress)); }); } @@ -217,6 +176,97 @@ await WithScope(async (realizer, stateStore) => await stateStore.SaveChangesAsync(); }); + await WithScope(async (_, stateStore) => + { + var networks = await stateStore.For().ListAsync(new GetAllNetworks()); + + networks.Should().SatisfyRespectively( + network => + { + network.Name.Should().Be("test"); + + // TODO Is this correct or do we want to have a provider port? + network.Subnets.Should().BeEmpty(); + network.NetworkPorts.Should().BeEmpty(); + }); + }); + } + + [Fact] + public async Task UpdateNetwork_IpRangeOfExistingPoolIsChanged_ExistingPoolIsUpdated() + { + var networkConfig = new ProjectNetworksConfig() + { + Networks = + [ + new NetworkConfig + { + Name = "test", + Address = "10.0.100.0/22", + }, + ], + }; + + await WithScope(async (realizer, stateStore) => + { + await realizer.UpdateNetwork(EryphConstants.DefaultProjectId, networkConfig, _networkProvidersConfig); + await stateStore.SaveChangesAsync(); + }); + + Guid ipPoolId = Guid.Empty; + await WithScope(async (_, stateStore) => + { + var networks = await stateStore.For().ListAsync(new GetAllNetworks()); + + networks.Should().SatisfyRespectively( + network => AssertOverlayNetwork( + network, + EryphConstants.DefaultProviderName, + EryphConstants.DefaultSubnetName, + EryphConstants.DefaultIpPoolName, + "10.249.248.12")); + + ipPoolId = networks.Should().ContainSingle() + .Which.Subnets.Should().ContainSingle() + .Which.IpPools.Should().ContainSingle() + .Which.Id; + }); + + var updatedNetworkConfig = new ProjectNetworksConfig() + { + Networks = + [ + new NetworkConfig + { + Name = "test", + Address = "10.0.100.0/22", + Subnets = + [ + new NetworkSubnetConfig() + { + Name = EryphConstants.DefaultSubnetName, + IpPools = + [ + new IpPoolConfig() + { + Name = EryphConstants.DefaultIpPoolName, + FirstIp = "10.0.100.2", + NextIp = "10.0.100.10", + LastIp = "10.0.100.100", + }, + ], + } + ] + }, + ], + }; + + await WithScope(async (realizer, stateStore) => + { + await realizer.UpdateNetwork(EryphConstants.DefaultProjectId, updatedNetworkConfig, _networkProvidersConfig); + await stateStore.SaveChangesAsync(); + }); + await WithScope(async (_, stateStore) => { var networks = await stateStore.For().ListAsync(new GetAllNetworks()); @@ -235,275 +285,244 @@ await WithScope(async (_, stateStore) => subnet.IpPools.Should().SatisfyRespectively( pool => { + pool.Id.Should().Be(ipPoolId); pool.Name.Should().Be("default"); pool.IpNetwork.Should().Be("10.0.100.0/22"); pool.FirstIp.Should().Be("10.0.100.2"); - pool.NextIp.Should().Be("10.0.100.2"); - pool.LastIp.Should().Be("10.0.103.254"); + pool.NextIp.Should().Be("10.0.100.10"); + pool.LastIp.Should().Be("10.0.100.100"); }); }); - - // TODO Is this correct or do we want to have a provider port? - network.NetworkPorts.Should().BeEmpty(); }); }); } - [Fact] - public async Task Existing_ip_pool_is_updated() + public async Task UpdateNetwork_ChangeNetworkFromOverlayToFlat_RemovesOldOverlayConfiguration() { - - var logger = new XUnitLogger("log", _testOutput, new XUnitLoggerOptions()); - - var routerPort = new NetworkRouterPort() - { - Name = "default", - MacAddress = "42:00:42:00:10:10", - IpAssignments = new List - { - new IpPoolAssignment - { - IpAddress = "192.168.0.10" - } - } - }; - - var network = new VirtualNetwork() + var networkConfig = new ProjectNetworksConfig() { - Id = new Guid(), - Name = "test", - Environment = EryphConstants.DefaultEnvironmentName, - NetworkProvider = EryphConstants.DefaultProviderName, - Subnets = new List - { - new() - { - Name = "default", - IpNetwork = "", - IpPools = new List - { - new() - { - Name = "default", - IpNetwork = "10.0.0.0/22" - } - } - } - }, - RouterPort = routerPort, - NetworkPorts = new List - { - new ProviderRouterPort() + Networks = + [ + new NetworkConfig { - Name = "test-provider-port", - MacAddress = "42:00:42:00:00:10", - SubnetName = "test-provider-subnet", - PoolName = "test-provider-pool", + Name = "test", + Address = "10.0.100.0/22", }, - routerPort - } + ], }; - var stateStore = new Mock(); - var networkRepo = new Mock>(); - - networkRepo.Setup(x => x.ListAsync(It.IsAny>(), - It.IsAny())) - .ReturnsAsync(new[] { network }.ToList); - - var subnetRepo = new Mock>(); - var ipPoolRepo = new Mock>(); + await WithScope(async (realizer, stateStore) => + { + await realizer.UpdateNetwork(EryphConstants.DefaultProjectId, networkConfig, _networkProvidersConfig); + await stateStore.SaveChangesAsync(); + }); + await WithScope(async (_, stateStore) => + { + var networks = await stateStore.For().ListAsync(new GetAllNetworks()); - stateStore.Setup(x => x.For()).Returns(networkRepo.Object); - stateStore.Setup(x => x.For()).Returns(subnetRepo.Object); - stateStore.Setup(x => x.For()).Returns(ipPoolRepo.Object); + networks.Should().SatisfyRespectively( + network => AssertOverlayNetwork( + network, + EryphConstants.DefaultProviderName, + EryphConstants.DefaultSubnetName, + EryphConstants.DefaultIpPoolName, + "10.249.248.12")); + }); - var projectId = new Guid(); - var networkConfig = new ProjectNetworksConfig() + var updatedNetworkConfig = new ProjectNetworksConfig() { - Networks = new[] - { + Networks = + [ new NetworkConfig { Name = "test", - Address = "10.0.0.0/22", - Subnets = new[] + Address = "10.0.100.0/22", + Provider = new ProviderConfig() { - new NetworkSubnetConfig - { - Name = "default", - IpPools = new[] - { - new IpPoolConfig - { - Name = "default", - FirstIp = "10.0.0.50", - LastIp = "10.0.0.200", - } - } - } - } - } - } + Name = "flat-provider", + }, + }, + ], }; - var networkProviderConfig = new NetworkProvidersConfiguration() + await WithScope(async (realizer, stateStore) => { - NetworkProviders = new NetworkProvider[] - { - new() + await realizer.UpdateNetwork(EryphConstants.DefaultProjectId, updatedNetworkConfig, _networkProvidersConfig); + await stateStore.SaveChangesAsync(); + }); + + await WithScope(async (_, stateStore) => + { + var networks = await stateStore.For().ListAsync(new GetAllNetworks()); + + networks.Should().SatisfyRespectively( + network => { - Name = "default", - Subnets = - new[] - { - new NetworkProviderSubnet - { - Name = "default", - Network = "10.0.0.0/24", - Gateway = "10.0.0.1" + network.Name.Should().Be("test"); - } - } - } - } - }; + // TODO Is this correct or do we want to have a provider port? + network.Subnets.Should().BeEmpty(); + network.NetworkPorts.Should().BeEmpty(); + }); - var realizer = new NetworkConfigRealizer(stateStore.Object, Mock.Of(), logger); - await realizer.UpdateNetwork(projectId, networkConfig, networkProviderConfig); + var providerPorts = await stateStore.For().ListAsync(); + providerPorts.Should().BeEmpty(); - network.Subnets![0].IpPools![0].FirstIp.Should().Be("10.0.0.50"); - network.Subnets![0].IpPools![0].LastIp.Should().Be("10.0.0.200"); + var networkRouterPorts = await stateStore.For().ListAsync(); + networkRouterPorts.Should().BeEmpty(); + var ipAssignments = await stateStore.For().ListAsync(); + ipAssignments.Should().BeEmpty(); + }); } - - [Fact] - public async Task Cleanup_of_overlay_when_switched_to_flat() + [Theory] + [InlineData("default", "default", "second-provider-pool", "10.249.248.22")] + [InlineData("default", "second-provider-subnet", "default", "10.249.249.12")] + [InlineData("second-overlay-provider", "default", "default", "10.249.250.12")] + public async Task UpdateNetwork_ChangeNetworkToDifferentOverlayProvider_UpdatesConfiguration( + string providerName, + string providerSubnetName, + string providerPoolName, + string expectedIpAddress) { - - var logger = new XUnitLogger("log", _testOutput, new XUnitLoggerOptions()); - - var routerPort = new NetworkRouterPort() - { - Name = "default", - MacAddress = "42:00:42:00:00:10", - IpAssignments = new List - { - new IpPoolAssignment - { - IpAddress = "192.168.0.10" - } - } - }; - - var network = new VirtualNetwork() + var networkConfig = new ProjectNetworksConfig() { - Id = new Guid(), - Name = "test", - Environment = EryphConstants.DefaultEnvironmentName, - NetworkProvider = EryphConstants.DefaultProviderName, - Subnets = new List - { - new() + Networks = + [ + new NetworkConfig { - Name = "default", - IpNetwork = "", - IpPools = new List + Name = "test", + Address = "10.0.100.0/22", + Provider = new ProviderConfig() { - new() - { - Name = "default", - IpNetwork = "10.0.0.0/22" - } - } - } - }, - RouterPort = routerPort, - NetworkPorts = new List - { - new ProviderRouterPort - { - Name = "test-provider-port", - MacAddress = "00:00:00:00:10:01", - SubnetName = "test-provider-subnet", - PoolName = "test-provider-pool", - IpAssignments = - [ - new IpPoolAssignment - { - IpAddress = "192.168.0.10" - } - ] + Name = EryphConstants.DefaultProviderName, + Subnet = EryphConstants.DefaultSubnetName, + IpPool = EryphConstants.DefaultIpPoolName, + }, }, - routerPort - } + ], }; - var stateStore = new Mock(); - var networkRepo = new Mock>(); - - networkRepo.Setup(x => x.ListAsync(It.IsAny>(), - It.IsAny())) - .ReturnsAsync(new[] { network }.ToList); - - var subnetRepo = new Mock>(); - var ipPoolRepo = new Mock>(); + await WithScope(async (realizer, stateStore) => + { + await realizer.UpdateNetwork(EryphConstants.DefaultProjectId, networkConfig, _networkProvidersConfig); + await stateStore.SaveChangesAsync(); + }); + await WithScope(async (_, stateStore) => + { + var networks = await stateStore.For().ListAsync(new GetAllNetworks()); - stateStore.Setup(x => x.For()).Returns(networkRepo.Object); - stateStore.Setup(x => x.For()).Returns(subnetRepo.Object); - stateStore.Setup(x => x.For()).Returns(ipPoolRepo.Object); + networks.Should().SatisfyRespectively( + network => AssertOverlayNetwork( + network, + EryphConstants.DefaultProviderName, + EryphConstants.DefaultSubnetName, + EryphConstants.DefaultIpPoolName, + "10.249.248.12")); + }); - var projectId = new Guid(); - var networkConfig = new ProjectNetworksConfig() + var updatedNetworkConfig = new ProjectNetworksConfig() { - Networks = new[] - { + Networks = + [ new NetworkConfig { Name = "test", - Address = "10.0.0.0/22", - Subnets = new[] + Address = "10.0.100.0/22", + Provider = new ProviderConfig() { - new NetworkSubnetConfig - { - Name = "default", - IpPools = new[] - { - new IpPoolConfig - { - Name = "default", - FirstIp = "10.0.0.50", - LastIp = "10.0.0.200", - } - } - } - } - } - } + Name = providerName, + Subnet = providerSubnetName, + IpPool = providerPoolName, + }, + }, + ], }; - var networkProviderConfig = new NetworkProvidersConfiguration() + await WithScope(async (realizer, stateStore) => { - NetworkProviders = new NetworkProvider[] - { - new() + await realizer.UpdateNetwork(EryphConstants.DefaultProjectId, updatedNetworkConfig, _networkProvidersConfig); + await stateStore.SaveChangesAsync(); + }); + + await WithScope(async (_, stateStore) => + { + var networks = await stateStore.For().ListAsync(new GetAllNetworks()); + + networks.Should().SatisfyRespectively( + network => AssertOverlayNetwork( + network, + providerName, + providerSubnetName, + providerPoolName, + expectedIpAddress)); + + var providerPorts = await stateStore.For().ListAsync(); + providerPorts.Should().SatisfyRespectively( + port => { - Name = "default", - TypeString = "flat" - } - } - }; + port.SubnetName.Should().Be(providerSubnetName); + port.PoolName.Should().Be(providerPoolName); + }); - var realizer = new NetworkConfigRealizer(stateStore.Object, Mock.Of(), logger); - await realizer.UpdateNetwork(projectId, networkConfig, networkProviderConfig); + var networkRouterPorts = await stateStore.For().ListAsync(); + networkRouterPorts.Should().HaveCount(1); - network.Subnets.Should().HaveCount(0); - routerPort.IpAssignments.Should().HaveCount(0); + var ipAssignments = await stateStore.For().ListAsync(); + ipAssignments.Should().Satisfy( + assignment => assignment.IpAddress == expectedIpAddress, + assignment => assignment.IpAddress == "10.0.100.1"); + }); + } + private void AssertOverlayNetwork( + VirtualNetwork network, + string expectedProviderName, + string expectedProviderSubnet, + string expectedProviderPool, + string expectedProviderIp) + { + network.Name.Should().Be("test"); + + network.Subnets.Should().SatisfyRespectively( + subnet => + { + subnet.Name.Should().Be("default"); + subnet.IpNetwork.Should().Be("10.0.100.0/22"); + + subnet.IpPools.Should().SatisfyRespectively( + pool => + { + pool.Name.Should().Be("default"); + pool.IpNetwork.Should().Be("10.0.100.0/22"); + pool.FirstIp.Should().Be("10.0.100.2"); + pool.NextIp.Should().Be("10.0.100.2"); + pool.LastIp.Should().Be("10.0.103.254"); + }); + }); + + network.NetworkPorts.Should().HaveCount(2); + network.NetworkPorts.OfType().Should().SatisfyRespectively( + providerPort => + { + providerPort.Name.Should().Be("provider"); + providerPort.ProviderName.Should().Be(expectedProviderName); + providerPort.SubnetName.Should().Be(expectedProviderSubnet); + providerPort.PoolName.Should().Be(expectedProviderPool); + providerPort.IpAssignments.Should().SatisfyRespectively( + ipAssignment => ipAssignment.IpAddress.Should().Be(expectedProviderIp)); + }); + network.NetworkPorts.OfType().Should().SatisfyRespectively( + routerPort => + { + routerPort.Name.Should().Be("default"); + routerPort.IpAssignments.Should().SatisfyRespectively( + ipAssignment => ipAssignment.IpAddress.Should().Be("10.0.100.1")); + }); } private async Task WithScope(Func func) From fc13e2691fe8f4ccb40ddfc28ae3f90b415d22f7 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Mon, 25 Nov 2024 21:17:18 +0100 Subject: [PATCH 24/30] Improve NetworkSyncService Cleanup tests --- .../NetworkSyncServiceBridgeService.cs | 9 +- .../src/Eryph.Core/INetworkSyncService.cs | 13 +- .../OperationTaskExtensions.cs | 25 + .../Networks/IProjectNetworkPlanBuilder.cs | 7 +- .../Networks/NetworkSyncService.cs | 222 ++++---- .../Networks/ProjectNetworkPlanBuilder.cs | 76 +-- .../UpdateProjectNetworkPlanCommandHandler.cs | 108 ++-- .../Networks/NetworkConfigRealizerTests.cs | 2 - .../ProjectNetworkPlanBuilderTests.cs | 489 +++--------------- 9 files changed, 296 insertions(+), 655 deletions(-) diff --git a/src/apps/src/Eryph-zero/NetworkSyncServiceBridgeService.cs b/src/apps/src/Eryph-zero/NetworkSyncServiceBridgeService.cs index 555569982..d60e67fd7 100644 --- a/src/apps/src/Eryph-zero/NetworkSyncServiceBridgeService.cs +++ b/src/apps/src/Eryph-zero/NetworkSyncServiceBridgeService.cs @@ -32,11 +32,4 @@ public EitherAsync ValidateChanges(NetworkProvider[] networkPro .GetInstance() .ValidateChanges(networkProviders); } - - public EitherAsync RealizeProviderNetworks(CancellationToken cancellationToken) - { - return _controllerModule.Services.GetRequiredService() - .GetInstance() - .RealizeProviderNetworks(cancellationToken); - } -} \ No newline at end of file +} diff --git a/src/core/src/Eryph.Core/INetworkSyncService.cs b/src/core/src/Eryph.Core/INetworkSyncService.cs index a6206ea64..0788f8048 100644 --- a/src/core/src/Eryph.Core/INetworkSyncService.cs +++ b/src/core/src/Eryph.Core/INetworkSyncService.cs @@ -8,14 +8,11 @@ using LanguageExt; using LanguageExt.Common; -namespace Eryph.Core -{ - public interface INetworkSyncService - { +namespace Eryph.Core; - public EitherAsync SyncNetworks(CancellationToken cancellationToken); +public interface INetworkSyncService +{ + public EitherAsync SyncNetworks(CancellationToken cancellationToken); - public EitherAsync ValidateChanges(NetworkProvider[] networkProviders); - EitherAsync RealizeProviderNetworks(CancellationToken cancellationToken); - } + public EitherAsync ValidateChanges(NetworkProvider[] networkProviders); } diff --git a/src/modules/src/Eryph.ModuleCore/OperationTaskExtensions.cs b/src/modules/src/Eryph.ModuleCore/OperationTaskExtensions.cs index f67350636..660e58058 100644 --- a/src/modules/src/Eryph.ModuleCore/OperationTaskExtensions.cs +++ b/src/modules/src/Eryph.ModuleCore/OperationTaskExtensions.cs @@ -4,12 +4,37 @@ using LanguageExt; using LanguageExt.Common; +using static LanguageExt.Prelude; + // ReSharper disable once CheckNamespace namespace Dbosoft.Rebus.Operations; #pragma warning restore 1998 public static class OperationTaskExtensions { + public static async Task FailOrComplete( + this Aff aff, + ITaskMessaging messaging, + IOperationTaskMessage message) + where TRet : notnull + { + var result = await aff.Run(); + await result.FailOrComplete(messaging, message); + return unit; + } + + public static Task FailOrComplete( + this Fin fin, + ITaskMessaging messaging, + IOperationTaskMessage message) + where TRet : notnull => + fin.Match( + Succ: ret => ret is Unit + ? messaging.CompleteTask(message).ToUnit() + : messaging.CompleteTask(message, ret).ToUnit(), + Fail: error => messaging.FailTask(message, error).ToUnit()); + + public static Task FailOrComplete( this EitherAsync either, ITaskMessaging messaging, diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/IProjectNetworkPlanBuilder.cs b/src/modules/src/Eryph.Modules.Controller/Networks/IProjectNetworkPlanBuilder.cs index 31057233f..21427b1f9 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/IProjectNetworkPlanBuilder.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/IProjectNetworkPlanBuilder.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using Dbosoft.OVN; +using Eryph.Core.Network; using LanguageExt; using LanguageExt.Common; @@ -8,5 +9,7 @@ namespace Eryph.Modules.Controller.Networks; public interface IProjectNetworkPlanBuilder { - EitherAsync GenerateNetworkPlan(Guid projectId, CancellationToken cancellationToken); -} \ No newline at end of file + EitherAsync GenerateNetworkPlan( + Guid projectId, + NetworkProvidersConfiguration providerConfig); +} diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs index 70bfcf07b..ab7c41a58 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs @@ -20,6 +20,8 @@ using SimpleInjector; using SimpleInjector.Lifestyles; +using static LanguageExt.Prelude; + namespace Eryph.Modules.Controller.Networks; internal class NetworkSyncService : INetworkSyncService @@ -38,116 +40,120 @@ public NetworkSyncService( _log = log; } - public EitherAsync SyncNetworks(CancellationToken cancellationToken) - { - async Task SyncNetworksAsync(NetworkProvidersConfiguration providersConfiguration) - { - await using var scope = AsyncScopedLifestyle.BeginScope(_container); - var realizer = scope.GetInstance(); - var stateStore = scope.GetInstance(); - - var projects = await stateStore.Read().ListAsync(new ProjectSpecs.GetAll(), cancellationToken); - - foreach (var project in projects) - { - try + public EitherAsync SyncNetworks(CancellationToken cancellationToken) => + from providerConfig in _providerManager.GetCurrentConfiguration() + from _ in SyncNetworks(providerConfig, cancellationToken) + .Run().Map(fin => fin.ToEither()).AsTask().ToAsync() + select Unit.Default; + + private Aff SyncNetworks( + NetworkProvidersConfiguration providersConfiguration, + CancellationToken cancellationToken) => + from _1 in RealizeProviderNetworks(providersConfiguration) + from _2 in RealizeProjectNetworks(providersConfiguration) + from neighbors in ApplyNetworkPlans(providersConfiguration) + from _4 in use( + Eff(() => AsyncScopedLifestyle.BeginScope(_container)), + scope => + from _ in unitAff + let bus = scope.GetInstance() + from __ in Aff(async () => await bus.Advanced.Topics.Publish( + $"broadcast_{QueueNames.VMHostAgent}", + new NetworkNeighborsUpdateRequestedEvent + { + UpdatedAddresses = neighbors.ToArray() + }) + .ToUnit()) + select unit) + select unit; + + private Aff RealizeProviderNetworks( + NetworkProvidersConfiguration providerConfig) => + use(Eff(() => AsyncScopedLifestyle.BeginScope(_container)), + scope => + from _ in unitAff + let configRealizer = scope.GetInstance() + from __ in Aff(async () => await configRealizer.RealizeConfigAsync(providerConfig, default).ToUnit()) + select unit); + + private Aff RealizeProjectNetworks( + NetworkProvidersConfiguration providerConfig) => + from projects in use( + Eff(() => AsyncScopedLifestyle.BeginScope(_container)), + scope => + from _ in unitAff + let stateStore = scope.GetInstance() + from projects in stateStore.Read().IO.ListAsync().ToAff(e => e) + select projects) + from _s in projects + .Map(p => RealizeProjectNetworks(p, providerConfig) + .IfFail(e => { - var networks = await stateStore.For() - .ListAsync(new VirtualNetworkSpecs.GetForProjectConfig(project.Id), cancellationToken); - - var projectConfig = networks.ToNetworksConfig(project.Name); - await realizer.UpdateNetwork(project.Id, projectConfig, providersConfiguration); - await stateStore.SaveChangesAsync(cancellationToken); - } - catch (Exception ex) + _log.LogError(e, "Failed to save network changes for project {ProjectName} ({ProjectId})", + p.Name, p.Id); + return unit; + })) + .SequenceSerial() + select unit; + + private Aff RealizeProjectNetworks( + Project project, + NetworkProvidersConfiguration providerConfig) => + use(Eff(() => AsyncScopedLifestyle.BeginScope(_container)), + scope => + from _ in unitAff + let stateStore = scope.GetInstance() + from networks in stateStore.For().IO.ListAsync( + new VirtualNetworkSpecs.GetForProjectConfig(project.Id)) + .ToAff(e => e) + from config in Eff(() => networks.ToNetworksConfig(project.Name)) + let realizer = scope.GetInstance() + from __ in Aff(async () => await realizer.UpdateNetwork(project.Id, config, providerConfig).ToUnit()) + from ___ in Aff(async () => await stateStore.SaveChangesAsync().ToUnit()) + select unit); + + private Aff> ApplyNetworkPlans( + NetworkProvidersConfiguration providerConfig) => + from projects in use( + Eff(() => AsyncScopedLifestyle.BeginScope(_container)), + scope => + from _ in unitAff + let stateStore = scope.GetInstance() + from projects in stateStore.Read().IO.ListAsync().ToAff(e => e) + select projects) + from neighbors in projects + .Map(p => ApplyNetworkPlan(p.Id, providerConfig) + .IfFail(e => { - _log.LogError(ex, "Failed to save network changes for project {projectId} ({projectName})", project.Id, project.Name); - } - - } - - return Unit.Default; - - } - - return from providerConfig in _providerManager.GetCurrentConfiguration() - from sync in Prelude.TryAsync(() => SyncNetworksAsync(providerConfig)).ToEither() - from realize in RealizeProviderNetworks(cancellationToken) - select Unit.Default; - } - - public EitherAsync RealizeProviderNetworks(CancellationToken cancellationToken) - { - async Task RealizeProviderNetworksAsync() - { - await using var scope = AsyncScopedLifestyle.BeginScope(_container); - - var configRealizer = scope.GetInstance(); - - var stateStore = scope.GetInstance(); - var providerManager = scope.GetInstance(); - - var bus = _container.GetInstance(); - - var config = await providerManager.GetCurrentConfiguration() - .IfLeft(e => e.ToException().Rethrow()); - - await configRealizer.RealizeConfigAsync(config, cancellationToken); - - var projects = await stateStore.For().ListAsync(cancellationToken); - - var updatedNetworkNeighbors = new List(); - foreach (var project in projects) - { - var result = await UpdateProjectNetworkPlan(project.Id); - result.IfLeft(l => - { - _log.LogError(l.ToException(), - "Failed to apply network plan for project {ProjectName}({ProjectId})", - project.Name, project.Id); - }); - result.IfRight(updatedNetworkNeighbors.AddRange); - } - - await stateStore.SaveChangesAsync(cancellationToken); - - await bus.Advanced.Topics.Publish( - $"broadcast_{QueueNames.VMHostAgent}", - new NetworkNeighborsUpdateRequestedEvent - { - UpdatedAddresses = updatedNetworkNeighbors.ToArray() - }); - - return Unit.Default; - } - - return Prelude.TryAsync(RealizeProviderNetworksAsync).ToEither(); - } - - - private EitherAsync UpdateProjectNetworkPlan(Guid projectId) - { - var sysEnv = _container.GetInstance(); - var ovnSettings = _container.GetInstance(); - var planBuilder = _container.GetInstance(); - var logger = _container.GetInstance(); - - var networkPlanRealizer = new NetworkPlanRealizer( - new OVNControlTool(sysEnv, ovnSettings.NorthDBConnection), - logger); - - return from networkPlan in planBuilder.GenerateNetworkPlan(projectId, CancellationToken.None) - from appliedNetworkPlan in networkPlanRealizer.ApplyNetworkPlan(networkPlan) - let updatedNetworkNeighbors = appliedNetworkPlan.PlannedNATRules.Values - .Map(port => new NetworkNeighborRecord - { - IpAddress = port.ExternalIP, - MacAddress = port.ExternalMAC - }).ToArray() - select updatedNetworkNeighbors; - } - - + _log.LogError(e, "Failed to apply network plan for project {ProjectName}({ProjectId})", + p.Name, p.Id); + return Seq(); + })) + .SequenceSerial() + select neighbors.Flatten(); + + private Aff> ApplyNetworkPlan( + Guid projectId, + NetworkProvidersConfiguration providerConfig) => + use(Eff(() => AsyncScopedLifestyle.BeginScope(_container)), + scope => + from _ in unitAff + let sysEnv = scope.GetInstance() + let ovnSettings = scope.GetInstance() + let planBuilder = scope.GetInstance() + let logger = scope.GetInstance() + let networkPlanRealizer = new NetworkPlanRealizer( + new OVNControlTool(sysEnv, ovnSettings.NorthDBConnection), + logger) + from networkPlan in planBuilder.GenerateNetworkPlan(projectId, providerConfig).ToAff(e => e) + from appliedNetworkPlan in networkPlanRealizer.ApplyNetworkPlan(networkPlan).ToAff(e => e) + let updatedNetworkNeighbors = appliedNetworkPlan.PlannedNATRules.Values + .Map(port => new NetworkNeighborRecord + { + IpAddress = port.ExternalIP, + MacAddress = port.ExternalMAC + }).ToSeq() + select updatedNetworkNeighbors); public EitherAsync ValidateChanges(NetworkProvider[] networkProviders) { diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs b/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs index 8b501d989..dd28de475 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs @@ -16,7 +16,6 @@ namespace Eryph.Modules.Controller.Networks; internal class ProjectNetworkPlanBuilder( - INetworkProviderManager networkProviderManager, IStateStore stateStore) : IProjectNetworkPlanBuilder { @@ -29,36 +28,36 @@ private sealed record ProviderSubnetInfo(ProviderSubnet Subnet, NetworkProviderS private sealed record CatletDnsInfo(string CatletMetadataId, Map Addresses); - public EitherAsync GenerateNetworkPlan(Guid projectId, CancellationToken cancellationToken) - { - var networkPlan = new NetworkPlan(projectId.ToString()); - - return from providerConfig in networkProviderManager.GetCurrentConfiguration() - let overLayProviders = - providerConfig.NetworkProviders - .Where(x => x.Type is NetworkProviderType.NatOverLay or NetworkProviderType.Overlay) - .ToSeq() - - from networks in GetAllOverlayNetworks(projectId, overLayProviders, cancellationToken) - - from providerSubnets in GetProviderSubnets(overLayProviders, networks, cancellationToken) - from providerRouterPorts in GetProviderRouterPorts(networks, providerSubnets) - - let catletPorts = FindPortsOfType(networks) - from floatingPorts in GetFloatingPorts(catletPorts, providerSubnets) - let dnsInfo = MapCatletPortsToDnsInfos(catletPorts) - let dnsNames = dnsInfo.Select(info => info.CatletMetadataId).ToArray() - - let p1 = AddProjectRouterAndPorts(networkPlan, networks) - from p2 in AddExternalNetSwitches(p1, providerSubnets, overLayProviders) - from p3 in AddProviderRouterPorts(p2, providerRouterPorts) - let p4 = AddNetworksAsSwitches(p3, networks, dnsNames) - let p5 = AddSubnetsAsDhcpOptions(p4, networks) - let p6 = AddCatletPorts(p5, catletPorts) - let p7 = AddFloatingPorts(p6, floatingPorts) - let p8 = AddDnsNames(p7, dnsInfo) - select p8; - } + public EitherAsync GenerateNetworkPlan( + Guid projectId, + NetworkProvidersConfiguration providerConfig) => + from _ in RightAsync(unit) + let networkPlan = new NetworkPlan(projectId.ToString()) + let overLayProviders = + providerConfig.NetworkProviders + .Where(x => x.Type is NetworkProviderType.NatOverLay or NetworkProviderType.Overlay) + .ToSeq() + + from networks in GetAllOverlayNetworks(projectId, overLayProviders) + + from providerSubnets in GetProviderSubnets(overLayProviders, networks) + from providerRouterPorts in GetProviderRouterPorts(networks, providerSubnets) + + let catletPorts = FindPortsOfType(networks) + from floatingPorts in GetFloatingPorts(catletPorts, providerSubnets) + let dnsInfo = MapCatletPortsToDnsInfos(catletPorts) + let dnsNames = dnsInfo.Select(info => info.CatletMetadataId).ToArray() + + let p1 = AddProjectRouterAndPorts(networkPlan, networks) + from p2 in AddExternalNetSwitches(p1, providerSubnets, overLayProviders) + from p3 in AddProviderRouterPorts(p2, providerRouterPorts) + let p4 = AddNetworksAsSwitches(p3, networks, dnsNames) + let p5 = AddSubnetsAsDhcpOptions(p4, networks) + let p6 = AddCatletPorts(p5, catletPorts) + let p7 = AddFloatingPorts(p6, floatingPorts) + let p8 = AddDnsNames(p7, dnsInfo) + select p8; + private EitherAsync> GetProviderRouterPorts( Seq networks, @@ -93,7 +92,8 @@ private static EitherAsync> GetFloatingPorts( private EitherAsync> GetProviderSubnets( - IEnumerable overLayProviders, Seq networks, CancellationToken cancellationToken) + IEnumerable overLayProviders, + Seq networks) { return networks.Map( network => network.NetworkPorts.Filter(x => x is ProviderRouterPort) @@ -111,7 +111,7 @@ private EitherAsync> GetProviderSubnets( .Bind(rs => //get all provider configurations and filter for configured providers stateStore.For().IO - .ListAsync(new NetplanBuilderSpecs.GetAllProviderSubnets(), cancellationToken) + .ListAsync(new NetplanBuilderSpecs.GetAllProviderSubnets()) .Map(all => all.Where(a => rs.Any(r => r.NetworkProvider == a.ProviderName && r.SubnetName == a.Name))) .Map(subnets => @@ -124,12 +124,12 @@ private EitherAsync> GetProviderSubnets( } - private EitherAsync> GetAllOverlayNetworks(Guid projectId, - Seq overLayProviders, CancellationToken cancellationToken) => + private EitherAsync> GetAllOverlayNetworks( + Guid projectId, + Seq overLayProviders) => stateStore.For().IO - .ListAsync(new NetplanBuilderSpecs.GetAllNetworks(projectId), cancellationToken) - .Map(s => - s.Filter(x => overLayProviders.Any(p => p.Name == x.NetworkProvider))); + .ListAsync(new NetplanBuilderSpecs.GetAllNetworks(projectId)) + .Map(s => s.Filter(x => overLayProviders.Any(p => p.Name == x.NetworkProvider))); private static EitherAsync AddProviderRouterPorts( diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateProjectNetworkPlanCommandHandler.cs b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateProjectNetworkPlanCommandHandler.cs index 4d805a751..f6dba72b8 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateProjectNetworkPlanCommandHandler.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateProjectNetworkPlanCommandHandler.cs @@ -5,86 +5,62 @@ using Dbosoft.OVN; using Dbosoft.OVN.OSCommands.OVN; using Dbosoft.Rebus.Operations; +using Eryph.Core; using Eryph.Messages.Resources.Catlets.Events; using Eryph.Messages.Resources.Networks.Commands; using Eryph.StateDb; using JetBrains.Annotations; using LanguageExt; +using LanguageExt.Common; using Microsoft.Extensions.Logging; using Rebus.Handlers; +using static LanguageExt.Prelude; + namespace Eryph.Modules.Controller.Networks; [UsedImplicitly] -public class UpdateProjectNetworkPlanCommandHandler : IHandleMessages> +public class UpdateProjectNetworkPlanCommandHandler( + ISysEnvironment sysEnvironment, + IOVNSettings ovnSettings, + ILogger logger, + INetworkProviderManager networkProviderManager, + IProjectNetworkPlanBuilder planBuilder, + IStateStore stateStore, + ITaskMessaging messaging) + : IHandleMessages> { - private readonly ISysEnvironment _sysEnvironment; - private readonly IOVNSettings _ovnSettings; - private readonly ILogger _logger; - private readonly ITaskMessaging _messaging; - private readonly IProjectNetworkPlanBuilder _planBuilder; - private readonly IStateStore _stateStore; - - - public UpdateProjectNetworkPlanCommandHandler( - ISysEnvironment sysEnvironment, - IOVNSettings ovnSettings, - ILogger logger, - IProjectNetworkPlanBuilder planBuilder, - IStateStore stateStore, ITaskMessaging messaging) - { - _sysEnvironment = sysEnvironment; - _ovnSettings = ovnSettings; - _logger = logger; - _planBuilder = planBuilder; - _stateStore = stateStore; - _messaging = messaging; - } - public async Task Handle(OperationTask message) { - var cancelSource = new CancellationTokenSource(TimeSpan.FromMinutes(5)); - - await _messaging.ProgressMessage(message.OperationId, message.TaskId, "Rebuilding project network settings"); - - // build plan and save it to DB (to make sure that everything can be saved - // we cannot use unit of work here, as OVN changes are not included in uow - var generatedPlan = await (from plan in _planBuilder.GenerateNetworkPlan(message.Command.ProjectId, cancelSource.Token) - from _ in Prelude.TryAsync(() =>_stateStore.SaveChangesAsync(cancelSource.Token)) - .ToEither() - select plan - - ).Match( - plan => plan, - l => - { - l.Throw(); - return new NetworkPlan(""); // will never be reached... - }); - - await UpdateOVN(generatedPlan, message); - } - - public async Task UpdateOVN(NetworkPlan networkPlan, OperationTask message) - { - var cancelSource = new CancellationTokenSource(TimeSpan.FromMinutes(5)); - - var networkPlanRealizer = - new NetworkPlanRealizer(new OVNControlTool(_sysEnvironment, _ovnSettings.NorthDBConnection), _logger); - - await networkPlanRealizer.ApplyNetworkPlan(networkPlan, cancelSource.Token) - .Map(plan => new UpdateProjectNetworkPlanResponse - { - ProjectId = message.Command.ProjectId, - UpdatedAddresses = plan.PlannedNATRules - .Values.Map(port => new NetworkNeighborRecord - { - IpAddress = port.ExternalIP, - MacAddress = port.ExternalMAC - }) - .ToArray() - }) - .FailOrComplete(_messaging, message); + await messaging.ProgressMessage(message.OperationId, message.TaskId, "Rebuilding project network settings"); + + await UpdateProjectNetwork(message.Command.ProjectId) + .FailOrComplete(messaging, message); } + private Aff UpdateProjectNetwork( + Guid projectId) => + from providerConfig in networkProviderManager.GetCurrentConfiguration().ToAff(e => e) + from networkPlan in planBuilder.GenerateNetworkPlan(projectId, providerConfig).ToAff(e => e) + from appliedPlan in use( + Eff(() => new CancellationTokenSource(TimeSpan.FromMinutes(5))), + cancelSource => + from _ in SuccessAff(unit) + let networkPlanRealizer = new NetworkPlanRealizer( + new OVNControlTool(sysEnvironment, ovnSettings.NorthDBConnection), + logger) + from appliedPlan in networkPlanRealizer.ApplyNetworkPlan(networkPlan, cancelSource.Token).ToAff(e => e) + select appliedPlan) + let response = new UpdateProjectNetworkPlanResponse + { + ProjectId = projectId, + UpdatedAddresses = appliedPlan.PlannedNATRules + .Values.Map(port => new NetworkNeighborRecord + { + IpAddress = port.ExternalIP, + MacAddress = port.ExternalMAC + }) + .ToArray() + } + select response; } \ No newline at end of file diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs index adcb9328e..2ebf93169 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs @@ -569,6 +569,4 @@ public GetAllNetworks() .ThenInclude(x => x.IpPools); } } - - } diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProjectNetworkPlanBuilderTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProjectNetworkPlanBuilderTests.cs index a223c5e8e..6128dcb3f 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProjectNetworkPlanBuilderTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProjectNetworkPlanBuilderTests.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using Dbosoft.Rebus.Operations; +using Eryph.ConfigModel.Networks; using Eryph.Core; using Eryph.Core.Network; using Eryph.Modules.Controller.Networks; @@ -16,6 +17,7 @@ using SimpleInjector.Integration.ServiceCollection; using SimpleInjector; using Xunit.Abstractions; +using System.Linq.Expressions; namespace Eryph.Modules.Controller.Tests.Networks; @@ -47,18 +49,76 @@ public class ProjectNetworkPlanBuilderTests( private const string CatletMetadataId = "15e2b061-c625-4469-9fe7-7c455058fcc0"; - private readonly Mock _networkProviderManagerMock = new(); + private readonly NetworkProvidersConfiguration _networkProvidersConfig = new() + { + NetworkProviders = + [ + new NetworkProvider + { + Name = "default", + TypeString = "nat_overlay", + BridgeName = "br-nat", + Subnets = + [ + new NetworkProviderSubnet + { + Name = "default", + Network = "10.249.248.0/22", + Gateway = "10.249.248.1", + IpPools = + [ + new NetworkProviderIpPool + { + Name = "default", + FirstIp = "10.249.248.10", + NextIp = "10.249.248.10", + LastIp = "10.249.251.241", + }, + ], + }, + ], + }, + ], + }; [Fact] - public async Task BuildPLan() + public async Task GenerateNetworkPlan_DefaultConfig_GeneratesValidNetworkPlan() { + await using(var scope = CreateScope()) + { + var providerConfigRealizer = scope.GetInstance(); + await providerConfigRealizer.RealizeConfigAsync(_networkProvidersConfig, default); + + var networkConfig = new ProjectNetworksConfig() + { + Networks = + [ + new NetworkConfig() + { + Name = EryphConstants.DefaultNetworkName, + Address = "10.0.100.0/24", + }, + ], + }; + + var configRealizer = scope.GetInstance(); + await configRealizer.UpdateNetwork(EryphConstants.DefaultProjectId, networkConfig, _networkProvidersConfig); + + var stateStore = scope.GetInstance(); + await stateStore.SaveChangesAsync(); + } + await WithScope(async (builder, _) => { - var result = await builder.GenerateNetworkPlan(Guid.Parse(DefaultProjectId), default); + var result = await builder.GenerateNetworkPlan( + Guid.Parse(DefaultProjectId), + _networkProvidersConfig); var networkPlan = result.Should().BeRight().Subject; - networkPlan.Id.Should().Be($"project-{DefaultProjectId}"); + networkPlan.Id.Should().Be(DefaultProjectId); + networkPlan.PlannedSwitchPorts.ToDictionary().Should().ContainKey( + $"SN-externalNet-{networkPlan.Id}-default-default-br-nat"); }); } @@ -72,432 +132,15 @@ private async Task WithScope(Func protected override void AddSimpleInjector(SimpleInjectorAddOptions options) { - options.Container.RegisterInstance(_networkProviderManagerMock.Object); - - // Use the proper manager instead of a mock. The code is quite - // interdependent as it modifies the same EF Core entities. options.Container.Register(Lifestyle.Scoped); + options.Container.Register(Lifestyle.Scoped); + options.Container.Register(Lifestyle.Scoped); options.Container.Register(Lifestyle.Scoped); } - public override async Task InitializeAsync() - { - await base.InitializeAsync(); - - var networkProvidersConfig = new NetworkProvidersConfiguration - { - NetworkProviders = - [ - new NetworkProvider - { - Name = "default", - TypeString = "nat_overlay", - BridgeName = "br-nat", - Subnets = - [ - new NetworkProviderSubnet - { - Name = "default", - Network = "10.249.248.0/24", - Gateway = "10.249.248.1", - IpPools = - [ - new NetworkProviderIpPool - { - Name = "default", - FirstIp = "10.249.248.10", - NextIp = "10.249.248.12", - LastIp = "10.249.248.19" - }, - new NetworkProviderIpPool - { - Name = "second-provider-pool", - FirstIp = "10.249.248.20", - NextIp = "10.249.248.22", - LastIp = "10.249.248.29" - }, - ], - }, - new NetworkProviderSubnet - { - Name = "second-provider-subnet", - Network = "10.249.249.0/24", - Gateway = "10.249.249.1", - IpPools = - [ - new NetworkProviderIpPool - { - Name = "default", - FirstIp = "10.249.249.10", - NextIp = "10.249.249.12", - LastIp = "10.249.249.19" - }, - ], - }, - ], - }, - new NetworkProvider - { - Name = "second-overlay-provider", - TypeString = "overlay", - BridgeName = "br-second-nat", - Subnets = - [ - new NetworkProviderSubnet - { - Name = "default", - Network = "10.249.248.0/24", - Gateway = "10.249.248.1", - IpPools = - [ - new NetworkProviderIpPool - { - Name = "default", - FirstIp = "10.249.248.10", - NextIp = "10.249.248.12", - LastIp = "10.249.248.19" - }, - new NetworkProviderIpPool - { - Name = "second-provider-pool", - FirstIp = "10.249.248.20", - NextIp = "10.249.248.22", - LastIp = "10.249.248.29" - }, - ], - }, - ], - }, - new NetworkProvider - { - Name = "flat-provider", - TypeString = "flat", - }, - ] - }; - - _networkProviderManagerMock - .Setup(m => m.GetCurrentConfiguration()) - .Returns(Prelude.RightAsync( - networkProvidersConfig)); - - await WithScope(async (_, stateStore) => - { - var configRealizer = new NetworkProvidersConfigRealizer(stateStore); - await configRealizer.RealizeConfigAsync(networkProvidersConfig, default); - }); - } - protected override async Task SeedAsync(IStateStore stateStore) { await SeedDefaultTenantAndProject(); - - await stateStore.For().AddAsync(new CatletMetadata - { - Id = Guid.Parse(CatletMetadataId), - }); - - await stateStore.For().AddAsync(new Project() - { - Id = Guid.Parse(SecondProjectId), - Name = "second-project", - TenantId = EryphConstants.DefaultTenantId, - }); - - await stateStore.For().AddAsync( - new VirtualNetwork - { - Id = Guid.Parse(DefaultNetworkId), - ProjectId = EryphConstants.DefaultProjectId, - Name = EryphConstants.DefaultNetworkName, - Environment = EryphConstants.DefaultEnvironmentName, - NetworkProvider = EryphConstants.DefaultProviderName, - IpNetwork = "10.0.0.0/15", - Subnets = - [ - new VirtualNetworkSubnet - { - Id = Guid.Parse(DefaultSubnetId), - Name = EryphConstants.DefaultSubnetName, - IpNetwork = "10.0.0.0/16", - IpPools = - [ - new IpPool() - { - Id = Guid.NewGuid(), - Name = EryphConstants.DefaultIpPoolName, - IpNetwork = "10.0.0.0/16", - FirstIp = "10.0.0.10", - NextIp = "10.0.0.12", - LastIp = "10.0.0.19", - }, - new IpPool() - { - Id = Guid.NewGuid(), - Name = "second-pool", - IpNetwork = "10.0.0.0/16", - FirstIp = "10.0.1.10", - NextIp = "10.0.1.12", - LastIp = "10.0.1.19", - } - ], - }, - new VirtualNetworkSubnet - { - Id = Guid.Parse(SecondSubnetId), - Name = "second-subnet", - IpNetwork = "10.1.0.0/16", - IpPools = - [ - new IpPool() - { - Id = Guid.NewGuid(), - Name = EryphConstants.DefaultIpPoolName, - IpNetwork = "10.1.0.0/16", - FirstIp = "10.1.0.10", - NextIp = "10.1.0.12", - LastIp = "10.1.0.19", - } - ], - }, - ], - NetworkPorts = - [ - new ProviderRouterPort - { - Name = "provider", - ProviderName = EryphConstants.DefaultProviderName, - SubnetName = EryphConstants.DefaultSubnetName, - PoolName = EryphConstants.DefaultIpPoolName, - MacAddress = "42:00:42:00:00:01", - } - ], - }); - - await stateStore.For().AddAsync( - new VirtualNetwork - { - Id = Guid.Parse(SecondNetworkId), - ProjectId = EryphConstants.DefaultProjectId, - Name = "second-network", - Environment = EryphConstants.DefaultEnvironmentName, - NetworkProvider = EryphConstants.DefaultProviderName, - IpNetwork = "10.5.0.0/16", - Subnets = - [ - new VirtualNetworkSubnet - { - Id = Guid.Parse(SecondNetworkSubnetId), - Name = EryphConstants.DefaultSubnetName, - IpNetwork = "10.5.0.0/16", - IpPools = - [ - new IpPool() - { - Id = Guid.NewGuid(), - Name = EryphConstants.DefaultIpPoolName, - IpNetwork = "10.5.0.0/16", - FirstIp = "10.5.0.10", - NextIp = "10.5.0.12", - LastIp = "10.5.0.19", - } - ], - }, - ], - NetworkPorts = - [ - new ProviderRouterPort - { - Name = "provider", - ProviderName = EryphConstants.DefaultProviderName, - SubnetName = EryphConstants.DefaultSubnetName, - PoolName = "second-provider-pool", - MacAddress = "42:00:42:00:00:02", - } - ] - }); - - await stateStore.For().AddAsync( - new VirtualNetwork - { - Id = Guid.Parse(ThirdNetworkId), - ProjectId = EryphConstants.DefaultProjectId, - Name = "third-network", - Environment = EryphConstants.DefaultEnvironmentName, - NetworkProvider = EryphConstants.DefaultProviderName, - IpNetwork = "10.6.0.0/16", - Subnets = - [ - new VirtualNetworkSubnet - { - Id = Guid.Parse(ThirdNetworkSubnetId), - Name = EryphConstants.DefaultSubnetName, - IpNetwork = "10.6.0.0/16", - IpPools = - [ - new IpPool() - { - Id = Guid.NewGuid(), - Name = EryphConstants.DefaultIpPoolName, - IpNetwork = "10.6.0.0/16", - FirstIp = "10.6.0.10", - NextIp = "10.6.0.12", - LastIp = "10.6.0.19", - } - ], - }, - ], - NetworkPorts = - [ - new ProviderRouterPort - { - Name = "provider", - ProviderName = EryphConstants.DefaultProviderName, - SubnetName = "second-provider-subnet", - PoolName = EryphConstants.DefaultIpPoolName, - MacAddress = "42:00:42:00:00:03", - } - ] - }); - - await stateStore.For().AddAsync( - new VirtualNetwork - { - Id = Guid.Parse(SecondEnvironmentNetworkId), - ProjectId = EryphConstants.DefaultProjectId, - Name = EryphConstants.DefaultNetworkName, - Environment = "second-environment", - NetworkProvider = EryphConstants.DefaultProviderName, - IpNetwork = "10.10.0.0/16", - Subnets = - [ - new VirtualNetworkSubnet - { - Id = Guid.Parse(SecondEnvironmentSubnetId), - Name = EryphConstants.DefaultSubnetName, - IpNetwork = "10.10.0.0/16", - IpPools = - [ - new IpPool() - { - Id = Guid.NewGuid(), - Name = EryphConstants.DefaultIpPoolName, - IpNetwork = "10.10.0.0/16", - FirstIp = "10.10.0.10", - NextIp = "10.10.0.12", - LastIp = "10.10.0.19", - } - ], - }, - ], - NetworkPorts = - [ - new ProviderRouterPort - { - Name = "provider", - ProviderName = EryphConstants.DefaultProviderName, - SubnetName = EryphConstants.DefaultSubnetName, - PoolName = EryphConstants.DefaultIpPoolName, - MacAddress = "42:00:42:00:00:04", - } - ] - }); - - await stateStore.For().AddAsync( - new VirtualNetwork - { - Id = Guid.NewGuid(), - ProjectId = EryphConstants.DefaultProjectId, - Name = "second-provider", - Environment = EryphConstants.DefaultEnvironmentName, - NetworkProvider = "second-overlay-provider", - IpNetwork = "10.200.0.0/16", - Subnets = - [ - new VirtualNetworkSubnet - { - Id = Guid.NewGuid(), - Name = EryphConstants.DefaultSubnetName, - IpNetwork = "10.200.0.0/16", - IpPools = - [ - new IpPool() - { - Id = Guid.NewGuid(), - Name = EryphConstants.DefaultIpPoolName, - IpNetwork = "10.200.0.0/16", - FirstIp = "10.200.0.10", - NextIp = "10.200.0.12", - LastIp = "10.200.0.19", - } - ], - }, - ], - NetworkPorts = - [ - new ProviderRouterPort - { - Name = "provider", - ProviderName = "second-overlay-provider", - SubnetName = EryphConstants.DefaultSubnetName, - PoolName = EryphConstants.DefaultIpPoolName, - MacAddress = "42:00:42:00:00:06", - } - ] - }); - - await stateStore.For().AddAsync( - new VirtualNetwork - { - Id = Guid.Parse(SecondProjectNetworkId), - ProjectId = Guid.Parse(SecondProjectId), - Name = EryphConstants.DefaultNetworkName, - Environment = EryphConstants.DefaultEnvironmentName, - NetworkProvider = EryphConstants.DefaultProviderName, - IpNetwork = "10.100.0.0/16", - Subnets = - [ - new VirtualNetworkSubnet - { - Id = Guid.Parse(SecondProjectSubnetId), - Name = EryphConstants.DefaultSubnetName, - IpNetwork = "10.100.0.0/16", - IpPools = - [ - new IpPool() - { - Id = Guid.NewGuid(), - Name = EryphConstants.DefaultIpPoolName, - IpNetwork = "10.100.0.0/16", - FirstIp = "10.100.0.10", - NextIp = "10.100.0.12", - LastIp = "10.100.0.19", - } - ], - }, - ], - NetworkPorts = - [ - new ProviderRouterPort - { - Name = "provider", - ProviderName = EryphConstants.DefaultProviderName, - SubnetName = EryphConstants.DefaultSubnetName, - PoolName = EryphConstants.DefaultIpPoolName, - MacAddress = "42:00:42:00:00:05", - } - ] - }); - - await stateStore.For().AddAsync( - new VirtualNetwork - { - Id = Guid.Parse(FlatNetworkId), - ProjectId = Guid.Parse(DefaultProjectId), - Name = "flat-network", - Environment = EryphConstants.DefaultEnvironmentName, - NetworkProvider = "flat-provider", - }); } } \ No newline at end of file From 00027bafd6092e129e7fae2b6216b4978e79d5e1 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Tue, 26 Nov 2024 11:25:57 +0100 Subject: [PATCH 25/30] Improve network service --- .../Networks/NetworkSyncService.cs | 70 ++++++++----------- .../Networks/ProjectNetworkPlanBuilder.cs | 18 ++--- .../UpdateProjectNetworkPlanCommandHandler.cs | 7 +- 3 files changed, 41 insertions(+), 54 deletions(-) diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs index ab7c41a58..1c14f0da0 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs @@ -24,36 +24,24 @@ namespace Eryph.Modules.Controller.Networks; -internal class NetworkSyncService : INetworkSyncService +internal class NetworkSyncService( + Container container, + INetworkProviderManager providerManager) + : INetworkSyncService { - private readonly Container _container; - private readonly INetworkProviderManager _providerManager; - private readonly ILogger _log; - - public NetworkSyncService( - Container container, - INetworkProviderManager providerManager, - ILogger log) - { - _container = container; - _providerManager = providerManager; - _log = log; - } - public EitherAsync SyncNetworks(CancellationToken cancellationToken) => - from providerConfig in _providerManager.GetCurrentConfiguration() - from _ in SyncNetworks(providerConfig, cancellationToken) + from providerConfig in providerManager.GetCurrentConfiguration() + from _ in SyncNetworks(providerConfig) .Run().Map(fin => fin.ToEither()).AsTask().ToAsync() select Unit.Default; private Aff SyncNetworks( - NetworkProvidersConfiguration providersConfiguration, - CancellationToken cancellationToken) => + NetworkProvidersConfiguration providersConfiguration) => from _1 in RealizeProviderNetworks(providersConfiguration) from _2 in RealizeProjectNetworks(providersConfiguration) from neighbors in ApplyNetworkPlans(providersConfiguration) from _4 in use( - Eff(() => AsyncScopedLifestyle.BeginScope(_container)), + Eff(() => AsyncScopedLifestyle.BeginScope(container)), scope => from _ in unitAff let bus = scope.GetInstance() @@ -69,7 +57,7 @@ from __ in Aff(async () => await bus.Advanced.Topics.Publish( private Aff RealizeProviderNetworks( NetworkProvidersConfiguration providerConfig) => - use(Eff(() => AsyncScopedLifestyle.BeginScope(_container)), + use(Eff(() => AsyncScopedLifestyle.BeginScope(container)), scope => from _ in unitAff let configRealizer = scope.GetInstance() @@ -79,27 +67,26 @@ from __ in Aff(async () => await configRealizer.RealizeConfigAsync(providerConfi private Aff RealizeProjectNetworks( NetworkProvidersConfiguration providerConfig) => from projects in use( - Eff(() => AsyncScopedLifestyle.BeginScope(_container)), + Eff(() => AsyncScopedLifestyle.BeginScope(container)), scope => from _ in unitAff let stateStore = scope.GetInstance() from projects in stateStore.Read().IO.ListAsync().ToAff(e => e) select projects) - from _s in projects + from errors in projects .Map(p => RealizeProjectNetworks(p, providerConfig) - .IfFail(e => - { - _log.LogError(e, "Failed to save network changes for project {ProjectName} ({ProjectId})", - p.Name, p.Id); - return unit; - })) + .Map(_ => Option.None) + .IfFail(e => Error.New($"Failed to save network changes for project {p.Name}({p.Id}).", e))) .SequenceSerial() + from _ in errors.Somes().Match( + Empty: () => unitAff, + Seq: errors => FailAff(Error.New("Failed to save network changes for projects.", Error.Many(errors)))) select unit; private Aff RealizeProjectNetworks( Project project, NetworkProvidersConfiguration providerConfig) => - use(Eff(() => AsyncScopedLifestyle.BeginScope(_container)), + use(Eff(() => AsyncScopedLifestyle.BeginScope(container)), scope => from _ in unitAff let stateStore = scope.GetInstance() @@ -115,27 +102,28 @@ from ___ in Aff(async () => await stateStore.SaveChangesAsync().ToUnit()) private Aff> ApplyNetworkPlans( NetworkProvidersConfiguration providerConfig) => from projects in use( - Eff(() => AsyncScopedLifestyle.BeginScope(_container)), + Eff(() => AsyncScopedLifestyle.BeginScope(container)), scope => from _ in unitAff let stateStore = scope.GetInstance() from projects in stateStore.Read().IO.ListAsync().ToAff(e => e) select projects) - from neighbors in projects + from results in projects .Map(p => ApplyNetworkPlan(p.Id, providerConfig) - .IfFail(e => - { - _log.LogError(e, "Failed to apply network plan for project {ProjectName}({ProjectId})", - p.Name, p.Id); - return Seq(); - })) + .Map(r => (Neighbors: r, Error: Option.None)) + .IfFail(e => ( + Seq(), + Error.New($"Failed to apply network plan for project {p.Name}({p.Id}).", e)))) .SequenceSerial() - select neighbors.Flatten(); + from _ in results.Map(r => r.Error).Somes().Match( + Empty: () => unitAff, + Seq: errors => FailAff(Error.New("Failed to apply network plans for projects.", Error.Many(errors)))) + select results.Map(r => r.Neighbors).Flatten(); private Aff> ApplyNetworkPlan( Guid projectId, NetworkProvidersConfiguration providerConfig) => - use(Eff(() => AsyncScopedLifestyle.BeginScope(_container)), + use(Eff(() => AsyncScopedLifestyle.BeginScope(container)), scope => from _ in unitAff let sysEnv = scope.GetInstance() @@ -159,7 +147,7 @@ public EitherAsync ValidateChanges(NetworkProvider[] networkPro { async Task ValidateChangesAsync() { - await using var scope = AsyncScopedLifestyle.BeginScope(_container); + await using var scope = AsyncScopedLifestyle.BeginScope(container); var validationService = scope.GetInstance(); var stateStore = scope.GetInstance(); diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs b/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs index dd28de475..3b20c0e46 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs @@ -5,12 +5,12 @@ using System.Threading; using Dbosoft.OVN; using Dbosoft.OVN.Model.OVN; -using Eryph.Core; using Eryph.Core.Network; using Eryph.StateDb; using Eryph.StateDb.Model; using LanguageExt; using LanguageExt.Common; + using static LanguageExt.Prelude; namespace Eryph.Modules.Controller.Networks; @@ -19,7 +19,6 @@ internal class ProjectNetworkPlanBuilder( IStateStore stateStore) : IProjectNetworkPlanBuilder { - private sealed record FloatingPortInfo(FloatingNetworkPort Port); private sealed record ProviderRouterPortInfo(ProviderRouterPort Port, VirtualNetwork Network, ProviderSubnetInfo Subnet); @@ -142,17 +141,20 @@ from plans in ports.Map(portInfo => AddProviderRouterPort(networkPlan, portInfo) private static EitherAsync AddProviderRouterPort( NetworkPlan networkPlan, ProviderRouterPortInfo portInfo) => + from _ in RightAsync(unit) + let providerName = portInfo.Subnet.Subnet.ProviderName + let subnetName = portInfo.Subnet.Subnet.Name from externalIpAssignment in portInfo.Port.IpAssignments .ToSeq().HeadOrNone() - .ToEitherAsync(Error.New($"The provider port for provider '' has no IP Address assigned.")) - from externalNetwork in Try(() => IPNetwork2.Parse(portInfo.Subnet.Subnet.IpNetwork)) - .ToEither(_ => Error.New($"The provider port for provider '' has an invalid IP Network assigned.")) - .ToAsync() + .ToEitherAsync(Error.New($"The port for provider '{providerName}' has no IP Address assigned.")) from parsedExternalIp in Try(() => IPAddress.Parse(externalIpAssignment.IpAddress)) - .ToEither(_ => Error.New($"The provider port for provider '' has an invalid IP Address assigned.")) + .ToEither(_ => Error.New($"The port for provider '{providerName}' has an invalid IP Address assigned.")) + .ToAsync() + from externalNetwork in Try(() => IPNetwork2.Parse(portInfo.Subnet.Subnet.IpNetwork)) + .ToEither(_ => Error.New($"The subnet '{subnetName}' of the provider '{providerName}' has an invalid IP Network assigned.")) .ToAsync() from gatewayIp in Try(() => IPAddress.Parse(portInfo.Subnet.Config.Gateway)) - .ToEither(_ => Error.New($"The provider port for provider '' has an invalid gateway IP Address assigned.")) + .ToEither(_ => Error.New($"The subnet '{subnetName}' of the provider '{providerName}' has an invalid gateway IP Address assigned.")) .ToAsync() let updatedPlan = networkPlan .AddRouterPort($"externalNet-{networkPlan.Id}-{portInfo.Network.NetworkProvider}", diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateProjectNetworkPlanCommandHandler.cs b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateProjectNetworkPlanCommandHandler.cs index f6dba72b8..1fe56e766 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/UpdateProjectNetworkPlanCommandHandler.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/UpdateProjectNetworkPlanCommandHandler.cs @@ -8,10 +8,8 @@ using Eryph.Core; using Eryph.Messages.Resources.Catlets.Events; using Eryph.Messages.Resources.Networks.Commands; -using Eryph.StateDb; using JetBrains.Annotations; using LanguageExt; -using LanguageExt.Common; using Microsoft.Extensions.Logging; using Rebus.Handlers; @@ -26,7 +24,6 @@ public class UpdateProjectNetworkPlanCommandHandler( ILogger logger, INetworkProviderManager networkProviderManager, IProjectNetworkPlanBuilder planBuilder, - IStateStore stateStore, ITaskMessaging messaging) : IHandleMessages> { @@ -45,7 +42,7 @@ from networkPlan in planBuilder.GenerateNetworkPlan(projectId, providerConfig).T from appliedPlan in use( Eff(() => new CancellationTokenSource(TimeSpan.FromMinutes(5))), cancelSource => - from _ in SuccessAff(unit) + from _ in SuccessAff(unit) let networkPlanRealizer = new NetworkPlanRealizer( new OVNControlTool(sysEnvironment, ovnSettings.NorthDBConnection), logger) @@ -63,4 +60,4 @@ from appliedPlan in networkPlanRealizer.ApplyNetworkPlan(networkPlan, cancelSour .ToArray() } select response; -} \ No newline at end of file +} From 01f86b689d1054dc2400c0651b97c53530856516 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Tue, 26 Nov 2024 13:22:48 +0100 Subject: [PATCH 26/30] Cleanup --- .../Networks/NetworkConfigRealizer.cs | 12 ++++++------ .../Networks/ProjectNetworkPlanBuilder.cs | 5 +++-- .../Networks/NetworkChangeOperationBuilder.cs | 2 +- .../Networks/NetworkConfigRealizerTests.cs | 6 ------ 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs index 3db4e28ca..224925659 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkConfigRealizer.cs @@ -49,7 +49,7 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne var savedNetwork = savedNetworks.Find(x => GetEnvironmentName(x.Environment, x.Name) == networkEnvName); if (savedNetwork == null) { - log.LogDebug("Environment {env}: network {network} not found. Creating new network.", + log.LogDebug("Environment {Environment}: network {Network} not found. Creating new network.", networkConfig.Environment ?? EryphConstants.DefaultEnvironmentName, networkConfig.Name); var newNetwork = new VirtualNetwork @@ -86,11 +86,11 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne var networkEnvName = GetEnvironmentName(networkConfig.Environment, networkConfig.Name); var savedNetwork = savedNetworks.First(x => GetEnvironmentName(x.Environment, x.Name) == networkEnvName); - var providerName = networkConfig.Provider?.Name ?? "default"; - var providerSubnet = networkConfig.Provider?.Subnet ?? "default"; - var providerIpPool = networkConfig.Provider?.IpPool ?? "default"; + var providerName = networkConfig.Provider?.Name ?? EryphConstants.DefaultProviderName; + var providerSubnet = networkConfig.Provider?.Subnet ?? EryphConstants.DefaultSubnetName; + var providerIpPool = networkConfig.Provider?.IpPool ?? EryphConstants.DefaultIpPoolName; - log.LogDebug("Environment {env}: Updating network {network}", savedNetwork.Environment ?? "default", savedNetwork.Name); + log.LogDebug("Environment {Environment}: Updating network {Network}", savedNetwork.Environment, savedNetwork.Name); var networkProvider = providerConfig.NetworkProviders.FirstOrDefault(x => x.Name == providerName) ?? throw new InconsistentNetworkConfigException($"Network provider {providerName} not found."); @@ -200,7 +200,7 @@ public async Task UpdateNetwork(Guid projectId, ProjectNetworksConfig config, Ne !networkAddress.Contains(ipAddress)) { log.LogInformation("Environment {env}, Network {network}: network router ip assignment changed to {ipAddress}.", - savedNetwork.Environment ?? "default", savedNetwork.Name, networkAddress.FirstUsable); + savedNetwork.Environment, savedNetwork.Name, networkAddress.FirstUsable); savedNetwork.NetworkPorts.Remove(routerPort); routerPort = null; diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs b/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs index 3b20c0e46..889ce5668 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs @@ -91,7 +91,7 @@ private static EitherAsync> GetFloatingPorts( private EitherAsync> GetProviderSubnets( - IEnumerable overLayProviders, + Seq overLayProviders, Seq networks) { return networks.Map( @@ -114,7 +114,6 @@ private EitherAsync> GetProviderSubnets( .Map(all => all.Where(a => rs.Any(r => r.NetworkProvider == a.ProviderName && r.SubnetName == a.Name))) .Map(subnets => - //map subnets to ProviderSubnetInfo and add config subnets.Map(subnet => new ProviderSubnetInfo( subnet, @@ -195,6 +194,8 @@ private static NetworkPlan AddExternalNetworkPortUnique( string bridgeName, int? tag) { + // Include the bridge name in the port name. This ensures that the patch port + // of OVN network is always connected to the correct bridge. var name = $"SN-{switchName}-{externalNetwork}-{bridgeName}"; return plan with diff --git a/src/modules/src/Eryph.Modules.VmHostAgent/Networks/NetworkChangeOperationBuilder.cs b/src/modules/src/Eryph.Modules.VmHostAgent/Networks/NetworkChangeOperationBuilder.cs index 26d84271c..7f1d172a6 100644 --- a/src/modules/src/Eryph.Modules.VmHostAgent/Networks/NetworkChangeOperationBuilder.cs +++ b/src/modules/src/Eryph.Modules.VmHostAgent/Networks/NetworkChangeOperationBuilder.cs @@ -371,7 +371,7 @@ public Aff> RemoveInvalidNats( Seq newBridges) => from _ in unitAff from result in use( - Eff(fun(() => _logger.BeginScope("Method: {method}", nameof(RemoveInvalidNats)))).ToAff(), + Eff(fun(() => _logger.BeginScope("Method: {method}", nameof(RemoveInvalidNats)))), _ => netNat.Filter(n => n.Name.StartsWith("eryph_")) .Map(n => RemoveInvalidNat(n, newConfig, newBridges)) .SequenceSerial()) diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs index 2ebf93169..6f52e3f41 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkConfigRealizerTests.cs @@ -100,8 +100,6 @@ public class NetworkConfigRealizerTests(ITestOutputHelper outputHelper) ] }; - private readonly ITestOutputHelper _testOutput = outputHelper; - [Theory] [InlineData("default", "default", "default", "10.249.248.12")] [InlineData("default", "default", "second-provider-pool", "10.249.248.22")] @@ -184,8 +182,6 @@ await WithScope(async (_, stateStore) => network => { network.Name.Should().Be("test"); - - // TODO Is this correct or do we want to have a provider port? network.Subnets.Should().BeEmpty(); network.NetworkPorts.Should().BeEmpty(); }); @@ -361,8 +357,6 @@ await WithScope(async (_, stateStore) => network => { network.Name.Should().Be("test"); - - // TODO Is this correct or do we want to have a provider port? network.Subnets.Should().BeEmpty(); network.NetworkPorts.Should().BeEmpty(); }); From 9fbcbee5be92cd1f571904c958c1abc4882ca891 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Tue, 26 Nov 2024 14:48:22 +0100 Subject: [PATCH 27/30] Tweak network sync logic --- .../Networks/NetworkSyncService.cs | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs index 1c14f0da0..e7e19dc3e 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs @@ -39,20 +39,18 @@ private Aff SyncNetworks( NetworkProvidersConfiguration providersConfiguration) => from _1 in RealizeProviderNetworks(providersConfiguration) from _2 in RealizeProjectNetworks(providersConfiguration) - from neighbors in ApplyNetworkPlans(providersConfiguration) - from _4 in use( - Eff(() => AsyncScopedLifestyle.BeginScope(container)), - scope => - from _ in unitAff - let bus = scope.GetInstance() - from __ in Aff(async () => await bus.Advanced.Topics.Publish( - $"broadcast_{QueueNames.VMHostAgent}", - new NetworkNeighborsUpdateRequestedEvent - { - UpdatedAddresses = neighbors.ToArray() - }) - .ToUnit()) - select unit) + from applyResult in ApplyNetworkPlans(providersConfiguration) + from updateResult in UpdateNetworkNeighbors(applyResult.Neighbors) + .Map(_ => Option.None) + .IfFail(e => Error.New("Failed to update network neighbors.", e)) + // The application of the networks plans and the update of the network + // neighbors is best-effort. We try to do as much as possible before + // raising an error to ensure that as much as possible of the networking + // keeps working. + from _ in applyResult.Error.Append(updateResult.ToSeq()).Match( + Empty: () => unitAff, + Seq: errors => FailAff( + Error.New("Failed to apply network plans for projects.", Error.Many(errors)))) select unit; private Aff RealizeProviderNetworks( @@ -99,7 +97,7 @@ from __ in Aff(async () => await realizer.UpdateNetwork(project.Id, config, prov from ___ in Aff(async () => await stateStore.SaveChangesAsync().ToUnit()) select unit); - private Aff> ApplyNetworkPlans( + private Aff<(Seq Neighbors, Seq Error)> ApplyNetworkPlans( NetworkProvidersConfiguration providerConfig) => from projects in use( Eff(() => AsyncScopedLifestyle.BeginScope(container)), @@ -115,10 +113,9 @@ from results in projects Seq(), Error.New($"Failed to apply network plan for project {p.Name}({p.Id}).", e)))) .SequenceSerial() - from _ in results.Map(r => r.Error).Somes().Match( - Empty: () => unitAff, - Seq: errors => FailAff(Error.New("Failed to apply network plans for projects.", Error.Many(errors)))) - select results.Map(r => r.Neighbors).Flatten(); + let neighbors = results.Map(r => r.Neighbors).Flatten() + let error = results.Map(r => r.Error).Somes() + select (neighbors, error); private Aff> ApplyNetworkPlan( Guid projectId, @@ -143,6 +140,21 @@ from appliedNetworkPlan in networkPlanRealizer.ApplyNetworkPlan(networkPlan).ToA }).ToSeq() select updatedNetworkNeighbors); + private Aff UpdateNetworkNeighbors( + Seq neighbors) => + use(Eff(() => AsyncScopedLifestyle.BeginScope(container)), + scope => + from _ in unitAff + let bus = scope.GetInstance() + from __ in Aff(async () => await bus.Advanced.Topics.Publish( + $"broadcast_{QueueNames.VMHostAgent}", + new NetworkNeighborsUpdateRequestedEvent + { + UpdatedAddresses = neighbors.ToArray() + }) + .ToUnit()) + select unit); + public EitherAsync ValidateChanges(NetworkProvider[] networkProviders) { async Task ValidateChangesAsync() From ee2e091875019e6be268e920f08c4fb4d38f817d Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Tue, 26 Nov 2024 15:28:22 +0100 Subject: [PATCH 28/30] Support in-place update of provider IP pools --- .../Networks/NetplanBuilderSpecs.cs | 4 +- .../NetworkProvidersConfigRealizer.cs | 26 ++- .../NetworkProvidersConfigRealizerTests.cs | 178 ++++++++++++------ 3 files changed, 149 insertions(+), 59 deletions(-) diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/NetplanBuilderSpecs.cs b/src/modules/src/Eryph.Modules.Controller/Networks/NetplanBuilderSpecs.cs index acfb3f9a8..2c7ae190c 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/NetplanBuilderSpecs.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/NetplanBuilderSpecs.cs @@ -12,8 +12,8 @@ public sealed class GetAllProviderSubnets : Specification { public GetAllProviderSubnets() { - Query - .Include(x => x.IpPools); + Query.Include(x => x.IpPools) + .ThenInclude(p => p.IpAssignments); } } diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkProvidersConfigRealizer.cs b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkProvidersConfigRealizer.cs index 819d38e7e..f0a3aa28d 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkProvidersConfigRealizer.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkProvidersConfigRealizer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -62,12 +63,19 @@ public async Task RealizeConfigAsync( foreach (var ipPool in subnet.IpPools) { var ipPoolEntity = subnetEntity.IpPools.FirstOrDefault(x => - x.Name == ipPool.Name && x.FirstIp == ipPool.FirstIp && x.LastIp == ipPool.LastIp && - x.IpNetwork == subnetEntity.IpNetwork); + x.Name == ipPool.Name && x.IpNetwork == subnetEntity.IpNetwork); if (ipPoolEntity != null) { foundIpPools.Add(ipPoolEntity); + + ipPoolEntity.FirstIp = ipPool.FirstIp; + ipPoolEntity.NextIp = ipPool.NextIp ?? ipPool.FirstIp; + ipPoolEntity.LastIp = ipPool.LastIp; + + var invalidAssignments = FindInvalidAssignments(ipPoolEntity); + await _stateStore.For().DeleteRangeAsync( + invalidAssignments, cancellationToken); } else { @@ -93,4 +101,18 @@ await _stateStore.For().AddAsync(new IpPool await _stateStore.For().DeleteRangeAsync(removeSubnets, cancellationToken); await _stateStore.For().SaveChangesAsync(cancellationToken); } + + private IList FindInvalidAssignments(IpPool pool) + { + var firstIpNo = IPNetwork2.ToBigInteger(IPAddress.Parse(pool.FirstIp)); + var lastIpNo = IPNetwork2.ToBigInteger(IPAddress.Parse(pool.LastIp)); + + return pool.IpAssignments + .Filter(x => + { + var assignedIpNo = IPNetwork2.ToBigInteger(IPAddress.Parse(x.IpAddress!)); + return assignedIpNo < firstIpNo || assignedIpNo > lastIpNo; + }) + .ToList(); + } } diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs index dabfb431c..21b2022a3 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/NetworkProvidersConfigRealizerTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Ardalis.Specification; using Eryph.Core.Network; using Eryph.Modules.Controller.Networks; using Eryph.StateDb; @@ -17,7 +18,7 @@ public class NetworkProvidersConfigRealizerTests( ITestOutputHelper outputHelper) : InMemoryStateDbTestBase(outputHelper) { - private readonly NetworkProvidersConfiguration _config = new() + private readonly NetworkProvidersConfiguration _complexConfig = new() { NetworkProviders = [ @@ -77,22 +78,53 @@ public class NetworkProvidersConfigRealizerTests( ] }; + private readonly NetworkProvidersConfiguration _simpleConfig = new() + { + NetworkProviders = + [ + new NetworkProvider + { + Name = "default", + TypeString = "nat_overlay", + BridgeName = "br-nat", + Subnets = + [ + new NetworkProviderSubnet + { + Name = "default", + Network = "10.249.248.0/24", + Gateway = "10.249.248.1", + IpPools = + [ + new NetworkProviderIpPool + { + Name = "default", + FirstIp = "10.249.248.10", + NextIp = "10.249.248.12", + LastIp = "10.249.248.19" + }, + ], + }, + ], + }, + ], + }; + [Fact] public async Task RealizeConfigAsync_NoExistingSubnets_CreatesCorrectSubnets() { await WithScope(async (realizer, _) => { - await realizer.RealizeConfigAsync(_config, default); + await realizer.RealizeConfigAsync(_complexConfig, default); }); await WithScope(async (_, stateStore) => { - var subnets = await stateStore.For().ListAsync(); + var subnets = await stateStore.For().ListAsync(new GetAllSubnets()); subnets.Should().HaveCount(2); { var subnet = subnets.Should().ContainSingle(s => s.Name == "default").Subject; - await stateStore.LoadCollectionAsync(subnet, s => s.IpPools); subnet.IpNetwork.Should().Be("10.249.248.0/24"); subnet.IpPools.Should().HaveCount(2); @@ -110,7 +142,6 @@ await WithScope(async (_, stateStore) => { var subnet = subnets.Should().ContainSingle(s => s.Name == "second-provider-subnet").Subject; - await stateStore.LoadCollectionAsync(subnet, s => s.IpPools); subnet.IpNetwork.Should().Be("10.249.249.0/24"); subnet.IpPools.Should().HaveCount(1); @@ -128,7 +159,7 @@ public async Task RealizeConfigAsync_ExistingSubnets_RemovesOldSubnets() { await WithScope(async (realizer, _) => { - await realizer.RealizeConfigAsync(_config, default); + await realizer.RealizeConfigAsync(_complexConfig, default); }); await WithScope(async (_, stateStore) => @@ -142,60 +173,89 @@ await WithScope(async (_, stateStore) => await WithScope(async (realizer, _) => { - var updatedConfig = new NetworkProvidersConfiguration - { - NetworkProviders = - [ - new NetworkProvider - { - Name = "default", - TypeString = "nat_overlay", - BridgeName = "br-nat", - Subnets = - [ - new NetworkProviderSubnet - { - Name = "default", - Network = "10.249.248.0/24", - Gateway = "10.249.248.1", - IpPools = - [ - new NetworkProviderIpPool - { - Name = "default", - FirstIp = "10.249.248.10", - NextIp = "10.249.248.12", - LastIp = "10.249.248.19" - }, - ], - }, - ], - }, - ], - }; - await realizer.RealizeConfigAsync(updatedConfig, default); - - await WithScope(async (_, stateStore) => - { - var subnets = await stateStore.For().ListAsync(); - subnets.Should().HaveCount(1); + await realizer.RealizeConfigAsync(_simpleConfig, default); + }); - var subnet = subnets.Should().ContainSingle(s => s.Name == "default").Subject; - await stateStore.LoadCollectionAsync(subnet, s => s.IpPools); - - subnet.IpNetwork.Should().Be("10.249.248.0/24"); - subnet.IpPools.Should().HaveCount(1); + await WithScope(async (_, stateStore) => + { + var subnets = await stateStore.For().ListAsync(new GetAllSubnets()); + subnets.Should().SatisfyRespectively( + subnet => + { + subnet.Name.Should().Be("default"); + subnet.IpNetwork.Should().Be("10.249.248.0/24"); - var defaultPool = subnet.IpPools.Should().ContainSingle(p => p.Name == "default").Subject; - defaultPool.FirstIp.Should().Be("10.249.248.10"); - defaultPool.NextIp.Should().Be("10.249.248.12"); - defaultPool.LastIp.Should().Be("10.249.248.19"); - - }); + subnet.IpPools.Should().SatisfyRespectively( + pool => + { + pool.Name.Should().Be("default"); + pool.FirstIp.Should().Be("10.249.248.10"); + pool.NextIp.Should().Be("10.249.248.12"); + pool.LastIp.Should().Be("10.249.248.19"); + }); + }); }); } - // TODO Add test keeps existing subnet and IP pool when updating + [Fact] + public async Task RealizeConfigAsync_IpRangeOfExistingPoolIsChanged_ExistingPoolIsUpdated() + { + await WithScope(async (realizer, _) => + { + await realizer.RealizeConfigAsync(_simpleConfig, default); + }); + + Guid ipPoolId = Guid.Empty; + await WithScope(async (_, stateStore) => + { + var subnets = await stateStore.For().ListAsync(new GetAllSubnets()); + subnets.Should().SatisfyRespectively( + subnet => + { + subnet.Name.Should().Be("default"); + subnet.IpNetwork.Should().Be("10.249.248.0/24"); + + subnet.IpPools.Should().SatisfyRespectively( + pool => + { + pool.Name.Should().Be("default"); + pool.FirstIp.Should().Be("10.249.248.10"); + pool.NextIp.Should().Be("10.249.248.12"); + pool.LastIp.Should().Be("10.249.248.19"); + }); + }); + ipPoolId = subnets.Should().ContainSingle() + .Which.IpPools.Should().ContainSingle() + .Which.Id; + }); + + await WithScope(async (realizer, _) => + { + _simpleConfig.NetworkProviders[0].Subnets[0].IpPools[0].LastIp = "10.249.248.100"; + await realizer.RealizeConfigAsync(_simpleConfig, default); + }); + + await WithScope(async (_, stateStore) => + { + var subnets = await stateStore.For().ListAsync(new GetAllSubnets()); + subnets.Should().SatisfyRespectively( + subnet => + { + subnet.Name.Should().Be("default"); + subnet.IpNetwork.Should().Be("10.249.248.0/24"); + + subnet.IpPools.Should().SatisfyRespectively( + pool => + { + pool.Id.Should().Be(ipPoolId); + pool.Name.Should().Be("default"); + pool.FirstIp.Should().Be("10.249.248.10"); + pool.NextIp.Should().Be("10.249.248.12"); + pool.LastIp.Should().Be("10.249.248.100"); + }); + }); + }); + } private async Task WithScope(Func func) { @@ -209,4 +269,12 @@ protected override void AddSimpleInjector(SimpleInjectorAddOptions options) { options.Container.Register(); } + + private sealed class GetAllSubnets : Specification + { + public GetAllSubnets() + { + Query.Include(x => x.IpPools); + } + } } From 295bf7ea7160020ca2a80d612200f87785cdb6d7 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Tue, 26 Nov 2024 15:44:37 +0100 Subject: [PATCH 29/30] Cleanup --- .../NetworkProvidersConfigRealizer.cs | 4 +-- .../Networks/ProjectNetworkPlanBuilder.cs | 2 +- .../ProjectNetworkPlanBuilderTests.cs | 29 ++----------------- 3 files changed, 6 insertions(+), 29 deletions(-) diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkProvidersConfigRealizer.cs b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkProvidersConfigRealizer.cs index f0a3aa28d..6c5a62088 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkProvidersConfigRealizer.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkProvidersConfigRealizer.cs @@ -104,8 +104,8 @@ await _stateStore.For().AddAsync(new IpPool private IList FindInvalidAssignments(IpPool pool) { - var firstIpNo = IPNetwork2.ToBigInteger(IPAddress.Parse(pool.FirstIp)); - var lastIpNo = IPNetwork2.ToBigInteger(IPAddress.Parse(pool.LastIp)); + var firstIpNo = IPNetwork2.ToBigInteger(IPAddress.Parse(pool.FirstIp!)); + var lastIpNo = IPNetwork2.ToBigInteger(IPAddress.Parse(pool.LastIp!)); return pool.IpAssignments .Filter(x => diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs b/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs index 889ce5668..8c43d35c2 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/ProjectNetworkPlanBuilder.cs @@ -195,7 +195,7 @@ private static NetworkPlan AddExternalNetworkPortUnique( int? tag) { // Include the bridge name in the port name. This ensures that the patch port - // of OVN network is always connected to the correct bridge. + // of the OVN network is always connected to the correct bridge. var name = $"SN-{switchName}-{externalNetwork}-{bridgeName}"; return plan with diff --git a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProjectNetworkPlanBuilderTests.cs b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProjectNetworkPlanBuilderTests.cs index 6128dcb3f..ff49a8f30 100644 --- a/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProjectNetworkPlanBuilderTests.cs +++ b/src/modules/test/Eryph.Modules.Controller.Tests/Networks/ProjectNetworkPlanBuilderTests.cs @@ -26,29 +26,6 @@ public class ProjectNetworkPlanBuilderTests( ITestOutputHelper outputHelper) : InMemoryStateDbTestBase(outputHelper) { - private const string DefaultProjectId = "4b4a3fcf-b5ed-4a9a-ab6e-03852752095e"; - private const string SecondProjectId = "75c27daf-77c8-4b98-a072-a4706dceb422"; - - private const string DefaultNetworkId = "cb58fe00-3f64-4b66-b58e-23fb15df3cac"; - private const string DefaultSubnetId = "ed6697cd-836f-4da7-914b-b09ed1567934"; - private const string SecondSubnetId = "4f976208-613a-40d4-a284-d32cbd4a1b8e"; - - private const string SecondNetworkId = "e480a020-57d0-4443-a973-57aa0c95872e"; - private const string SecondNetworkSubnetId = "27ec11a4-5d6a-47da-9f9f-eb7486db38ea"; - - private const string ThirdNetworkId = "9016fa5b-e0c7-4626-b1ba-6dc21902d04f"; - private const string ThirdNetworkSubnetId = "106fa5c1-8cf1-4ccd-915a-f9dc230cc299"; - - private const string SecondEnvironmentNetworkId = "81a139e5-ab61-4fe3-b81f-59c11a665d22"; - private const string SecondEnvironmentSubnetId = "dc807357-50e7-4263-8298-0c97ff69f4cf"; - - private const string SecondProjectNetworkId = "c0043e88-8268-4ac0-b027-2fa37ad3168f"; - private const string SecondProjectSubnetId = "0c721846-5e2e-40a9-83d2-f1b75206ef84"; - - private const string FlatNetworkId = "98ff838a-a2c3-464d-8884-f348888ed804"; - - private const string CatletMetadataId = "15e2b061-c625-4469-9fe7-7c455058fcc0"; - private readonly NetworkProvidersConfiguration _networkProvidersConfig = new() { NetworkProviders = @@ -111,12 +88,12 @@ public async Task GenerateNetworkPlan_DefaultConfig_GeneratesValidNetworkPlan() await WithScope(async (builder, _) => { var result = await builder.GenerateNetworkPlan( - Guid.Parse(DefaultProjectId), + EryphConstants.DefaultProjectId, _networkProvidersConfig); var networkPlan = result.Should().BeRight().Subject; - networkPlan.Id.Should().Be(DefaultProjectId); + networkPlan.Id.Should().Be(EryphConstants.DefaultProjectId.ToString()); networkPlan.PlannedSwitchPorts.ToDictionary().Should().ContainKey( $"SN-externalNet-{networkPlan.Id}-default-default-br-nat"); }); @@ -143,4 +120,4 @@ protected override async Task SeedAsync(IStateStore stateStore) { await SeedDefaultTenantAndProject(); } -} \ No newline at end of file +} From 90fb4c84ae50d90dbe7cda09a0b302dc3b53d50d Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Tue, 26 Nov 2024 18:36:14 +0100 Subject: [PATCH 30/30] Configure missing floating IPs during network sync --- .../Specifications/VirtualNetworkSpecs.cs | 14 ++++++++++++++ .../Networks/NetworkSyncService.cs | 17 +++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/data/src/Eryph.StateDb/Specifications/VirtualNetworkSpecs.cs b/src/data/src/Eryph.StateDb/Specifications/VirtualNetworkSpecs.cs index 6a78c7ec3..f1013492f 100644 --- a/src/data/src/Eryph.StateDb/Specifications/VirtualNetworkSpecs.cs +++ b/src/data/src/Eryph.StateDb/Specifications/VirtualNetworkSpecs.cs @@ -28,6 +28,20 @@ public GetForProjectConfig(Guid projectId) } } + public sealed class GetForNetworkSync : Specification + { + public GetForNetworkSync(Guid projectId) + { + Query.Where(x => x.ProjectId == projectId) + .Include(x => x.NetworkPorts).ThenInclude(x => x.IpAssignments) + .Include(x => x.NetworkPorts).ThenInclude(x => x.FloatingPort) + .Include(x => x.Subnets) + .Include(x => x.RouterPort).ThenInclude(x => x!.FloatingPort) + .Include(x => x.Subnets) + .ThenInclude(x => x.IpPools); + } + } + public sealed class GetForChangeTracking : Specification { public GetForChangeTracking(Guid projectId) diff --git a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs index e7e19dc3e..b5b2128c2 100644 --- a/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs +++ b/src/modules/src/Eryph.Modules.Controller/Networks/NetworkSyncService.cs @@ -86,15 +86,24 @@ private Aff RealizeProjectNetworks( NetworkProvidersConfiguration providerConfig) => use(Eff(() => AsyncScopedLifestyle.BeginScope(container)), scope => - from _ in unitAff + from _1 in unitAff let stateStore = scope.GetInstance() + let ipManager = scope.GetInstance() from networks in stateStore.For().IO.ListAsync( - new VirtualNetworkSpecs.GetForProjectConfig(project.Id)) + new VirtualNetworkSpecs.GetForNetworkSync(project.Id)) .ToAff(e => e) from config in Eff(() => networks.ToNetworksConfig(project.Name)) let realizer = scope.GetInstance() - from __ in Aff(async () => await realizer.UpdateNetwork(project.Id, config, providerConfig).ToUnit()) - from ___ in Aff(async () => await stateStore.SaveChangesAsync().ToUnit()) + from _2 in Aff(async () => await realizer.UpdateNetwork(project.Id, config, providerConfig).ToUnit()) + // Configure any missing floating IPs. Some floating IPs might have been + // removed when the network provider configuration was updated. + from _3 in networks.SelectMany(n => n.NetworkPorts, (n, p) => (Provider: n.NetworkProvider, Port: p)) + .Map(t => from fp in Optional(t.Port.FloatingPort) + select (t.Provider, Port: fp)) + .Somes() + .Map(t => ipManager.ConfigureFloatingPortIps(t.Provider, t.Port).ToAff(e => e)) + .SequenceSerial() + from _4 in Aff(async () => await stateStore.SaveChangesAsync().ToUnit()) select unit); private Aff<(Seq Neighbors, Seq Error)> ApplyNetworkPlans(