Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/repair-postgres-migration-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agentweaver": patch
---

PostgreSQL migration containers now use their injected runtime database configuration instead of an image-embedded local database address.
73 changes: 48 additions & 25 deletions apps/Agentweaver.Api/MemoryDbContextDesignFactory.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,48 @@
using Agentweaver.Api.Memory;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;

namespace Agentweaver.Api;

public sealed class MemoryDbContextDesignFactory : IDesignTimeDbContextFactory<MemoryDbContext>
{
public MemoryDbContext CreateDbContext(string[] args)
{
var options = new DbContextOptionsBuilder<MemoryDbContext>();
if (args.Contains("--postgres-migrations", StringComparer.Ordinal))
{
options.UseNpgsql(
"Host=localhost;Database=agentweaver_design;Username=postgres;Password=postgres",
npg => npg.MigrationsAssembly("Agentweaver.Api.Migrations.Postgres"));
}
else
{
options.UseSqlite("Data Source=agentweaver-design.db", sqlite => sqlite.MigrationsAssembly("Agentweaver.Api"));
}

return new MemoryDbContext(options.Options);
}
}
using Agentweaver.Api.Memory;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;

namespace Agentweaver.Api;

public sealed class MemoryDbContextDesignFactory : IDesignTimeDbContextFactory<MemoryDbContext>
{
public MemoryDbContext CreateDbContext(string[] args)
{
var options = new DbContextOptionsBuilder<MemoryDbContext>();
if (args.Contains("--postgres-migrations", StringComparer.Ordinal))
{
options.UseNpgsql(
ResolvePostgresConnectionString(),
npg => npg.MigrationsAssembly("Agentweaver.Api.Migrations.Postgres"));
}
else
{
options.UseSqlite("Data Source=agentweaver-design.db", sqlite => sqlite.MigrationsAssembly("Agentweaver.Api"));
}

return new MemoryDbContext(options.Options);
}

private static string ResolvePostgresConnectionString()
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
var configurationBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile($"appsettings.{environment}.json", optional: true);

if (string.Equals(environment, "Development", StringComparison.OrdinalIgnoreCase))
configurationBuilder.AddUserSecrets<MemoryDbContextDesignFactory>(optional: true);

var configuration = configurationBuilder
.AddEnvironmentVariables()
.Build();

return configuration.GetConnectionString("Postgres")
?? configuration.GetConnectionString("MemoryDb")
?? configuration["Database:ConnectionString"]
?? throw new InvalidOperationException(
"ConnectionStrings:Postgres (or MemoryDb / Database:ConnectionString) is required when using --postgres-migrations.");
}
}
4 changes: 2 additions & 2 deletions docs/guide/architecture-aks.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,9 @@ PVC: agentweaver-workspace (Azure Files, RWX)

### EF Core migrations

On startup, the API runs schema migrations via an **init container** (`migrate-memory-db`) that executes the EF bundle (`/app/efbundle`) against the Postgres connection string. This runs before the main API container starts, ensuring the schema is always current before the application accepts traffic.
On startup, the API and worker run schema migrations via their **init containers** (`migrate-memory-db`). They execute the EF bundle as `/app/efbundle --verbose -- --postgres-migrations`, which selects the Postgres migrations assembly and reads the production connection string from the injected configuration. This runs before the main container starts, ensuring the schema is always current before the application accepts traffic.

The init container uses the same image as the API (`agentweaver-api:${IMAGE_TAG}`) and reads `ConnectionStrings__MemoryDb` + `ConnectionStrings__Postgres` from the `agentweaver-postgres` Secret.
The init container uses the same image as the API (`agentweaver-api:${IMAGE_TAG}`) and reads `ConnectionStrings__MemoryDb` + `ConnectionStrings__Postgres` from the `agentweaver-postgres` Secret. No connection string is embedded in the image or manifest. Local design-time commands remain SQLite by default; use `--postgres-migrations` only when a Postgres connection string is supplied through configuration, environment variables, or Development user secrets.

### Ephemeral storage for testing

Expand Down
2 changes: 2 additions & 0 deletions k8s/base/api-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ spec:
command:
- /app/efbundle
- --verbose
- --
- --postgres-migrations
env:
- name: ASPNETCORE_ENVIRONMENT
value: Production
Expand Down
2 changes: 2 additions & 0 deletions k8s/base/worker-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ spec:
command:
- /app/efbundle
- --verbose
- --
- --postgres-migrations
env:
- name: ASPNETCORE_ENVIRONMENT
value: Production
Expand Down
2 changes: 2 additions & 0 deletions k8s/reference/job-ef-migrate-postgres.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ spec:
command:
- /app/efbundle
- --verbose
- --
- --postgres-migrations
env:
- name: ASPNETCORE_ENVIRONMENT
value: Production
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using Agentweaver.Api;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;

