diff --git a/dev/docker-compose-seeder.yml b/dev/docker-compose-seeder.yml new file mode 100644 index 000000000000..931d7e2ee3a9 --- /dev/null +++ b/dev/docker-compose-seeder.yml @@ -0,0 +1,85 @@ +services: + # Identity Service + identity: + build: + context: .. + dockerfile: ./src/Identity/Dockerfile + environment: + ASPNETCORE_ENVIRONMENT: Development + globalSettings__selfHosted: "false" + globalSettings__baseServiceUri__vault: "http://localhost:8080" + globalSettings__baseServiceUri__api: "http://api:5000" + globalSettings__baseServiceUri__identity: "http://identity:5000" + globalSettings__databaseProvider: "sqlite" + globalSettings__sqlite__connectionString: "Data Source=/etc/bitwarden/db/vault_dev.db" + globalSettings__licenseDirectory: "/etc/bitwarden/core/licenses" + globalSettings__attachment__baseDirectory: "/etc/bitwarden/core/attachments" + globalSettings__dataProtection__directory: "/etc/bitwarden/core/aspnet-dataprotection" + globalSettings__developmentDirectory: "/etc/bitwarden/dev" + globalSettings__logDirectory: "/etc/bitwarden/logs" + ports: + - "33656:5000" + volumes: + - ./.data/db:/etc/bitwarden/db + - ./.data/dev:/etc/bitwarden/dev + networks: + - bitwarden + + # API Service + api: + build: + context: .. + dockerfile: ./src/Api/Dockerfile + environment: + ASPNETCORE_ENVIRONMENT: Development + globalSettings__selfHosted: "false" + globalSettings__baseServiceUri__vault: "http://localhost:8080" + globalSettings__baseServiceUri__api: "http://api:5000" + globalSettings__baseServiceUri__identity: "http://identity:5000" + globalSettings__databaseProvider: "sqlite" + globalSettings__sqlite__connectionString: "Data Source=/etc/bitwarden/db/vault_dev.db" + globalSettings__licenseDirectory: "/etc/bitwarden/core/licenses" + globalSettings__attachment__baseDirectory: "/etc/bitwarden/core/attachments" + globalSettings__dataProtection__directory: "/etc/bitwarden/core/aspnet-dataprotection" + globalSettings__logDirectory: "/etc/bitwarden/logs" + ports: + - "4000:5000" + volumes: + - ./.data/api:/etc/bitwarden + - ./.data/db:/etc/bitwarden/db + depends_on: + - identity + networks: + - bitwarden + + # Seeder API Service + seeder: + build: + context: .. + dockerfile: ./util/SeederApi/Dockerfile + environment: + ASPNETCORE_ENVIRONMENT: Development + globalSettings__selfHosted: "false" + globalSettings__baseServiceUri__vault: "http://localhost:8080" + globalSettings__baseServiceUri__api: "http://api:5000" + globalSettings__baseServiceUri__identity: "http://identity:5000" + globalSettings__databaseProvider: "sqlite" + globalSettings__sqlite__connectionString: "Data Source=/etc/bitwarden/db/vault_dev.db" + globalSettings__licenseDirectory: "/etc/bitwarden/core/licenses" + globalSettings__attachment__baseDirectory: "/etc/bitwarden/core/attachments" + globalSettings__dataProtection__directory: "/etc/bitwarden/core/aspnet-dataprotection" + globalSettings__logDirectory: "/etc/bitwarden/logs" + ports: + - "5100:5000" + volumes: + - ./.data/seeder:/etc/bitwarden + - ./.data/db:/etc/bitwarden/db + depends_on: + - api + - identity + networks: + - bitwarden + +networks: + bitwarden: + driver: bridge diff --git a/util/RustSdk/RustSdk.csproj b/util/RustSdk/RustSdk.csproj index 14cc01736567..2ed7f7c327f9 100644 --- a/util/RustSdk/RustSdk.csproj +++ b/util/RustSdk/RustSdk.csproj @@ -26,7 +26,7 @@ Always true - runtimes/linux-x64/native/libsdk.so + runtimes/linux-arm64/native/libsdk.so Always @@ -49,7 +49,7 @@ Always true - runtimes/linux-x64/native/libsdk.so + runtimes/linux-arm64/native/libsdk.so Always diff --git a/util/SeederApi/Dockerfile b/util/SeederApi/Dockerfile new file mode 100644 index 000000000000..a0021311aa13 --- /dev/null +++ b/util/SeederApi/Dockerfile @@ -0,0 +1,83 @@ +############################################### +# Build stage # +############################################### +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build + +# Docker buildx supplies the value for this arg +ARG TARGETPLATFORM + +# Determine proper runtime value for .NET +# We put the value in a file to be read by later layers. +RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ + RID=linux-x64 ; \ + RUST_TARGET=x86_64-unknown-linux-gnu ; \ + elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ + RID=linux-arm64 ; \ + RUST_TARGET=aarch64-unknown-linux-gnu ; \ + elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ + RID=linux-arm ; \ + RUST_TARGET=armv7-unknown-linux-gnueabihf ; \ + fi \ + && echo "RID=$RID" > /tmp/rid.txt \ + && echo "$RUST_TARGET" > /tmp/rust_target.txt + +# Install Rust +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal \ + && . $HOME/.cargo/env \ + && rustup target add $(cat /tmp/rust_target.txt) \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +ENV PATH="/root/.cargo/bin:${PATH}" + +# Copy required project files +WORKDIR /source +COPY . ./ + +# Restore project dependencies and tools +WORKDIR /source/util/SeederApi +RUN . /tmp/rid.txt && dotnet restore -r $RID + +# Build project +RUN . /tmp/rid.txt && dotnet publish \ + -c release \ + --no-restore \ + --self-contained \ + /p:PublishSingleFile=true \ + -r $RID \ + -o out + +############################################### +# App stage # +############################################### +FROM mcr.microsoft.com/dotnet/aspnet:8.0 + +ARG TARGETPLATFORM +LABEL com.bitwarden.product="bitwarden" +ENV ASPNETCORE_ENVIRONMENT=Production +ENV ASPNETCORE_URLS=http://+:5000 +ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false +EXPOSE 5000 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + curl \ + krb5-user \ + gosu \ + tzdata \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Copy app from the build stage +WORKDIR /app +COPY --from=build /source/util/SeederApi/out /app +COPY ./util/SeederApi/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1 + +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/util/SeederApi/HostedServices/DatabaseMigrationHostedService.cs b/util/SeederApi/HostedServices/DatabaseMigrationHostedService.cs new file mode 100644 index 000000000000..8a71cef59a25 --- /dev/null +++ b/util/SeederApi/HostedServices/DatabaseMigrationHostedService.cs @@ -0,0 +1,49 @@ +using System.Data.Common; +using Bit.Core.Utilities; + +namespace Bit.SeederApi.HostedServices; + +public sealed class DatabaseMigrationHostedService( + IDbMigrator dbMigrator, + ILogger logger) + : IHostedService, IDisposable +{ + public async Task StartAsync(CancellationToken cancellationToken) + { + logger.LogInformation("Starting database migration..."); + + // Wait 5 seconds to allow database file to be ready + await Task.Delay(5000, cancellationToken); + + const int maxMigrationAttempts = 10; + for (var i = 1; i <= maxMigrationAttempts; i++) + { + try + { + dbMigrator.MigrateDatabase(true, cancellationToken); + logger.LogInformation("Database migration completed successfully"); + break; + } + catch (DbException e) + { + if (i >= maxMigrationAttempts) + { + logger.LogError(e, "Database failed to migrate after {MaxAttempts} attempts", maxMigrationAttempts); + throw; + } + + logger.LogWarning(e, + "Database unavailable for migration. Trying again (attempt #{AttemptNumber})...", i + 1); + await Task.Delay(5000, cancellationToken); + } + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.FromResult(0); + } + + public void Dispose() + { } +} diff --git a/util/SeederApi/SeederApi.csproj b/util/SeederApi/SeederApi.csproj index 53e9941c1cb4..b427eb59552e 100644 --- a/util/SeederApi/SeederApi.csproj +++ b/util/SeederApi/SeederApi.csproj @@ -11,6 +11,7 @@ + diff --git a/util/SeederApi/Startup.cs b/util/SeederApi/Startup.cs index a38ff8256bde..7b35f33b664b 100644 --- a/util/SeederApi/Startup.cs +++ b/util/SeederApi/Startup.cs @@ -1,6 +1,7 @@ using System.Globalization; using Bit.Core.Settings; using Bit.SeederApi.Extensions; +using Bit.SeederApi.HostedServices; using Bit.SharedWeb.Utilities; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -28,7 +29,17 @@ public void ConfigureServices(IServiceCollection services) services.AddCustomDataProtectionServices(Environment, globalSettings); services.AddTokenizers(); - services.AddDatabaseRepositories(globalSettings); + var databaseProvider = services.AddDatabaseRepositories(globalSettings); + + // Register database migrator based on provider + switch (databaseProvider) + { + case Core.Enums.SupportedDatabaseProviders.Sqlite: + services.AddSingleton(); + services.AddHostedService(); + break; + } + services.AddTestPlayIdTracking(globalSettings); services.AddManglerService(globalSettings); diff --git a/util/SeederApi/entrypoint.sh b/util/SeederApi/entrypoint.sh new file mode 100644 index 000000000000..b72b41683a50 --- /dev/null +++ b/util/SeederApi/entrypoint.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +# Setup + +GROUPNAME="bitwarden" +USERNAME="bitwarden" + +LUID=${LOCAL_UID:-0} +LGID=${LOCAL_GID:-0} + +# Step down from host root to well-known nobody/nogroup user + +if [ $LUID -eq 0 ] +then + LUID=65534 +fi +if [ $LGID -eq 0 ] +then + LGID=65534 +fi + +if [ "$(id -u)" = "0" ] +then + # Create user and group + + groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 || + groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1 + useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 || + usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 + mkhomedir_helper $USERNAME + + # The rest... + + chown -R $USERNAME:$GROUPNAME /app + mkdir -p /etc/bitwarden/core + mkdir -p /etc/bitwarden/logs + mkdir -p /etc/bitwarden/ca-certificates + chown -R $USERNAME:$GROUPNAME /etc/bitwarden + + if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then + chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos + fi + + gosu_cmd="gosu $USERNAME:$GROUPNAME" +else + gosu_cmd="" +fi + +if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then + cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf + $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab +fi + +exec $gosu_cmd /app/SeederApi \ No newline at end of file