diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index d5cab607dd..1817277376 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -71,7 +71,7 @@
-
+
diff --git a/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs b/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs
index 7ecd1b3490..d61bd4abcf 100644
--- a/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs
+++ b/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs
@@ -31,16 +31,16 @@ public class Product
public record QueueIdentity
{
- public string? NameHash { get; set; }
+ public required string NameHash { get; set; }
public string? ScopeHash { get; set; }
public string? OriginalLocation { get; set; }
}
public record LicensedEndpoint
{
- public string? ClientId { get; set; }
- public string? Name { get; set; }
- public int? Classification { get; set; }
+ public string ClientId { get; set; } = Guid.NewGuid().ToString();
+ public required string Name { get; set; }
+ public required int Classification { get; set; }
public string? EndpointSize { get; set; }
public QueueIdentity[] Queues { get; set; } = [];
@@ -48,9 +48,9 @@ public record LicensedEndpoint
public class LicensedEndpointAmendment
{
- public string? ClientId { get; set; }
- public string? Name { get; set; }
- public int? Classification { get; set; }
+ public required string ClientId { get; set; }
+ public required string Name { get; set; }
+ public required int Classification { get; set; }
public QueueIdentity[] AmendedQueues { get; set; } = [];
}
}
diff --git a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs
index 8ed4679bbe..0cb9a921c9 100644
--- a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs
+++ b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs
@@ -176,7 +176,8 @@ public Task SaveReportMasks(List reportMasks, CancellationToken cancella
return Task.CompletedTask;
}
- public Task GetLicensedEndpointDetails(CancellationToken cancellationToken = default) => Task.FromResult(endpointDetails?.Amend(endpointDetailsAmendment));
+ public Task GetLicensedEndpointDetails(bool amendWithUserChanges = true, CancellationToken cancellationToken = default) =>
+ Task.FromResult(amendWithUserChanges ? endpointDetails?.Amend(endpointDetailsAmendment) : endpointDetails);
public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken = default)
{
@@ -185,9 +186,12 @@ public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, Cancella
return Task.CompletedTask;
}
- public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment result, CancellationToken cancellationToken = default)
+ public Task GetLicensedEndpointDetailsAmendments(CancellationToken cancellationToken = default) =>
+ Task.FromResult(endpointDetailsAmendment);
+
+ public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment? result, CancellationToken cancellationToken = default)
{
- result.UnmappedQueues = [.. endpointDetailsAmendment?.UnmappedQueues ?? [], .. result.UnmappedQueues];
+ result?.UnmappedQueues = [.. endpointDetailsAmendment?.UnmappedQueues ?? [], .. result.UnmappedQueues];
endpointDetailsAmendment = result;
return Task.CompletedTask;
}
diff --git a/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs b/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs
index dab80b5fc4..36b73de7c2 100644
--- a/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs
+++ b/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs
@@ -39,7 +39,8 @@ Task RecordEndpointThroughput(string endpointName, ThroughputSource throughputSo
Task> GetReportMasks(CancellationToken cancellationToken = default);
Task SaveReportMasks(List reportMasks, CancellationToken cancellationToken = default);
- Task GetLicensedEndpointDetails(CancellationToken cancellationToken = default);
+ Task GetLicensedEndpointDetails(bool amendWithUserChanges = true, CancellationToken cancellationToken = default);
Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken = default);
- Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment result, CancellationToken cancellationToken = default);
+ Task GetLicensedEndpointDetailsAmendments(CancellationToken cancellationToken = default);
+ Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment? result, CancellationToken cancellationToken = default);
}
\ No newline at end of file
diff --git a/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs b/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs
index 1b4104f5be..4233e405c3 100644
--- a/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs
+++ b/src/Particular.LicensingComponent.UnitTests/MonitoringService_Tests.cs
@@ -258,12 +258,13 @@ public Task> GetReportMasks(CancellationToken cancellationToken = d
public Task SaveReportMasks(List reportMasks, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
- public Task GetLicensedEndpointDetails(CancellationToken cancellationToken = default) =>
+ public Task GetLicensedEndpointDetails(bool amendWithUserChanges = true, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public Task RemoveEndpoints(EndpointIdentifier[] endpointIds, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+ public Task GetLicensedEndpointDetailsAmendments(CancellationToken cancellationToken = default) => throw new NotImplementedException();
public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment result, CancellationToken cancellationToken = default) => throw new NotImplementedException();
}
diff --git a/src/Particular.LicensingComponent/ThroughputCollector.cs b/src/Particular.LicensingComponent/ThroughputCollector.cs
index 945640ea10..733a7d4cff 100644
--- a/src/Particular.LicensingComponent/ThroughputCollector.cs
+++ b/src/Particular.LicensingComponent/ThroughputCollector.cs
@@ -158,6 +158,8 @@ public async Task GenerateThroughputReport(string spVersion, DateT
var auditServiceMetadata = await dataStore.GetAuditServiceMetadata(cancellationToken);
var brokerMetaData = await dataStore.GetBrokerMetadata(cancellationToken);
+ var endpointMetadata = await dataStore.GetLicensedEndpointDetails(false, cancellationToken);
+ var endpointMetadataAmendments = await dataStore.GetLicensedEndpointDetailsAmendments(cancellationToken);
var report = new Report.Report
{
EndTime = new DateTimeOffset((DateTime)reportEndDate, TimeSpan.Zero),
@@ -172,7 +174,33 @@ public async Task GenerateThroughputReport(string spVersion, DateT
Queues = [.. queueThroughputs],
TotalQueues = queueThroughputs.Count,
// A copy, because the report adds its own keys. The data store can return a shared instance.
- EnvironmentInformation = new EnvironmentInformation { AuditServicesData = new AuditServicesData(auditServiceMetadata.Versions, auditServiceMetadata.Transports), EnvironmentData = new Dictionary(brokerMetaData.Data) }
+ EnvironmentInformation = new EnvironmentInformation { AuditServicesData = new AuditServicesData(auditServiceMetadata.Versions, auditServiceMetadata.Transports), EnvironmentData = new Dictionary(brokerMetaData.Data) },
+ CustomerLicenseAmendments = (endpointMetadata, endpointMetadataAmendments) switch
+ {
+ (null, null) => null,
+ (_, null) => null,
+ (null, _) => null,
+ (LicensedEndpointDetails baseDetails, LicensedEndpointDetailsAmendment amendments) => new LicenseAmendments
+ {
+ ExcludedQueues = [.. amendments.ExcludedQueues.Select(qId => new Report.QueueIdentity { NameHash = qId.NameHash, ScopeHash = qId.ScopeHash })],
+ InfrastructureQueues = [.. amendments.InfrastructureQueues.Select(qId => new Report.QueueIdentity { NameHash = qId.NameHash, ScopeHash = qId.ScopeHash })],
+ RemovedEndpointNames = [.. baseDetails.Endpoints.Where(endpoint => endpointMetadataAmendments.RemovedEndpointIds.Contains(endpoint.ClientId)).Select(endpoint => endpoint.Name)],
+ Endpoints = [.. amendments.Endpoints.Select(amended => {
+ var previousEndpoint = baseDetails.Endpoints.FirstOrDefault(endpoint => endpoint.ClientId == amended.ClientId);
+ Contracts.QueueIdentity[] notThisEndpointQueues = [.. amendments.UnassignedQueues, .. amendments.ExcludedQueues, .. amendments.InfrastructureQueues, .. amendments.Endpoints.Where(endpoint => endpoint.ClientId != amended.ClientId).SelectMany(endpoint => endpoint.AmendedQueues)];
+ return new Report.LicensedEndpointAmendment
+ {
+ PreviousName = previousEndpoint?.Name,
+ PreviousNameHash = previousEndpoint is not null ? OneWayHasher.CalculateOneWayHash(previousEndpoint.Name) : null,
+ CurrentName = masker.Mask(amended.Name),
+ CurrentNameHash = OneWayHasher.CalculateOneWayHash(amended.Name),
+ IsSendOnly = amended.Classification == 1,
+ AmendedQueues = [.. amended.AmendedQueues.Select(qId => new Report.QueueIdentity { NameHash = qId.NameHash, ScopeHash = qId.ScopeHash })],
+ RemovedQueues = previousEndpoint is not null ? [.. previousEndpoint.Queues.Where(qId => notThisEndpointQueues.Contains(qId)).Select(qId => new Report.QueueIdentity { NameHash = qId.NameHash, ScopeHash = qId.ScopeHash })] : []
+ };
+ })]
+ },
+ }
};
var auditThroughput = queueThroughputs.SelectMany(w => w.DailyThroughputFromAudit);
diff --git a/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs b/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs
index f6fd359362..a5c2f0801f 100644
--- a/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs
+++ b/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs
@@ -308,16 +308,16 @@ public Task> GetReportMasks(CancellationToken cancellationToken = d
public Task SaveReportMasks(List reportMasks, CancellationToken cancellationToken = default) =>
ExecuteWithDbContext((context, token) => context.StoreSetting(SettingKeys.ReportMasks, reportMasks, token), cancellationToken);
- public Task GetLicensedEndpointDetails(CancellationToken cancellationToken = default) =>
+ public Task GetLicensedEndpointDetails(bool amendWithUserChanges = true, CancellationToken cancellationToken = default) =>
ExecuteWithDbContext(async (context, token) =>
{
var baseDetails = await context.GetSetting(SettingKeys.LicensedEndpointDetails, token);
- if (baseDetails is null)
+ if (baseDetails is not null && amendWithUserChanges)
{
- return null;
+ var amendments = await context.GetSetting(SettingKeys.LicensedEndpointDetailsAmendment, token);
+ return baseDetails.Amend(amendments);
}
- var amendments = await context.GetSetting(SettingKeys.LicensedEndpointDetailsAmendment, token);
- return baseDetails.Amend(amendments);
+ return baseDetails;
}, cancellationToken);
public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken = default) =>
@@ -327,12 +327,21 @@ public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, Cancella
await context.StoreSetting(SettingKeys.LicensedEndpointDetailsAmendment, new LicensedEndpointDetailsAmendment(), token);
}, cancellationToken);
- public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment result, CancellationToken cancellationToken = default) =>
+ public Task GetLicensedEndpointDetailsAmendments(CancellationToken cancellationToken = default) =>
+ ExecuteWithDbContext((context, token) => context.GetSetting(SettingKeys.LicensedEndpointDetailsAmendment, token), cancellationToken);
+
+ public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment? result, CancellationToken cancellationToken = default) =>
ExecuteWithDbContext(async (context, token) =>
{
var existing = await context.GetSetting(SettingKeys.LicensedEndpointDetailsAmendment, token);
if (existing is not null)
{
+ if (result is null)
+ {
+ context.Remove(existing);
+ await context.SaveChangesAsync(cancellationToken);
+ return;
+ }
// these do not round-trip
result.UnmappedQueues = [.. existing.UnmappedQueues, .. result.UnmappedQueues];
}
diff --git a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs
index dfecbbe661..9b547a1062 100644
--- a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs
+++ b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs
@@ -338,18 +338,18 @@ public async Task SaveReportMasks(List reportMasks, CancellationToken ca
await session.SaveChangesAsync(cancellationToken);
}
- public async Task GetLicensedEndpointDetails(CancellationToken cancellationToken = default)
+ public async Task GetLicensedEndpointDetails(bool amendWithUserChanges = true, CancellationToken cancellationToken = default)
{
var store = await storeProvider.GetDocumentStore(cancellationToken);
using IAsyncDocumentSession session = store.OpenAsyncSession(databaseConfiguration.Name);
var baseDetails = await session.LoadAsync(LicencedEndpointDetailsDocumentId, cancellationToken);
- if (baseDetails is null)
+ if (baseDetails is not null && amendWithUserChanges)
{
- return null;
+ var amendments = await session.LoadAsync(LicencedEndpointDetailsAmendmentDocumentId, cancellationToken);
+ return baseDetails.Amend(amendments);
}
- var amendments = await session.LoadAsync(LicencedEndpointDetailsAmendmentDocumentId, cancellationToken);
- return baseDetails.Amend(amendments);
+ return baseDetails;
}
public async Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, CancellationToken cancellationToken = default)
@@ -362,20 +362,36 @@ public async Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, Ca
await session.SaveChangesAsync(cancellationToken);
}
- public async Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment result, CancellationToken cancellationToken = default)
+ public async Task GetLicensedEndpointDetailsAmendments(CancellationToken cancellationToken = default)
{
var store = await storeProvider.GetDocumentStore(cancellationToken);
using IAsyncDocumentSession session = store.OpenAsyncSession(databaseConfiguration.Name);
- var existing = await session.LoadAsync(LicencedEndpointDetailsAmendmentDocumentId, cancellationToken);
- if (existing is not null)
+ return await session.LoadAsync(LicencedEndpointDetailsAmendmentDocumentId, cancellationToken);
+ }
+
+ public async Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment? result, CancellationToken cancellationToken = default)
+ {
+ var store = await storeProvider.GetDocumentStore(cancellationToken);
+ using IAsyncDocumentSession session = store.OpenAsyncSession(databaseConfiguration.Name);
+
+ if (result is null)
{
- // these do not round-trip
- result.UnmappedQueues = [.. existing.UnmappedQueues, .. result.UnmappedQueues];
- session.Advanced.Evict(existing);
+ session.Delete(LicencedEndpointDetailsAmendmentDocumentId);
+ }
+ else
+ {
+ var existing = await session.LoadAsync(LicencedEndpointDetailsAmendmentDocumentId, cancellationToken);
+ if (existing is not null)
+ {
+ // these do not round-trip
+ result.UnmappedQueues = [.. existing.UnmappedQueues, .. result.UnmappedQueues];
+ session.Advanced.Evict(existing);
+ }
+
+ await session.StoreAsync(result, LicencedEndpointDetailsAmendmentDocumentId, cancellationToken);
}
- await session.StoreAsync(result, LicencedEndpointDetailsAmendmentDocumentId, cancellationToken);
await session.SaveChangesAsync(cancellationToken);
}
}
\ No newline at end of file
diff --git a/src/ServiceControl.Persistence.Tests/Throughput/LicenseMetadataTests.cs b/src/ServiceControl.Persistence.Tests/Throughput/LicenseMetadataTests.cs
index 756e66c250..2a4d5ee859 100644
--- a/src/ServiceControl.Persistence.Tests/Throughput/LicenseMetadataTests.cs
+++ b/src/ServiceControl.Persistence.Tests/Throughput/LicenseMetadataTests.cs
@@ -92,12 +92,14 @@ await LicensingDataStore.SaveLicensedEndpointDetails(new LicensedEndpointDetails
new LicensedEndpoint
{
ClientId = "endpoint-to-keep",
- Name = "Endpoint to keep"
+ Name = "Endpoint to keep",
+ Classification = 0
},
new LicensedEndpoint
{
ClientId = "endpoint-to-remove",
- Name = "Endpoint to remove"
+ Name = "Endpoint to remove",
+ Classification = 0
}
]
});
@@ -130,12 +132,14 @@ await LicensingDataStore.SaveLicensedEndpointDetails(new LicensedEndpointDetails
new LicensedEndpoint
{
ClientId = "first-endpoint",
- Name = "First endpoint"
+ Name = "First endpoint",
+ Classification = 0
},
new LicensedEndpoint
{
ClientId = "second-endpoint",
- Name = "Second endpoint"
+ Name = "Second endpoint",
+ Classification = 0
}
]
});
@@ -170,7 +174,8 @@ await LicensingDataStore.SaveLicensedEndpointDetails(new LicensedEndpointDetails
new LicensedEndpoint
{
ClientId = "endpoint-id",
- Name = "Original name"
+ Name = "Original name",
+ Classification = 0
}
]
});
@@ -188,7 +193,8 @@ await LicensingDataStore.SaveLicensedEndpointDetails(new LicensedEndpointDetails
new LicensedEndpoint
{
ClientId = "endpoint-id",
- Name = "Replacement name"
+ Name = "Replacement name",
+ Classification = 0
}
]
});
diff --git a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt
index d9f20d7486..edb8c3ec55 100644
--- a/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt
+++ b/src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt
@@ -44,6 +44,7 @@ GET /license => ServiceControl.Licensing.LicenseController:License(Boolean refre
POST /license/amendDetails => ServiceControl.Licensing.LicenseController:AmendLicenseDetails(LicensedEndpointDetailsAmendment amendment, CancellationToken cancellationToken)
GET /license/details => ServiceControl.Licensing.LicenseController:LicenseDetails(CancellationToken cancellationToken)
POST /license/detailsUpload => ServiceControl.Licensing.LicenseController:UploadLicenseDetails(IFormFile file, CancellationToken cancellationToken)
+POST /license/removeAmendments => ServiceControl.Licensing.LicenseController:RemoveLicenseDetailsAmendments(CancellationToken cancellationToken)
GET /messages => ServiceControl.CompositeViews.Messages.GetMessagesController:Messages(PagingInfo pagingInfo, SortInfo sortInfo, Boolean includeSystemMessages, CancellationToken cancellationToken)
GET /messages/{id}/body => ServiceControl.CompositeViews.Messages.GetMessagesController:Get(String id, String instanceId, CancellationToken cancellationToken)
GET /messages/search => ServiceControl.CompositeViews.Messages.GetMessagesController:Search(PagingInfo pagingInfo, SortInfo sortInfo, String q, CancellationToken cancellationToken)
diff --git a/src/ServiceControl/Licensing/LicenseController.cs b/src/ServiceControl/Licensing/LicenseController.cs
index fd2ff4b142..a9c8b90d66 100644
--- a/src/ServiceControl/Licensing/LicenseController.cs
+++ b/src/ServiceControl/Licensing/LicenseController.cs
@@ -62,7 +62,7 @@ public async Task> License(bool refresh, string client
return (LicensedEndpointDetails?)null;
}
- var licenseDetails = await dataStore.GetLicensedEndpointDetails(cancellationToken);
+ var licenseDetails = await dataStore.GetLicensedEndpointDetails(cancellationToken: cancellationToken);
if (licenseDetails is null)
{
return (LicensedEndpointDetails?)null;
@@ -108,5 +108,11 @@ public async Task UploadLicenseDetails([FromForm] IFormFile file, CancellationTo
[Route("license/amendDetails")]
public async Task AmendLicenseDetails([FromBody] LicensedEndpointDetailsAmendment amendment, CancellationToken cancellationToken = default) =>
await dataStore.SaveLicensedEndpointDetailsAmendment(amendment, cancellationToken);
+
+ [Authorize(Policy = Permissions.ErrorThroughputManage)]
+ [HttpPost]
+ [Route("license/removeAmendments")]
+ public async Task RemoveLicenseDetailsAmendments(CancellationToken cancellationToken = default) =>
+ await dataStore.SaveLicensedEndpointDetailsAmendment(null, cancellationToken);
}
}
\ No newline at end of file