diff --git a/KustoSchemaTools.Tests/Changes/ClusterChangesTest.cs b/KustoSchemaTools.Tests/Changes/ClusterChangesTest.cs index 329bfbc..991f8d1 100644 --- a/KustoSchemaTools.Tests/Changes/ClusterChangesTest.cs +++ b/KustoSchemaTools.Tests/Changes/ClusterChangesTest.cs @@ -18,8 +18,8 @@ public ClusterChangesTests() public void GenerateChanges_WithIdenticalPolicies_ShouldDetectNoChanges() { // Arrange - var oldCluster = CreateClusterWithPolicy(0.2, 1, 2, 3); - var newCluster = CreateClusterWithPolicy(0.2, 1, 2, 3); + var oldCluster = CreateClusterWithPolicy(0.2, 1, 2); + var newCluster = CreateClusterWithPolicy(0.2, 1, 2); // Act var changeSet = ClusterChanges.GenerateChanges(oldCluster, newCluster, _loggerMock.Object); @@ -32,8 +32,8 @@ public void GenerateChanges_WithIdenticalPolicies_ShouldDetectNoChanges() public void GenerateChanges_WithSingleChange_ShouldDetectChangeAndCreateScript() { // Arrange - var oldCluster = CreateClusterWithPolicy(0.2, 1, 2, 3); - var newCluster = CreateClusterWithPolicy(0.2, 1, 2, 5); + var oldCluster = CreateClusterWithPolicy(0.2, 1, 2); + var newCluster = CreateClusterWithPolicy(0.2, 1, 5); // Act var changeSet = ClusterChanges.GenerateChanges(oldCluster, newCluster, _loggerMock.Object); @@ -93,8 +93,7 @@ public void GenerateChanges_WithNullNewCapacityPolicy_ShouldNotGenerateChanges() private Cluster CreateClusterWithPolicy( double? ingestionCapacityCoreUtilizationCoefficient = null, int? materializedViewsCapacityClusterMaximumConcurrentOperations = null, - int? extentsRebuildClusterMaximumConcurrentOperations = null, - int? extentsRebuildMaximumConcurrentOperationsPerNode = null + int? materializedViewsCapacityClusterMinimumConcurrentOperations = null ) { return new Cluster @@ -104,11 +103,7 @@ private Cluster CreateClusterWithPolicy( MaterializedViewsCapacity = new MaterializedViewsCapacity { ClusterMaximumConcurrentOperations = materializedViewsCapacityClusterMaximumConcurrentOperations, - ExtentsRebuildCapacity = (extentsRebuildClusterMaximumConcurrentOperations != null || extentsRebuildMaximumConcurrentOperationsPerNode != null) ? new ExtentsRebuildCapacity - { - ClusterMaximumConcurrentOperations = extentsRebuildClusterMaximumConcurrentOperations, - MaximumConcurrentOperationsPerNode = extentsRebuildMaximumConcurrentOperationsPerNode - } : null + ClusterMinimumConcurrentOperations = materializedViewsCapacityClusterMinimumConcurrentOperations }, IngestionCapacity = new IngestionCapacity { diff --git a/KustoSchemaTools.Tests/KustoClusterOrchestratorTests.cs b/KustoSchemaTools.Tests/KustoClusterOrchestratorTests.cs index 6461d0f..8e05231 100644 --- a/KustoSchemaTools.Tests/KustoClusterOrchestratorTests.cs +++ b/KustoSchemaTools.Tests/KustoClusterOrchestratorTests.cs @@ -3,13 +3,6 @@ using KustoSchemaTools.Parser; using Microsoft.Extensions.Logging; using Moq; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using Xunit; -using System.Data; -using System; -using System.Linq; using Kusto.Data.Common; namespace KustoSchemaTools.Tests @@ -28,10 +21,9 @@ public KustoClusterOrchestratorTests() kustoClusterHandlerFactoryMock = new Mock(); yamlClusterHandlerFactoryMock = new Mock(); - // Create mock for KustoClusterHandler - var kustoClientMock = new Mock("test.eastus"); + var adminClientMock = new Mock(); var kustoLoggerMock = new Mock>(); - kustoHandlerMock = new Mock(kustoClientMock.Object, kustoLoggerMock.Object, "test", "test.eastus"); + kustoHandlerMock = new Mock(adminClientMock.Object, kustoLoggerMock.Object, "test", "test.eastus"); orchestrator = new KustoClusterOrchestrator( loggerMock.Object, @@ -57,12 +49,10 @@ private Clusters CreateClustersWithCapacityPolicy(ClusterCapacityPolicy? capacit private void SetupMockHandler(Cluster kustoCluster) { - // Configure the handler factory to return our mock handler kustoClusterHandlerFactoryMock .Setup(f => f.Create("test", "test.eastus")) .Returns(kustoHandlerMock.Object); - // Set up the mock handler to return our test cluster kustoHandlerMock .Setup(h => h.LoadAsync()) .ReturnsAsync(kustoCluster); @@ -106,8 +96,7 @@ private Clusters CreateMultipleClusters() private void SetupMultipleClusterMocks() { - // Mock for cluster1 - var kustoHandler1Mock = new Mock(new Mock("cluster1.eastus").Object, new Mock>().Object, "cluster1", "cluster1.eastus"); + var kustoHandler1Mock = new Mock(new Mock().Object, new Mock>().Object, "cluster1", "cluster1.eastus"); var kustoCluster1 = new Cluster { Name = "cluster1", @@ -128,8 +117,7 @@ private void SetupMultipleClusterMocks() .Setup(h => h.LoadAsync()) .ReturnsAsync(kustoCluster1); - // Mock for cluster2 - same as config, no changes - var kustoHandler2Mock = new Mock(new Mock("cluster2.westus").Object, new Mock>().Object, "cluster2", "cluster2.westus"); + var kustoHandler2Mock = new Mock(new Mock().Object, new Mock>().Object, "cluster2", "cluster2.westus"); var kustoCluster2 = new Cluster { Name = "cluster2", @@ -404,7 +392,7 @@ public async Task GenerateChangesFromFileAsync_ValidYamlFile_ReturnsChanges() .Returns(new YamlClusterHandler(yamlFilePath)); // Set up mocks for the clusters defined in the YAML file - var kustoHandler1Mock = new Mock(new Mock("test1.eastus").Object, new Mock>().Object, "test1", "test1.eastus"); + var kustoHandler1Mock = new Mock(new Mock().Object, new Mock>().Object, "test1", "test1.eastus"); var kustoCluster1 = new Cluster { Name = "test1", @@ -425,7 +413,7 @@ public async Task GenerateChangesFromFileAsync_ValidYamlFile_ReturnsChanges() .Setup(h => h.LoadAsync()) .ReturnsAsync(kustoCluster1); - var kustoHandler2Mock = new Mock(new Mock("test2.eastus").Object, new Mock>().Object, "test2", "test2.eastus"); + var kustoHandler2Mock = new Mock(new Mock().Object, new Mock>().Object, "test2", "test2.eastus"); var kustoCluster2 = new Cluster { Name = "test2", @@ -568,8 +556,8 @@ public async Task GenerateChangesFromFileAsync_VerifyLoggingCalled() .Returns(new YamlClusterHandler(yamlFilePath)); // Set up a simple mock for the clusters - var kustoHandler1Mock = new Mock(new Mock("test1.eastus").Object, new Mock>().Object, "test1", "test1.eastus"); - var kustoHandler2Mock = new Mock(new Mock("test2.eastus").Object, new Mock>().Object, "test2", "test2.eastus"); + var kustoHandler1Mock = new Mock(new Mock().Object, new Mock>().Object, "test1", "test1.eastus"); + var kustoHandler2Mock = new Mock(new Mock().Object, new Mock>().Object, "test2", "test2.eastus"); kustoClusterHandlerFactoryMock .Setup(f => f.Create("test1", "test1.eastus")) @@ -639,7 +627,7 @@ public async Task GenerateChangesFromFileAsync_LoadAsyncThrowsException_Propagat .Setup(f => f.Create(yamlFilePath)) .Returns(new YamlClusterHandler(yamlFilePath)); - var kustoHandler1Mock = new Mock(new Mock("test1.eastus").Object, new Mock>().Object, "test1", "test1.eastus"); + var kustoHandler1Mock = new Mock(new Mock().Object, new Mock>().Object, "test1", "test1.eastus"); kustoClusterHandlerFactoryMock .Setup(f => f.Create("test1", "test1.eastus")) diff --git a/KustoSchemaTools.Tests/Parser/KustoClusterHandlerTests.cs b/KustoSchemaTools.Tests/Parser/KustoClusterHandlerTests.cs new file mode 100644 index 0000000..a16866a --- /dev/null +++ b/KustoSchemaTools.Tests/Parser/KustoClusterHandlerTests.cs @@ -0,0 +1,390 @@ +using KustoSchemaTools.Changes; +using KustoSchemaTools.Model; +using Microsoft.Extensions.Logging; +using Moq; +using Kusto.Data.Common; +using System.Data; + +namespace KustoSchemaTools.Tests.Parser +{ + public class KustoClusterHandlerTests + { + private readonly Mock> _loggerMock; + private readonly Mock _adminClientMock; + private readonly KustoClusterHandler _handler; + + public KustoClusterHandlerTests() + { + _loggerMock = new Mock>(); + _adminClientMock = new Mock(); + + _handler = new KustoClusterHandler( + _adminClientMock.Object, + _loggerMock.Object, + "test-cluster", + "test.eastus" + ); + } + + [Fact] + public async Task WriteAsync_WithEmptyChangeSet_ReturnsEmptyResult() + { + // Arrange + var changeSet = new ClusterChangeSet("test-cluster", new Cluster(), new Cluster()); + + // Act + var result = await _handler.WriteAsync(changeSet); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + + // Verify no commands were executed + _adminClientMock.Verify(x => x.ExecuteControlCommandAsync( + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never); + } + + [Fact] + public async Task WriteAsync_WithInvalidScripts_SkipsInvalidScripts() + { + // Arrange + var changeSet = CreateChangeSetWithScripts(new[] + { + new DatabaseScriptContainer("policy", 0, ".alter cluster policy capacity", false) { IsValid = false }, + new DatabaseScriptContainer("policy", 1, ".show cluster policy capacity", false) { IsValid = true } + }); + + var mockResult = CreateMockDataReader(); + _adminClientMock + .Setup(x => x.ExecuteControlCommandAsync("", It.IsAny(), It.IsAny())) + .ReturnsAsync(mockResult.Object); + + // Act + var result = await _handler.WriteAsync(changeSet); + + // Assert + Assert.NotNull(result); + + // Verify only valid scripts were included in the execution + _adminClientMock.Verify(x => x.ExecuteControlCommandAsync( + "", + It.Is(cmd => cmd.Contains(".show cluster policy capacity") && !cmd.Contains(".alter cluster policy capacity")), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task WriteAsync_WithNegativeOrderScripts_SkipsNegativeOrderScripts() + { + // Arrange + var changeSet = CreateChangeSetWithScripts(new[] + { + new DatabaseScriptContainer("policy", -1, ".alter cluster policy capacity", false) { IsValid = true }, + new DatabaseScriptContainer("policy", 0, ".show cluster policy capacity", false) { IsValid = true } + }); + + var mockResult = CreateMockDataReader(); + _adminClientMock + .Setup(x => x.ExecuteControlCommandAsync("", It.IsAny(), It.IsAny())) + .ReturnsAsync(mockResult.Object); + + // Act + var result = await _handler.WriteAsync(changeSet); + + // Assert + Assert.NotNull(result); + + // Verify negative order scripts were excluded + _adminClientMock.Verify(x => x.ExecuteControlCommandAsync( + "", + It.Is(cmd => cmd.Contains(".show cluster policy capacity") && !cmd.Contains(".alter cluster policy capacity")), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task WriteAsync_WithMultipleValidScripts_ExecutesInCorrectOrder() + { + // Arrange + var changeSet = CreateChangeSetWithScripts(new[] + { + new DatabaseScriptContainer("policy", 2, ".alter cluster policy capacity script2", false) { IsValid = true }, + new DatabaseScriptContainer("policy", 0, ".alter cluster policy capacity script0", false) { IsValid = true }, + new DatabaseScriptContainer("policy", 1, ".alter cluster policy capacity script1", false) { IsValid = true } + }); + + var mockResult = CreateMockDataReader(); + _adminClientMock + .Setup(x => x.ExecuteControlCommandAsync("", It.IsAny(), It.IsAny())) + .ReturnsAsync(mockResult.Object); + + // Act + var result = await _handler.WriteAsync(changeSet); + + // Assert + Assert.NotNull(result); + + // Verify scripts were executed in order (script0, script1, script2) + _adminClientMock.Verify(x => x.ExecuteControlCommandAsync( + "", + It.Is(cmd => + cmd.Contains("script0") && + cmd.Contains("script1") && + cmd.Contains("script2") && + cmd.IndexOf("script0") < cmd.IndexOf("script1") && + cmd.IndexOf("script1") < cmd.IndexOf("script2")), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task WriteAsync_WithValidScripts_GeneratesCorrectClusterScript() + { + // Arrange + var changeSet = CreateChangeSetWithScripts(new[] + { + new DatabaseScriptContainer("policy", 0, ".alter cluster policy capacity", false) { IsValid = true }, + new DatabaseScriptContainer("policy", 1, ".show cluster policy capacity", false) { IsValid = true } + }); + + var mockResult = CreateMockDataReader(); + _adminClientMock + .Setup(x => x.ExecuteControlCommandAsync("", It.IsAny(), It.IsAny())) + .ReturnsAsync(mockResult.Object); + + // Act + var result = await _handler.WriteAsync(changeSet); + + // Assert + Assert.NotNull(result); + + // Verify the correct cluster script format was generated + _adminClientMock.Verify(x => x.ExecuteControlCommandAsync( + "", + It.Is(cmd => + cmd.StartsWith(".execute cluster script with(ContinueOnErrors = true) <|") && + cmd.Contains(".alter cluster policy capacity") && + cmd.Contains(".show cluster policy capacity")), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task WriteAsync_WithMixedValidityAndOrder_FiltersCorrectly() + { + // Arrange + var changeSet = CreateChangeSetWithScripts(new[] + { + new DatabaseScriptContainer("policy", -1, "script1", false) { IsValid = true }, // Filtered out (negative order) + new DatabaseScriptContainer("policy", 0, "script2", false) { IsValid = false }, // Filtered out (invalid) + new DatabaseScriptContainer("policy", 1, "script3", false) { IsValid = true }, // Should be included + new DatabaseScriptContainer("policy", 2, "script4", false) { IsValid = null }, // Filtered out (not explicitly valid) + new DatabaseScriptContainer("policy", 3, "script5", false) { IsValid = true } // Should be included + }); + + var mockResult = CreateMockDataReader(); + _adminClientMock + .Setup(x => x.ExecuteControlCommandAsync("", It.IsAny(), It.IsAny())) + .ReturnsAsync(mockResult.Object); + + // Act + var result = await _handler.WriteAsync(changeSet); + + // Assert + Assert.NotNull(result); + + // Verify only script3 and script5 were included + _adminClientMock.Verify(x => x.ExecuteControlCommandAsync( + "", + It.Is(cmd => + cmd.Contains("script3") && + cmd.Contains("script5") && + !cmd.Contains("script1") && + !cmd.Contains("script2") && + !cmd.Contains("script4")), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task WriteAsync_CallsExecuteControlCommandAsync() + { + // Arrange + var changeSet = CreateChangeSetWithScripts(new[] + { + new DatabaseScriptContainer("policy", 0, ".alter cluster policy capacity", false) { IsValid = true } + }); + + var mockResult = CreateMockDataReader(); + _adminClientMock + .Setup(x => x.ExecuteControlCommandAsync("", It.IsAny(), It.IsAny())) + .ReturnsAsync(mockResult.Object); + + // Act + var result = await _handler.WriteAsync(changeSet); + + // Assert + Assert.NotNull(result); + _adminClientMock.Verify(x => x.ExecuteControlCommandAsync( + "", + It.IsAny(), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task LoadAsync_WithCapacityPolicy_ReturnsClusterWithPolicy() + { + // Arrange + var policyJson = """ + { + "IngestionCapacity": { + "ClusterMaximumConcurrentOperations": 500, + "CoreUtilizationCoefficient": 0.75 + }, + "MaterializedViewsCapacity": { + "ClusterMinimumConcurrentOperations": 10, + "ClusterMaximumConcurrentOperations": 100 + }, + "MirroringCapacity": { + "ClusterMaximumConcurrentOperations": 50 + } + } + """; + + var mockReader = new Mock(); + mockReader.SetupSequence(x => x.Read()) + .Returns(true) // First call returns true (data available) + .Returns(false); // Second call returns false (no more data) + mockReader.Setup(x => x["Policy"]).Returns(policyJson); + + _adminClientMock + .Setup(x => x.ExecuteControlCommandAsync("", ".show cluster policy capacity", It.IsAny())) + .ReturnsAsync(mockReader.Object); + + // Act + var result = await _handler.LoadAsync(); + + // Assert + Assert.NotNull(result); + Assert.Equal("test-cluster", result.Name); + Assert.Equal("test.eastus", result.Url); + Assert.NotNull(result.CapacityPolicy); + + Assert.NotNull(result.CapacityPolicy.IngestionCapacity); + Assert.Equal(500, result.CapacityPolicy.IngestionCapacity.ClusterMaximumConcurrentOperations); + Assert.Equal(0.75, result.CapacityPolicy.IngestionCapacity.CoreUtilizationCoefficient); + + Assert.NotNull(result.CapacityPolicy.MaterializedViewsCapacity); + Assert.Equal(10, result.CapacityPolicy.MaterializedViewsCapacity.ClusterMinimumConcurrentOperations); + Assert.Equal(100, result.CapacityPolicy.MaterializedViewsCapacity.ClusterMaximumConcurrentOperations); + + Assert.NotNull(result.CapacityPolicy.MirroringCapacity); + Assert.Equal(50, result.CapacityPolicy.MirroringCapacity.ClusterMaximumConcurrentOperations); + + // Verify the correct command was executed + _adminClientMock.Verify(x => x.ExecuteControlCommandAsync( + "", + ".show cluster policy capacity", + It.IsAny()), Times.Once); + } + + [Fact] + public async Task LoadAsync_WithNoPolicyData_ReturnsClusterWithoutPolicy() + { + // Arrange + var mockReader = new Mock(); + mockReader.Setup(x => x.Read()).Returns(false); // No data + + _adminClientMock + .Setup(x => x.ExecuteControlCommandAsync("", ".show cluster policy capacity", It.IsAny())) + .ReturnsAsync(mockReader.Object); + + // Act + var result = await _handler.LoadAsync(); + + // Assert + Assert.NotNull(result); + Assert.Equal("test-cluster", result.Name); + Assert.Equal("test.eastus", result.Url); + Assert.Null(result.CapacityPolicy); + + // Verify the correct command was executed + _adminClientMock.Verify(x => x.ExecuteControlCommandAsync( + "", + ".show cluster policy capacity", + It.IsAny()), Times.Once); + } + + [Fact] + public async Task LoadAsync_WithEmptyPolicyJson_ReturnsClusterWithoutPolicy() + { + // Arrange + var mockReader = new Mock(); + mockReader.SetupSequence(x => x.Read()) + .Returns(true) // First call returns true (data available) + .Returns(false); // Second call returns false (no more data) + mockReader.Setup(x => x["Policy"]).Returns(""); // Empty policy + + _adminClientMock + .Setup(x => x.ExecuteControlCommandAsync("", ".show cluster policy capacity", It.IsAny())) + .ReturnsAsync(mockReader.Object); + + // Act + var result = await _handler.LoadAsync(); + + // Assert + Assert.NotNull(result); + Assert.Equal("test-cluster", result.Name); + Assert.Equal("test.eastus", result.Url); + Assert.Null(result.CapacityPolicy); + } + + [Fact] + public async Task LoadAsync_WithNullPolicyJson_ReturnsClusterWithoutPolicy() + { + // Arrange + var mockReader = new Mock(); + mockReader.SetupSequence(x => x.Read()) + .Returns(true) // First call returns true (data available) + .Returns(false); // Second call returns false (no more data) + mockReader.Setup(x => x["Policy"]).Returns((object?)null); // Null policy + + _adminClientMock + .Setup(x => x.ExecuteControlCommandAsync("", ".show cluster policy capacity", It.IsAny())) + .ReturnsAsync(mockReader.Object); + + // Act + var result = await _handler.LoadAsync(); + + // Assert + Assert.NotNull(result); + Assert.Equal("test-cluster", result.Name); + Assert.Equal("test.eastus", result.Url); + Assert.Null(result.CapacityPolicy); + } + + #region Helper Methods + + private ClusterChangeSet CreateChangeSetWithScripts(DatabaseScriptContainer[] scripts) + { + var changeSet = new ClusterChangeSet("test-cluster", new Cluster(), new Cluster()); + + // Create a mock change that contains the scripts + var mockChange = new Mock(); + mockChange.Setup(x => x.Scripts).Returns(scripts.ToList()); + + changeSet.Changes.Add(mockChange.Object); + + return changeSet; + } + + private Mock CreateMockDataReader() + { + var mockReader = new Mock(); + + // Make Read() return false to simulate no data + mockReader.Setup(x => x.Read()).Returns(false); + + return mockReader; + } + + #endregion + } +} diff --git a/KustoSchemaTools/KustoClusterOrchestrator.cs b/KustoSchemaTools/KustoClusterOrchestrator.cs index 2121dcb..c55501f 100644 --- a/KustoSchemaTools/KustoClusterOrchestrator.cs +++ b/KustoSchemaTools/KustoClusterOrchestrator.cs @@ -2,6 +2,7 @@ using KustoSchemaTools.Model; using KustoSchemaTools.Parser; using Microsoft.Extensions.Logging; +using Kusto.Data; namespace KustoSchemaTools { @@ -65,5 +66,44 @@ public async Task> GenerateChangesFromFileAsync(string cl return await GenerateChangesAsync(clusters); } + + /// + /// Loads cluster configurations from a YAML file, generates changes by comparing + /// them with the live Kusto clusters, and then applies those changes. + /// + /// The path to the YAML file containing cluster configurations. + /// A task representing the asynchronous apply operation. + public async Task> ApplyAsync(string clusterConfigFilePath) + { + Log.LogInformation($"Starting apply operation for cluster config file: {clusterConfigFilePath}"); + + // Generate the changes first + var changeSets = await GenerateChangesFromFileAsync(clusterConfigFilePath); + var allResults = new List(); + + // Apply changes for each cluster + foreach (var changeSet in changeSets) + { + Log.LogInformation($"Applying changes to cluster: {changeSet.Entity}"); + + if (changeSet.Changes.Count == 0) + { + Log.LogInformation($"No changes to apply for cluster: {changeSet.Entity}"); + continue; + } + + var clusterName = changeSet.To.Name; + var clusterUrl = changeSet.To.Url; + + var kustoHandler = KustoClusterHandlerFactory.Create(clusterName, clusterUrl); + var result = await kustoHandler.WriteAsync(changeSet); + + // Add the results from this cluster to the overall results list + allResults.AddRange(result); + } + + Log.LogInformation($"Finished applying. Total scripts executed: {allResults.Count}"); + return allResults; + } } } \ No newline at end of file diff --git a/KustoSchemaTools/Model/ClusterCapacityPolicy.cs b/KustoSchemaTools/Model/ClusterCapacityPolicy.cs index 65ec103..fa0809b 100644 --- a/KustoSchemaTools/Model/ClusterCapacityPolicy.cs +++ b/KustoSchemaTools/Model/ClusterCapacityPolicy.cs @@ -19,6 +19,7 @@ public class ClusterCapacityPolicy : IEquatable public StreamingIngestionPostProcessingCapacity? StreamingIngestionPostProcessingCapacity { get; set; } public PurgeStorageArtifactsCleanupCapacity? PurgeStorageArtifactsCleanupCapacity { get; set; } public PeriodicStorageArtifactsCleanupCapacity? PeriodicStorageArtifactsCleanupCapacity { get; set; } + public MirroringCapacity? MirroringCapacity { get; set; } public QueryAccelerationCapacity? QueryAccelerationCapacity { get; set; } public GraphSnapshotsCapacity? GraphSnapshotsCapacity { get; set; } @@ -37,6 +38,7 @@ public bool Equals(ClusterCapacityPolicy? other) EqualityComparer.Default.Equals(StreamingIngestionPostProcessingCapacity, other.StreamingIngestionPostProcessingCapacity) && EqualityComparer.Default.Equals(PurgeStorageArtifactsCleanupCapacity, other.PurgeStorageArtifactsCleanupCapacity) && EqualityComparer.Default.Equals(PeriodicStorageArtifactsCleanupCapacity, other.PeriodicStorageArtifactsCleanupCapacity) && + EqualityComparer.Default.Equals(MirroringCapacity, other.MirroringCapacity) && EqualityComparer.Default.Equals(QueryAccelerationCapacity, other.QueryAccelerationCapacity) && EqualityComparer.Default.Equals(GraphSnapshotsCapacity, other.GraphSnapshotsCapacity); } @@ -55,6 +57,7 @@ public override int GetHashCode() hc.Add(StreamingIngestionPostProcessingCapacity); hc.Add(PurgeStorageArtifactsCleanupCapacity); hc.Add(PeriodicStorageArtifactsCleanupCapacity); + hc.Add(MirroringCapacity); hc.Add(QueryAccelerationCapacity); hc.Add(GraphSnapshotsCapacity); return hc.ToHashCode(); @@ -192,40 +195,17 @@ public override string ToString() public class MaterializedViewsCapacity : IEquatable { + public int? ClusterMinimumConcurrentOperations { get; set; } public int? ClusterMaximumConcurrentOperations { get; set; } - public ExtentsRebuildCapacity? ExtentsRebuildCapacity { get; set; } public bool Equals(MaterializedViewsCapacity? other) { if (other is null) return false; - return ClusterMaximumConcurrentOperations == other.ClusterMaximumConcurrentOperations && - EqualityComparer.Default.Equals(ExtentsRebuildCapacity, other.ExtentsRebuildCapacity); + return ClusterMinimumConcurrentOperations == other.ClusterMinimumConcurrentOperations && + ClusterMaximumConcurrentOperations == other.ClusterMaximumConcurrentOperations; } public override bool Equals(object? obj) => Equals(obj as MaterializedViewsCapacity); - public override int GetHashCode() => HashCode.Combine(ClusterMaximumConcurrentOperations, ExtentsRebuildCapacity); - public override string ToString() - { - return JsonConvert.SerializeObject(this, new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore, - Formatting = Formatting.None - }); - } - } - - public class ExtentsRebuildCapacity : IEquatable - { - public int? ClusterMaximumConcurrentOperations { get; set; } - public int? MaximumConcurrentOperationsPerNode { get; set; } - - public bool Equals(ExtentsRebuildCapacity? other) - { - if (other is null) return false; - return ClusterMaximumConcurrentOperations == other.ClusterMaximumConcurrentOperations && - MaximumConcurrentOperationsPerNode == other.MaximumConcurrentOperationsPerNode; - } - public override bool Equals(object? obj) => Equals(obj as ExtentsRebuildCapacity); - public override int GetHashCode() => HashCode.Combine(ClusterMaximumConcurrentOperations, MaximumConcurrentOperationsPerNode); + public override int GetHashCode() => HashCode.Combine(ClusterMinimumConcurrentOperations, ClusterMaximumConcurrentOperations); public override string ToString() { return JsonConvert.SerializeObject(this, new JsonSerializerSettings @@ -322,6 +302,29 @@ public override string ToString() } } + public class MirroringCapacity : IEquatable + { + public int? ClusterMaximumConcurrentOperations { get; set; } + public double? CoreUtilizationCoefficient { get; set; } + + public bool Equals(MirroringCapacity? other) + { + if (other is null) return false; + return ClusterMaximumConcurrentOperations == other.ClusterMaximumConcurrentOperations && + CoreUtilizationCoefficient == other.CoreUtilizationCoefficient; + } + public override bool Equals(object? obj) => Equals(obj as MirroringCapacity); + public override int GetHashCode() => HashCode.Combine(ClusterMaximumConcurrentOperations, CoreUtilizationCoefficient); + public override string ToString() + { + return JsonConvert.SerializeObject(this, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.None + }); + } + } + public class QueryAccelerationCapacity : IEquatable { public int? ClusterMaximumConcurrentOperations { get; set; } diff --git a/KustoSchemaTools/Parser/KustoClusterHandler.cs b/KustoSchemaTools/Parser/KustoClusterHandler.cs index 3391cf1..ea2db1b 100644 --- a/KustoSchemaTools/Parser/KustoClusterHandler.cs +++ b/KustoSchemaTools/Parser/KustoClusterHandler.cs @@ -3,19 +3,21 @@ using Kusto.Data.Common; using Newtonsoft.Json; using KustoSchemaTools.Parser; +using KustoSchemaTools.Changes; +using Kusto.Data; namespace KustoSchemaTools { public class KustoClusterHandler { - private readonly KustoClient _client; + private readonly ICslAdminProvider _adminClient; private readonly ILogger _logger; private readonly string _clusterName; private readonly string _clusterUrl; - public KustoClusterHandler(KustoClient client, ILogger logger, string clusterName, string clusterUrl) + public KustoClusterHandler(ICslAdminProvider adminClient, ILogger logger, string clusterName, string clusterUrl) { - _client = client; + _adminClient = adminClient; _logger = logger; _clusterName = clusterName; _clusterUrl = clusterUrl; @@ -27,7 +29,7 @@ public virtual async Task LoadAsync() _logger.LogInformation("Loading cluster capacity policy..."); - using (var reader = await _client.AdminClient.ExecuteControlCommandAsync("", ".show cluster policy capacity", new ClientRequestProperties())) + using (var reader = await _adminClient.ExecuteControlCommandAsync("", ".show cluster policy capacity", new ClientRequestProperties())) { if (reader.Read()) { @@ -42,5 +44,36 @@ public virtual async Task LoadAsync() return cluster; } + + public virtual async Task> WriteAsync(ClusterChangeSet changeSet) + { + var scripts = changeSet.Changes + .SelectMany(itm => itm.Scripts) + .Where(itm => itm.Order >= 0) + .Where(itm => itm.IsValid == true) + .OrderBy(itm => itm.Order) + .ToList(); + + var result = await ExecuteClusterScriptAsync(scripts); + return result; + } + + private async Task> ExecuteClusterScriptAsync(List scripts) + { + if (scripts.Count == 0) + { + _logger.LogInformation("No scripts to execute."); + return new List(); + } + + var scriptTexts = scripts.Select(script => script.Text); + var script = ".execute cluster script with(ContinueOnErrors = true) <|" + Environment.NewLine + + string.Join(Environment.NewLine, scriptTexts); + + _logger.LogInformation($"Applying cluster script:\n{script}"); + + var result = await _adminClient.ExecuteControlCommandAsync("", script, new ClientRequestProperties()); + return result.As(); + } } } \ No newline at end of file diff --git a/KustoSchemaTools/Parser/KustoClusterHandlerFactory.cs b/KustoSchemaTools/Parser/KustoClusterHandlerFactory.cs index 60ec6af..52941de 100644 --- a/KustoSchemaTools/Parser/KustoClusterHandlerFactory.cs +++ b/KustoSchemaTools/Parser/KustoClusterHandlerFactory.cs @@ -16,7 +16,7 @@ public virtual KustoClusterHandler Create(string clusterName, string clusterUrl) { var client = new KustoClient(clusterUrl); var logger = _loggerFactory.CreateLogger(); - return new KustoClusterHandler(client, logger, clusterName, clusterUrl); + return new KustoClusterHandler(client.AdminClient, logger, clusterName, clusterUrl); } } } \ No newline at end of file diff --git a/KustoSchemaTools/Parser/YamlClusterHandlerFactory.cs b/KustoSchemaTools/Parser/YamlClusterHandlerFactory.cs index 93c5ee0..c5dbcc8 100644 --- a/KustoSchemaTools/Parser/YamlClusterHandlerFactory.cs +++ b/KustoSchemaTools/Parser/YamlClusterHandlerFactory.cs @@ -1,6 +1,8 @@ +using KustoSchemaTools.Parser; + namespace KustoSchemaTools { - public class YamlClusterHandlerFactory + public class YamlClusterHandlerFactory : IYamlClusterHandlerFactory { public virtual YamlClusterHandler Create(string path) {