namespace Agentweaver.Tests.Persistence;

public sealed class MemoryDbContextDesignFactoryTests
{
[Fact]
public void PostgresMigrations_UsesInjectedPostgresConnectionString()
{
const string connectionString =
"Host=postgres-service;Database=agentweaver;Username=agentweaver;Password=test-only";

WithEnvironmentVariable("ConnectionStrings__Postgres", connectionString, () =>
{
using var context = new MemoryDbContextDesignFactory()
.CreateDbContext(["--postgres-migrations"]);

context.Database.ProviderName.Should().Be("Npgsql.EntityFrameworkCore.PostgreSQL");
context.Database.GetDbConnection().ConnectionString.Should().Be(connectionString);
});
}

[Fact]
public void DefaultDesignTimeWorkflow_RemainsLocalSqlite()
{
using var context = new MemoryDbContextDesignFactory().CreateDbContext([]);

context.Database.ProviderName.Should().Be("Microsoft.EntityFrameworkCore.Sqlite");
context.Database.GetDbConnection().ConnectionString.Should().Be("Data Source=agentweaver-design.db");
}

[Fact]
public void PostgresMigrations_WithoutConnectionString_DoesNotFallBackToSqlite()
{
WithEnvironmentVariables(
new Dictionary<string, string?>
{
["ASPNETCORE_ENVIRONMENT"] = "Production",
["ConnectionStrings__Postgres"] = null,
["ConnectionStrings__MemoryDb"] = null,
["Database__ConnectionString"] = null,
},
() =>
{
var create = () => new MemoryDbContextDesignFactory()
.CreateDbContext(["--postgres-migrations"]);

create.Should().Throw<InvalidOperationException>()
.WithMessage("*ConnectionStrings:Postgres*");
});
}

[Fact]
public void Factory_DoesNotEmbedLocalhostPostgresConfiguration()
{
var source = File.ReadAllText(
Path.Combine(RepositoryRoot(), "apps", "Agentweaver.Api", "MemoryDbContextDesignFactory.cs"));

source.Should().NotContain("Host=localhost", "production migration configuration must be injected");
source.Should().Contain("AddEnvironmentVariables");
source.Should().Contain("AddUserSecrets");
}

private static void WithEnvironmentVariable(string name, string value, Action assertion)
=> WithEnvironmentVariables(new Dictionary<string, string?> { [name] = value }, assertion);

private static void WithEnvironmentVariables(
IReadOnlyDictionary<string, string?> values,
Action assertion)
{
var previous = values.Keys.ToDictionary(
name => name,
Environment.GetEnvironmentVariable,
StringComparer.Ordinal);
try
{
foreach (var (name, value) in values)
Environment.SetEnvironmentVariable(name, value);

assertion();
}
finally
{
foreach (var (name, value) in previous)
Environment.SetEnvironmentVariable(name, value);
}
}

private static string RepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "agentweaver.sln")))
directory = directory.Parent;

return directory?.FullName
?? throw new DirectoryNotFoundException("Could not locate the repository root.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Text.RegularExpressions;
using FluentAssertions;

namespace Agentweaver.Tests.Persistence;

public sealed class PostgresMigrationManifestTests
{
[Theory]
[InlineData("api-deployment.yaml")]
[InlineData("worker-deployment.yaml")]
public void InitContainer_ForwardsPostgresMigrationsArgument(string fileName)
{
var manifest = File.ReadAllText(Path.Combine(RepositoryRoot(), "k8s", "base", fileName));

var command = Regex.Match(
manifest,
@"(?s)- name: migrate-memory-db\s+image:.*?command:\s+(?<command>- /app/efbundle\s+- --verbose\s+- --\s+- --postgres-migrations)\s+env:");

command.Success.Should().BeTrue($"{fileName} must select the Postgres EF migrations assembly");
manifest.Should().MatchRegex(
@"(?s)- name: ConnectionStrings__Postgres\s+valueFrom:\s+secretKeyRef:\s+name: agentweaver-postgres\s+key: connectionstring");
}

[Fact]
public void ReferencePostgresMigrationJob_ForwardsPostgresMigrationsArgument()
{
var manifest = File.ReadAllText(
Path.Combine(RepositoryRoot(), "k8s", "reference", "job-ef-migrate-postgres.yaml"));

manifest.Should().MatchRegex(
@"(?s)command:\s+- /app/efbundle\s+- --verbose\s+- --\s+- --postgres-migrations\s+env:");
}

private static string RepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "agentweaver.sln")))
directory = directory.Parent;

return directory?.FullName
?? throw new DirectoryNotFoundException("Could not locate the repository root.");
}
}
Loading