diff --git a/.github/workflows/build-db-container.yml b/.github/workflows/build-db-container.yml index 9bcc8ac4e5..b8180b9e20 100644 --- a/.github/workflows/build-db-container.yml +++ b/.github/workflows/build-db-container.yml @@ -26,6 +26,8 @@ jobs: version: ${{ env.MinVerVersion }} - name: Log in to GitHub container registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + - name: Install Docker arm64 emulation + run: docker run --privileged --rm tonistiigi/binfmt --install arm64 - name: Build images env: TAG_NAME: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || env.MinVerVersion }} diff --git a/.github/workflows/container-integration-test.yml b/.github/workflows/container-integration-test.yml index c35b016c8e..49d029b001 100644 --- a/.github/workflows/container-integration-test.yml +++ b/.github/workflows/container-integration-test.yml @@ -57,6 +57,11 @@ jobs: uses: Particular/run-minver-action@v1.0.0 - name: Log in to GitHub container registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + - name: Log in to Docker Hub + uses: docker/login-action@v3.3.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Azure Login if: ${{ matrix.name == 'asb' || matrix.name == 'asq' }} uses: azure/login@v2.2.0 @@ -101,13 +106,20 @@ jobs: } Write-Output "healthy-containers=$numHealthy" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf-8 -Append - name: Dump Docker container list + if: ${{ always() }} run: docker ps --all - name: Dump Primary instance logs + if: ${{ always() }} run: docker logs service-platform-servicecontrol-1 - name: Dump Audit instance logs + if: ${{ always() }} run: docker logs service-platform-servicecontrol-audit-1 - name: Dump Monitoring instance logs + if: ${{ always() }} run: docker logs service-platform-servicecontrol-monitoring-1 + - name: Dump Database logs + if: ${{ always() }} + run: docker logs service-platform-servicecontrol-db-1 - name: Return status shell: pwsh run: | diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 9a98173468..854b8d86ac 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -60,7 +60,7 @@ - + diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedMessageRetriesController.cs b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedMessageRetriesController.cs index 8ca6705b59..b511a9db28 100644 --- a/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedMessageRetriesController.cs +++ b/src/ServiceControl.AcceptanceTests.RavenDB/Recoverability/MessageFailures/FailedMessageRetriesController.cs @@ -10,7 +10,7 @@ public class FailedMessageRetriesCountReponse { - public int Count { get; set; } + public long Count { get; set; } } [ApiController] diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/DatabaseSetup.cs b/src/ServiceControl.Audit.Persistence.RavenDB/DatabaseSetup.cs index 2e1a6148de..cb31bf977a 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/DatabaseSetup.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDB/DatabaseSetup.cs @@ -1,17 +1,17 @@ namespace ServiceControl.Audit.Persistence.RavenDB { + using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Raven.Client.Documents; using Raven.Client.Documents.Indexes; - using Raven.Client.Documents.Operations; using Raven.Client.Documents.Operations.Expiration; using Raven.Client.Documents.Operations.Indexes; using Raven.Client.Exceptions; - using Raven.Client.Exceptions.Database; using Raven.Client.ServerWide; using Raven.Client.ServerWide.Operations; + using Raven.Client.ServerWide.Operations.Configuration; using ServiceControl.Audit.Persistence.RavenDB.Indexes; using ServiceControl.SagaAudit; @@ -19,51 +19,55 @@ class DatabaseSetup(DatabaseConfiguration configuration) { public async Task Execute(IDocumentStore documentStore, CancellationToken cancellationToken) { - try - { - await documentStore.Maintenance.ForDatabase(configuration.Name).SendAsync(new GetStatisticsOperation(), cancellationToken); - } - catch (DatabaseDoesNotExistException) + await CreateDatabase(documentStore, configuration.Name, cancellationToken); + await UpdateDatabaseSettings(documentStore, configuration.Name, cancellationToken); + + await CreateIndexes(documentStore, cancellationToken); + + await ConfigureExpiration(documentStore, cancellationToken); + } + + async Task CreateDatabase(IDocumentStore documentStore, string databaseName, CancellationToken cancellationToken) + { + var dbRecord = await documentStore.Maintenance.Server.SendAsync(new GetDatabaseRecordOperation(databaseName), cancellationToken); + + if (dbRecord is null) { try { - await documentStore.Maintenance.Server - .SendAsync(new CreateDatabaseOperation(new DatabaseRecord(configuration.Name)), cancellationToken); + var databaseRecord = new DatabaseRecord(databaseName); + databaseRecord.Settings.Add("Indexing.Auto.SearchEngineType", "Corax"); + databaseRecord.Settings.Add("Indexing.Static.SearchEngineType", "Corax"); + + await documentStore.Maintenance.Server.SendAsync(new CreateDatabaseOperation(databaseRecord), cancellationToken); } catch (ConcurrencyException) { // The database was already created before calling CreateDatabaseOperation } } + } - var indexList = new List { - new FailedAuditImportIndex(), - new SagaDetailsIndex() - }; - - await DeleteLegacySagaDetailsIndex(documentStore, cancellationToken); + async Task UpdateDatabaseSettings(IDocumentStore documentStore, string databaseName, CancellationToken cancellationToken) + { + var dbRecord = await documentStore.Maintenance.Server.SendAsync(new GetDatabaseRecordOperation(databaseName), cancellationToken); - if (configuration.EnableFullTextSearch) + if (dbRecord is null) { - indexList.Add(new MessagesViewIndexWithFullTextSearch()); - await documentStore.Maintenance.SendAsync(new DeleteIndexOperation("MessagesViewIndex"), cancellationToken); - } - else - { - indexList.Add(new MessagesViewIndex()); - await documentStore.Maintenance - .SendAsync(new DeleteIndexOperation("MessagesViewIndexWithFullTextSearch"), cancellationToken); + throw new InvalidOperationException($"Database '{databaseName}' does not exist."); } - await IndexCreation.CreateIndexesAsync(indexList, documentStore, null, null, cancellationToken); + var updated = false; - var expirationConfig = new ExpirationConfiguration - { - Disabled = false, - DeleteFrequencyInSec = configuration.ExpirationProcessTimerInSeconds - }; + updated |= dbRecord.Settings.TryAdd("Indexing.Auto.SearchEngineType", "Corax"); + updated |= dbRecord.Settings.TryAdd("Indexing.Static.SearchEngineType", "Corax"); - await documentStore.Maintenance.SendAsync(new ConfigureExpirationOperation(expirationConfig), cancellationToken); + if (updated) + { + await documentStore.Maintenance.ForDatabase(databaseName).SendAsync(new PutDatabaseSettingsOperation(databaseName, dbRecord.Settings), cancellationToken); + await documentStore.Maintenance.Server.SendAsync(new ToggleDatabasesStateOperation(databaseName, true), cancellationToken); + await documentStore.Maintenance.Server.SendAsync(new ToggleDatabasesStateOperation(databaseName, false), cancellationToken); + } } public static async Task DeleteLegacySagaDetailsIndex(IDocumentStore documentStore, CancellationToken cancellationToken) @@ -81,5 +85,36 @@ public static async Task DeleteLegacySagaDetailsIndex(IDocumentStore documentSto await documentStore.Maintenance.SendAsync(new DeleteIndexOperation("SagaDetailsIndex"), cancellationToken); } } + + async Task CreateIndexes(IDocumentStore documentStore, CancellationToken cancellationToken) + { + await DeleteLegacySagaDetailsIndex(documentStore, cancellationToken); + + List indexList = [new FailedAuditImportIndex(), new SagaDetailsIndex()]; + + if (configuration.EnableFullTextSearch) + { + indexList.Add(new MessagesViewIndexWithFullTextSearch()); + await documentStore.Maintenance.SendAsync(new DeleteIndexOperation("MessagesViewIndex"), cancellationToken); + } + else + { + indexList.Add(new MessagesViewIndex()); + await documentStore.Maintenance.SendAsync(new DeleteIndexOperation("MessagesViewIndexWithFullTextSearch"), cancellationToken); + } + + await IndexCreation.CreateIndexesAsync(indexList, documentStore, null, null, cancellationToken); + } + + async Task ConfigureExpiration(IDocumentStore documentStore, CancellationToken cancellationToken) + { + var expirationConfig = new ExpirationConfiguration + { + Disabled = false, + DeleteFrequencyInSec = configuration.ExpirationProcessTimerInSeconds + }; + + await documentStore.Maintenance.SendAsync(new ConfigureExpirationOperation(expirationConfig), cancellationToken); + } } } diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/RavenAuditDataStore.cs b/src/ServiceControl.Audit.Persistence.RavenDB/RavenAuditDataStore.cs index d966fc0d1b..a963d6015b 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/RavenAuditDataStore.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDB/RavenAuditDataStore.cs @@ -175,9 +175,9 @@ public async Task>> QueryAuditCounts(string endpoi .Take(0) .ToArrayAsync(); - if (stats.LongTotalResults > 0) + if (stats.TotalResults > 0) { - results.Add(new AuditCount { UtcDate = date, Count = stats.LongTotalResults }); + results.Add(new AuditCount { UtcDate = date, Count = stats.TotalResults }); } } } diff --git a/src/ServiceControl.Audit.Persistence.RavenDB/RavenExternalPersistenceLifecycle.cs b/src/ServiceControl.Audit.Persistence.RavenDB/RavenExternalPersistenceLifecycle.cs index 9909fea599..02b386354c 100644 --- a/src/ServiceControl.Audit.Persistence.RavenDB/RavenExternalPersistenceLifecycle.cs +++ b/src/ServiceControl.Audit.Persistence.RavenDB/RavenExternalPersistenceLifecycle.cs @@ -70,9 +70,6 @@ public async Task Initialize(CancellationToken cancellationToken = default) var databaseSetup = new DatabaseSetup(configuration); await databaseSetup.Execute(store, cancellationToken); - - // Must go after the database setup, as database must exist - StartupChecks.EnsureSingleNodeTopology(store); } finally { diff --git a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/SagaHistoryIndexTests.cs b/src/ServiceControl.Audit.Persistence.Tests.RavenDB/SagaHistoryIndexTests.cs deleted file mode 100644 index e37c6b8537..0000000000 --- a/src/ServiceControl.Audit.Persistence.Tests.RavenDB/SagaHistoryIndexTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace ServiceControl.Audit.Persistence.Tests -{ - using System; - using System.Threading.Tasks; - using NUnit.Framework; - using Raven.Client.Documents; - using ServiceControl.SagaAudit; - - [TestFixture] - class SagaHistoryIndexTests : PersistenceTestFixture - { - [Test] - public async Task Should_only_index_saga_id_property() - { - var sagaType = "MySagaType"; - var sagaState = "some-saga-state"; - - await IngestSagaAudits(new SagaSnapshot - { - SagaId = Guid.NewGuid(), - SagaType = sagaType, - Status = SagaStateChangeStatus.New, - StateAfterChange = sagaState - }); - - await configuration.CompleteDBOperation(); - - using (var session = configuration.DocumentStore.OpenAsyncSession()) - { - var sagaTypeResults = await session.Query() - .Search(s => s.SagaType, sagaType) - .ToListAsync(); - - Assert.That(sagaTypeResults.Count, Is.EqualTo(0)); - - var sagaChangeResults = await session.Query() - .Search(s => s.Changes, sagaState) - .ToListAsync(); - - Assert.That(sagaChangeResults.Count, Is.EqualTo(0)); - } - } - - async Task IngestSagaAudits(params SagaSnapshot[] snapshots) - { - var unitOfWork = await StartAuditUnitOfWork(snapshots.Length); - foreach (var snapshot in snapshots) - { - await unitOfWork.RecordSagaSnapshot(snapshot); - } - await unitOfWork.DisposeAsync(); - await configuration.CompleteDBOperation(); - } - } -} \ No newline at end of file diff --git a/src/ServiceControl.Audit.Persistence/Infrastructure/QueryStatsInfo.cs b/src/ServiceControl.Audit.Persistence/Infrastructure/QueryStatsInfo.cs index bae5067730..7e21326bd6 100644 --- a/src/ServiceControl.Audit.Persistence/Infrastructure/QueryStatsInfo.cs +++ b/src/ServiceControl.Audit.Persistence/Infrastructure/QueryStatsInfo.cs @@ -3,10 +3,10 @@ namespace ServiceControl.Audit.Auditing.MessagesView public struct QueryStatsInfo { public readonly string ETag; - public readonly int TotalCount; - public readonly int HighestTotalCountOfAllTheInstances; + public readonly long TotalCount; + public readonly long HighestTotalCountOfAllTheInstances; - public QueryStatsInfo(string eTag, int totalCount, int? highestTotalCountOfAllTheInstances = null) + public QueryStatsInfo(string eTag, long totalCount, long? highestTotalCountOfAllTheInstances = null) { ETag = eTag; TotalCount = totalCount; diff --git a/src/ServiceControl.Audit/Infrastructure/WebApi/HttpResponseExtensions.cs b/src/ServiceControl.Audit/Infrastructure/WebApi/HttpResponseExtensions.cs index 2d0f9f9078..f4af3b765d 100644 --- a/src/ServiceControl.Audit/Infrastructure/WebApi/HttpResponseExtensions.cs +++ b/src/ServiceControl.Audit/Infrastructure/WebApi/HttpResponseExtensions.cs @@ -12,7 +12,7 @@ namespace ServiceControl.Audit.Infrastructure.WebApi static class HttpResponseExtensions { - public static void WithTotalCount(this HttpResponse response, int totalCount) => response.WithHeader("Total-Count", totalCount.ToString(CultureInfo.InvariantCulture)); + public static void WithTotalCount(this HttpResponse response, long totalCount) => response.WithHeader("Total-Count", totalCount.ToString(CultureInfo.InvariantCulture)); public static void WithEtag(this HttpResponse response, StringValues value) => response.Headers.ETag = value; @@ -35,7 +35,7 @@ public static void WithDeterministicEtag(this HttpResponse response, string data static void WithHeader(this HttpResponse response, string header, StringValues value) => response.Headers.Append(header, value); - public static void WithPagingLinks(this HttpResponse response, PagingInfo pageInfo, int highestTotalCountOfAllInstances, int totalResults) + public static void WithPagingLinks(this HttpResponse response, PagingInfo pageInfo, long highestTotalCountOfAllInstances, long totalResults) { if (totalResults <= PagingInfo.DefaultPageSize) { @@ -96,7 +96,7 @@ public static void WithQueryStatsAndPagingInfo(this HttpResponse response, Query } public static void WithPagingLinksAndTotalCount(this HttpResponse response, - PagingInfo pagingInfo, int totalCount, int highestTotalCountOfAllInstances = 1) + PagingInfo pagingInfo, long totalCount, long highestTotalCountOfAllInstances = 1) { response.WithTotalCount(totalCount); response.WithPagingLinks(pagingInfo, highestTotalCountOfAllInstances, totalCount); diff --git a/src/ServiceControl.Persistence.RavenDB/DatabaseSetup.cs b/src/ServiceControl.Persistence.RavenDB/DatabaseSetup.cs index 8525e82f2f..89ea46ec3a 100644 --- a/src/ServiceControl.Persistence.RavenDB/DatabaseSetup.cs +++ b/src/ServiceControl.Persistence.RavenDB/DatabaseSetup.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Persistence.RavenDB { + using System; using System.Threading; using System.Threading.Tasks; using Raven.Client.Documents; @@ -8,6 +9,7 @@ namespace ServiceControl.Persistence.RavenDB using Raven.Client.Exceptions; using Raven.Client.ServerWide; using Raven.Client.ServerWide.Operations; + using Raven.Client.ServerWide.Operations.Configuration; class DatabaseSetup(RavenPersisterSettings settings, IDocumentStore documentStore) { @@ -16,29 +18,27 @@ public async Task Execute(CancellationToken cancellationToken) await CreateDatabase(settings.DatabaseName, cancellationToken); await CreateDatabase(settings.ThroughputDatabaseName, cancellationToken); - await IndexCreation.CreateIndexesAsync(typeof(DatabaseSetup).Assembly, documentStore, null, null, cancellationToken); + await UpdateDatabaseSettings(settings.DatabaseName, cancellationToken); + await UpdateDatabaseSettings(settings.ThroughputDatabaseName, cancellationToken); - var expirationConfig = new ExpirationConfiguration - { - Disabled = false, - DeleteFrequencyInSec = settings.ExpirationProcessTimerInSeconds - }; + await IndexCreation.CreateIndexesAsync(typeof(DatabaseSetup).Assembly, documentStore, null, null, cancellationToken); - await documentStore.Maintenance.SendAsync(new ConfigureExpirationOperation(expirationConfig), cancellationToken); + await ConfigureExpiration(settings, cancellationToken); } async Task CreateDatabase(string databaseName, CancellationToken cancellationToken) { - DatabaseRecordWithEtag dbRecord = - await documentStore.Maintenance.Server.SendAsync(new GetDatabaseRecordOperation(databaseName), - cancellationToken); + var dbRecord = await documentStore.Maintenance.Server.SendAsync(new GetDatabaseRecordOperation(databaseName), cancellationToken); - if (dbRecord == null) + if (dbRecord is null) { try { - await documentStore.Maintenance.Server.SendAsync( - new CreateDatabaseOperation(new DatabaseRecord(databaseName)), cancellationToken); + var databaseRecord = new DatabaseRecord(databaseName); + databaseRecord.Settings.Add("Indexing.Auto.SearchEngineType", "Corax"); + databaseRecord.Settings.Add("Indexing.Static.SearchEngineType", "Corax"); + + await documentStore.Maintenance.Server.SendAsync(new CreateDatabaseOperation(databaseRecord), cancellationToken); } catch (ConcurrencyException) { @@ -46,5 +46,38 @@ await documentStore.Maintenance.Server.SendAsync( } } } + + async Task UpdateDatabaseSettings(string databaseName, CancellationToken cancellationToken) + { + var dbRecord = await documentStore.Maintenance.Server.SendAsync(new GetDatabaseRecordOperation(databaseName), cancellationToken); + + if (dbRecord is null) + { + throw new InvalidOperationException($"Database '{databaseName}' does not exist."); + } + + var updated = false; + + updated |= dbRecord.Settings.TryAdd("Indexing.Auto.SearchEngineType", "Corax"); + updated |= dbRecord.Settings.TryAdd("Indexing.Static.SearchEngineType", "Corax"); + + if (updated) + { + await documentStore.Maintenance.ForDatabase(databaseName).SendAsync(new PutDatabaseSettingsOperation(databaseName, dbRecord.Settings), cancellationToken); + await documentStore.Maintenance.Server.SendAsync(new ToggleDatabasesStateOperation(databaseName, true), cancellationToken); + await documentStore.Maintenance.Server.SendAsync(new ToggleDatabasesStateOperation(databaseName, false), cancellationToken); + } + } + + async Task ConfigureExpiration(RavenPersisterSettings settings, CancellationToken cancellationToken) + { + var expirationConfig = new ExpirationConfiguration + { + Disabled = false, + DeleteFrequencyInSec = settings.ExpirationProcessTimerInSeconds + }; + + await documentStore.Maintenance.SendAsync(new ConfigureExpirationOperation(expirationConfig), cancellationToken); + } } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.RavenDB/EventLogDataStore.cs b/src/ServiceControl.Persistence.RavenDB/EventLogDataStore.cs index ec9e171875..f11461c94e 100644 --- a/src/ServiceControl.Persistence.RavenDB/EventLogDataStore.cs +++ b/src/ServiceControl.Persistence.RavenDB/EventLogDataStore.cs @@ -15,7 +15,7 @@ public async Task Add(EventLogItem logItem) await session.SaveChangesAsync(); } - public async Task<(IList, int, string)> GetEventLogItems(PagingInfo pagingInfo) + public async Task<(IList, long, string)> GetEventLogItems(PagingInfo pagingInfo) { using var session = await sessionProvider.OpenSession(); var results = await session diff --git a/src/ServiceControl.Persistence.RavenDB/RavenExternalPersistenceLifecycle.cs b/src/ServiceControl.Persistence.RavenDB/RavenExternalPersistenceLifecycle.cs index 83473e7dc8..41f6e0ce04 100644 --- a/src/ServiceControl.Persistence.RavenDB/RavenExternalPersistenceLifecycle.cs +++ b/src/ServiceControl.Persistence.RavenDB/RavenExternalPersistenceLifecycle.cs @@ -65,9 +65,6 @@ public async Task Initialize(CancellationToken cancellationToken) var databaseSetup = new DatabaseSetup(settings, store); await databaseSetup.Execute(cancellationToken).ConfigureAwait(false); - - // Must go after the database setup, as database must exist - StartupChecks.EnsureSingleNodeTopology(store); } finally { diff --git a/src/ServiceControl.Persistence/IEventLogDataStore.cs b/src/ServiceControl.Persistence/IEventLogDataStore.cs index b9725cac47..33d71b7a68 100644 --- a/src/ServiceControl.Persistence/IEventLogDataStore.cs +++ b/src/ServiceControl.Persistence/IEventLogDataStore.cs @@ -8,6 +8,6 @@ public interface IEventLogDataStore { Task Add(EventLogItem logItem); - Task<(IList items, int total, string version)> GetEventLogItems(PagingInfo pagingInfo); + Task<(IList items, long total, string version)> GetEventLogItems(PagingInfo pagingInfo); } } \ No newline at end of file diff --git a/src/ServiceControl.Persistence/Infrastructure/QueryStatsInfo.cs b/src/ServiceControl.Persistence/Infrastructure/QueryStatsInfo.cs index 18d14e63d4..91abfcef4b 100644 --- a/src/ServiceControl.Persistence/Infrastructure/QueryStatsInfo.cs +++ b/src/ServiceControl.Persistence/Infrastructure/QueryStatsInfo.cs @@ -3,11 +3,11 @@ namespace ServiceControl.Persistence.Infrastructure public struct QueryStatsInfo { public readonly string ETag; - public readonly int TotalCount; - public readonly int HighestTotalCountOfAllTheInstances; + public readonly long TotalCount; + public readonly long HighestTotalCountOfAllTheInstances; public readonly bool IsStale; - public QueryStatsInfo(string eTag, int totalCount, bool isStale, int? highestTotalCountOfAllTheInstances = null) + public QueryStatsInfo(string eTag, long totalCount, bool isStale, long? highestTotalCountOfAllTheInstances = null) { ETag = eTag; TotalCount = totalCount; diff --git a/src/ServiceControl.RavenDB/Container-README.md b/src/ServiceControl.RavenDB/Container-README.md index 6f791f4cd3..0c7b42a517 100644 --- a/src/ServiceControl.RavenDB/Container-README.md +++ b/src/ServiceControl.RavenDB/Container-README.md @@ -20,7 +20,8 @@ This is the most basic way to start the container using `docker run`: ```shell docker run -d --name servicecontrol-db \ - -v :/opt/RavenDB/Server/RavenData \ + -v db-config:/etc/ravendb \ + -v db-data:/var/lib/ravendb/data \ particular/servicecontrol-ravendb:latest ``` diff --git a/src/ServiceControl.RavenDB/Dockerfile b/src/ServiceControl.RavenDB/Dockerfile index 1d73893aea..2d11600e2e 100644 --- a/src/ServiceControl.RavenDB/Dockerfile +++ b/src/ServiceControl.RavenDB/Dockerfile @@ -2,10 +2,15 @@ ARG BASETAG ARG VERSION FROM ravendb/ravendb:$BASETAG AS ravendb -COPY src/ServiceControl.RavenDB/RavenLicense.json /opt/RavenDB/servicecontrol-license.json +COPY src/ServiceControl.RavenDB/RavenLicense.json /usr/lib/ravendb/servicecontrol-license.json +COPY src/ServiceControl.RavenDB/sc-container-startup.sh /usr/lib/ravendb/scripts/ + +USER root +RUN chown 999:999 /var/lib/ravendb/data +USER ravendb ENV RAVEN_License_Eula_Accepted=true \ - RAVEN_License_Path=/opt/RavenDB/servicecontrol-license.json \ + RAVEN_License_Path=/usr/lib/ravendb/servicecontrol-license.json \ RAVEN_Setup_Mode=None LABEL org.opencontainers.image.source=https://github.com/Particular/ServiceControl \ @@ -15,3 +20,5 @@ LABEL org.opencontainers.image.source=https://github.com/Particular/ServiceContr org.opencontainers.image.version=$VERSION \ org.opencontainers.image.title=ServiceControl.RavenDB \ org.opencontainers.image.description="ServiceControl database storage using RavenDB" + +CMD [ "/bin/bash", "/usr/lib/ravendb/scripts/sc-container-startup.sh" ] diff --git a/src/ServiceControl.RavenDB/EmbeddedDatabase.cs b/src/ServiceControl.RavenDB/EmbeddedDatabase.cs index 3d093434a8..9e4b711b0b 100644 --- a/src/ServiceControl.RavenDB/EmbeddedDatabase.cs +++ b/src/ServiceControl.RavenDB/EmbeddedDatabase.cs @@ -57,18 +57,19 @@ public static EmbeddedDatabase Start(EmbeddedDatabaseConfiguration databaseConfi { CommandLineArgs = [ - $"--License.Path=\"{licenseFileNameAndServerDirectory.LicenseFileName}\"", $"--Logs.Mode={databaseConfiguration.LogsMode}", // HINT: If this is not set, then Raven will pick a default location relative to the server binaries // See https://github.com/ravendb/ravendb/issues/15694 $"--Indexing.NuGetPackagesPath=\"{nugetPackagesPath}\"" ], - AcceptEula = true, DataDirectory = databaseConfiguration.DbPath, ServerUrl = databaseConfiguration.ServerUrl, LogsPath = databaseConfiguration.LogPath }; + serverOptions.Licensing.EulaAccepted = true; + serverOptions.Licensing.LicensePath = licenseFileNameAndServerDirectory.LicenseFileName; + if (!string.IsNullOrWhiteSpace(licenseFileNameAndServerDirectory.ServerDirectory)) { serverOptions.ServerDirectory = licenseFileNameAndServerDirectory.ServerDirectory; @@ -146,7 +147,8 @@ public async Task Connect(CancellationToken cancellationToken) Conventions = new DocumentConventions { SaveEnumsAsIntegers = true - } + }, + SkipCreatingDatabase = true }; if (configuration.FindClrType != null) diff --git a/src/ServiceControl.RavenDB/StartupChecks.cs b/src/ServiceControl.RavenDB/StartupChecks.cs index c3538937c0..46558850e9 100644 --- a/src/ServiceControl.RavenDB/StartupChecks.cs +++ b/src/ServiceControl.RavenDB/StartupChecks.cs @@ -3,32 +3,21 @@ using System.Reflection; using System.Threading; using Raven.Client.Documents; - using Raven.Client.Documents.Conventions; - using Raven.Client.Http; using Raven.Client.ServerWide.Operations; - using Sparrow.Json; public static class StartupChecks { public static async Task EnsureServerVersion(IDocumentStore store, CancellationToken cancellationToken = default) { var build = await store.Maintenance.Server.SendAsync(new GetBuildNumberOperation(), cancellationToken); - var embeddedVersion = typeof(Raven.Embedded.EmbeddedServer).Assembly.GetCustomAttributes().First().InformationalVersion; - if (embeddedVersion != build.FullVersion) - { - throw new Exception($"ServiceControl expects RavenDB Server version {embeddedVersion} but the server is using {build.FullVersion}."); - } - } - // Must go after the database setup, as database must exist - public static void EnsureSingleNodeTopology(IDocumentStore store) - { - using (var session = store.OpenAsyncSession()) + var clientVersion = typeof(Raven.Client.Constants).Assembly.GetCustomAttributes().First().InformationalVersion; + var parts = clientVersion.Split('.'); + var clientProductVersion = $"{parts[0]}.{parts[1]}"; + + if (clientProductVersion != build.ProductVersion) { - if (session.Advanced.RequestExecutor.Topology.Nodes.Count > 1) - { - throw new Exception("ServiceControl expects to run against a single-node RavenDB database. Cluster configurations are not supported."); - } + throw new Exception($"ServiceControl expects RavenDB Server version {clientProductVersion} but the server is using {build.ProductVersion}."); } } } diff --git a/src/ServiceControl.RavenDB/sc-container-startup.sh b/src/ServiceControl.RavenDB/sc-container-startup.sh new file mode 100755 index 0000000000..cd317bbcb5 --- /dev/null +++ b/src/ServiceControl.RavenDB/sc-container-startup.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +LEGACY_PATH="/opt/RavenDB/Server/RavenData" + +if [[ -d "$LEGACY_PATH" ]]; then + echo "ERROR: RavenDB data is being mounted to the wrong location. It should be mounted to $RAVEN_DataDir. Refer to the article 'Upgrade ServiceControl from Version 5 to Version 6' in the documentation for more details about the steps that must be taken to update ServiceControl to this version." + exit 1 +fi + +chmod -R u+rwX $RAVEN_DataDir 2> /dev/null + +if [[ $? -ne 0 ]]; then + echo "ERROR: RavenDB data permissions are not correct. The owner and group should be set to ID 999. Refer to the article 'Upgrade ServiceControl from Version 5 to Version 6' in the documentation for more details about the steps that must be taken to update ServiceControl to this version." + exit 1 +fi + +source /usr/lib/ravendb/scripts/run-raven.sh diff --git a/src/ServiceControl/Infrastructure/WebApi/HttpResponseExtensions.cs b/src/ServiceControl/Infrastructure/WebApi/HttpResponseExtensions.cs index af2fcfd345..6d33e4394e 100644 --- a/src/ServiceControl/Infrastructure/WebApi/HttpResponseExtensions.cs +++ b/src/ServiceControl/Infrastructure/WebApi/HttpResponseExtensions.cs @@ -11,7 +11,7 @@ namespace ServiceControl.Infrastructure.WebApi static class HttpResponseExtensions { - public static void WithTotalCount(this HttpResponse response, int totalCount) => response.WithHeader("Total-Count", totalCount.ToString(CultureInfo.InvariantCulture)); + public static void WithTotalCount(this HttpResponse response, long totalCount) => response.WithHeader("Total-Count", totalCount.ToString(CultureInfo.InvariantCulture)); public static void WithEtag(this HttpResponse response, StringValues value) => response.Headers.ETag = value; @@ -34,7 +34,7 @@ public static void WithDeterministicEtag(this HttpResponse response, string data static void WithHeader(this HttpResponse response, string header, StringValues value) => response.Headers.Append(header, value); - public static void WithPagingLinks(this HttpResponse response, PagingInfo pageInfo, int highestTotalCountOfAllInstances, int totalResults) + public static void WithPagingLinks(this HttpResponse response, PagingInfo pageInfo, long highestTotalCountOfAllInstances, long totalResults) { if (totalResults <= PagingInfo.DefaultPageSize) { @@ -95,7 +95,7 @@ public static void WithQueryStatsAndPagingInfo(this HttpResponse response, Query } public static void WithPagingLinksAndTotalCount(this HttpResponse response, - PagingInfo pagingInfo, int totalCount, int highestTotalCountOfAllInstances = 1) + PagingInfo pagingInfo, long totalCount, long highestTotalCountOfAllInstances = 1) { response.WithTotalCount(totalCount); response.WithPagingLinks(pagingInfo, highestTotalCountOfAllInstances, totalCount); diff --git a/src/ServiceControlInstaller.Engine.UnitTests/Validation/SelfContainedRavenDBTest.cs b/src/ServiceControlInstaller.Engine.UnitTests/Validation/SelfContainedRavenDBTest.cs index e1a825e1f0..9e1b1582be 100644 --- a/src/ServiceControlInstaller.Engine.UnitTests/Validation/SelfContainedRavenDBTest.cs +++ b/src/ServiceControlInstaller.Engine.UnitTests/Validation/SelfContainedRavenDBTest.cs @@ -15,16 +15,16 @@ public void CheckForSelfContainedRavenDB() var ravenServerPath = Path.GetFullPath(Path.Combine(TestContext.CurrentContext.TestDirectory, "..", "..", "..", "..", "..", "deploy", "RavenDBServer")); var ravenStudio = Path.Combine(ravenServerPath, "Raven.Studio.zip"); - var runtimes = Path.Combine(ravenServerPath, "runtimes"); - var systemCollections = Path.Combine(ravenServerPath, "System.Collections.dll"); - var aspNetCoreHttp = Path.Combine(ravenServerPath, "Microsoft.AspNetCore.Http.dll"); + var ravenServerDll = Path.Combine(ravenServerPath, "Raven.Server.dll"); + var ravenServerExe = Path.Combine(ravenServerPath, "Raven.Server.exe"); + var offlineOperationsUtility = Path.Combine(ravenServerPath, "rvn.exe"); Assert.Multiple(() => { Assert.That(ravenStudio, Does.Exist); // No matter what - Assert.That(Directory.Exists(runtimes), Is.EqualTo(isLocal)); // Only in local development - Assert.That(File.Exists(systemCollections), Is.EqualTo(isCI)); // Only on CI - Assert.That(File.Exists(aspNetCoreHttp), Is.EqualTo(isCI)); // Only on CI + Assert.That(File.Exists(ravenServerDll), Is.EqualTo(isLocal)); // Only in local development + Assert.That(File.Exists(ravenServerExe), Is.EqualTo(isCI)); // Only on CI + Assert.That(File.Exists(offlineOperationsUtility), Is.EqualTo(isCI)); // Only on CI }); } } diff --git a/src/container-integration-test/servicecontrol.yml b/src/container-integration-test/servicecontrol.yml index f821418489..19b2c0df91 100644 --- a/src/container-integration-test/servicecontrol.yml +++ b/src/container-integration-test/servicecontrol.yml @@ -20,8 +20,8 @@ services: ports: - "8080:8080" volumes: - - ./volumes/db-data:/opt/RavenDB/Server/RavenData - - ./volumes/db-config:/opt/RavenDB/config + - db-config:/etc/ravendb + - db-data:/var/lib/ravendb/data servicecontrol-audit: image: ghcr.io/particular/servicecontrol-audit:${SERVICECONTROL_TAG} @@ -41,3 +41,7 @@ services: command: --setup-and-run ports: - "33633:33633" + +volumes: + db-config: + db-data: