diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 40f8710c..d14c224e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -52,7 +52,7 @@ jobs:
run: dotnet restore
- name: Build
#run: dotnet pack ./grate/grate.csproj -c release -p:PackAsTool=true -p:PackageOutputPath=/tmp/grate/nupkg
- run: dotnet pack ./grate/grate.csproj -p:SelfContained=false -p:PackAsTool=true -p:PackageOutputPath=/tmp/grate/nupkg
+ run: dotnet pack ./src/grate/grate.csproj -p:SelfContained=false -p:PackAsTool=true -p:PackageOutputPath=/tmp/grate/nupkg
env:
VERSION: ${{ needs.set-version-number.outputs.nuGetVersion }}
@@ -66,6 +66,39 @@ jobs:
- name: Push to Nuget.org
if: ${{ needs.set-version-number.outputs.is-release == 'true' }}
run: dotnet nuget push /tmp/grate/nupkg/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{secrets.NUGET_ORG_KEY}} --skip-duplicate
+
+ build-nuget-package:
+ needs: set-version-number
+ name: Build Nuget Packages
+
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ package: [ "grate.mariadb",
+ "grate.oracle",
+ "grate.postgresql",
+ "grate.sqlite",
+ "grate.sqlserver"
+ ]
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET 8
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Pack Nuget package ${{ matrix.package }}
+ run: dotnet pack ./src/${{ matrix.package }} -c Release --include-symbols -o /tmp/grate/nupkg /p:Version=${{ env.VERSION }}
+ env:
+ VERSION: ${{ needs.set-version-number.outputs.nuGetVersion }}
+
+ - name: Push to Nuget.org
+ if: ${{ needs.set-version-number.outputs.is-release == 'true' }}
+ run: dotnet nuget push /tmp/grate/nupkg/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{secrets.NUGET_ORG_KEY}} --skip-duplicate
build-standalone:
name: Build
@@ -87,12 +120,12 @@ jobs:
dotnet-version: 8.0.x
- name: Publish self-contained ${{ matrix.arch }}
- run: dotnet publish ./grate/grate.csproj -f net8.0 -r ${{ matrix.arch }} -c release --self-contained -p:SelfContained=true -o ./publish/${{ matrix.arch }}/self-contained
+ run: dotnet publish ./src/grate/grate.csproj -f net8.0 -r ${{ matrix.arch }} -c release --self-contained -p:SelfContained=true -o ./publish/${{ matrix.arch }}/self-contained
env:
VERSION: ${{ needs.set-version-number.outputs.nuGetVersion }}
- name: Publish .NET 6/7/8 dependent ${{ matrix.arch }}
- run: dotnet publish ./grate/grate.csproj -r ${{ matrix.arch }} -c release --no-self-contained -o ./publish/${{ matrix.arch }}/dependent
+ run: dotnet publish ./src/grate/grate.csproj -r ${{ matrix.arch }} -c release --no-self-contained -o ./publish/${{ matrix.arch }}/dependent
env:
VERSION: ${{ needs.set-version-number.outputs.nuGetVersion }}
@@ -144,7 +177,7 @@ jobs:
name: Build and push docker image
needs:
- set-version-number
- - build-standalone
+ #- build-standalone ## no need, we build directly from source
runs-on: ubuntu-latest
if: ${{ needs.set-version-number.outputs.is-release == 'true' }}
env:
@@ -154,10 +187,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- - uses: actions/download-artifact@v3
- with:
- name: grate-linux-musl-x64-self-contained-${{ needs.set-version-number.outputs.nuGetVersion }}
- path: installers/docker/
+ # - uses: actions/download-artifact@v3 # download from another artifact is not a good idea, we need to build directly from source
+ # with:
+ # name: grate-linux-musl-x64-self-contained-${{ needs.set-version-number.outputs.nuGetVersion }}
+ # path: installers/docker/
- name: Log in to the Container registry
@@ -183,7 +216,8 @@ jobs:
- name: Build and push Docker image
uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56
with:
- context: ./installers/docker/
+ file: ./installers/docker/Dockerfile
+ context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -212,7 +246,7 @@ jobs:
arch=$(echo ${{ matrix.arch }} | cut -d- -f2 | sed 's/x64/amd64/')
echo "::set-output name=arch::$arch"
- - name: Create dpkg
+ - name: Create dpkg # Linux with powershell script? really?
if: ${{ needs.set-version-number.outputs.is-release == 'true' }}
run: ./installers/deb/Create-Package.ps1 -grateExe ./${{ matrix.arch }}/grate -Version "${{ needs.set-version-number.outputs.nuGetVersion }}" -arch ${{ steps.get-arch.outputs.arch}}
env:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 06373be4..ad665355 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,7 +29,7 @@ jobs:
run: |
dotnet restore -r linux-x64 grate.sln
- name: Build
- run: dotnet build -f net8.0 --no-restore --no-self-contained -r linux-x64 grate/grate.csproj -c release
+ run: dotnet build -f net8.0 --no-restore --no-self-contained -r linux-x64 src/grate/grate.csproj -c release
analyze:
@@ -47,8 +47,10 @@ jobs:
- name: Setup .NET 8
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 8.0.x
-
+ dotnet-version: |
+ 6.0.x
+ 7.0.x
+ 8.0.x
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
diff --git a/Directory.Build.props b/Directory.Build.props
index 02646c73..fb433b3d 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -24,7 +24,6 @@
net6.0;net7.0;net8.0
- net8.0
diff --git a/docs/GettingGrate.md b/docs/GettingGrate.md
index 31fbc3ca..7e55f5c3 100644
--- a/docs/GettingGrate.md
+++ b/docs/GettingGrate.md
@@ -16,6 +16,23 @@ The [github site](https://github.com/erikbra/grate/) has both the raw source cod
There's a `{{ site.github.repository_nwo }}` docker image published to [dockerhub](https://hub.docker.com/r/{{ site.github.repository_nwo }}) on every release. See the [examples](https://github.com/erikbra/grate/tree/main/examples) folder for a demo using this to a migration.
+Start the sqlserver database
+```sh
+docker network create grate_network && docker run -e SA_PASSWORD=gs8j4AS7h87jHg -e ACCEPT_EULA=Y --name db --network grate_network -d mcr.microsoft.com/mssql/server:2019-latest
+```
+Run grate migration
+```sh
+docker run -v ./examples/docker/db:/db -e APP_CONNSTRING="Server=db;Database=grate_test_db;User Id=sa;Password=gs8j4AS7h87jHg;TrustServerCertificate=True" --network grate_network erikbra/grate
+# run with database type, accept: sqlserver, postgresql, mariadb, sqlite, oracle
+# docker run -v ./examples/docker/db:/db -e DATABASE_TYPE=sqlserver -e CREATE_DATABASE=true -e ENVIRONMENT=Dev -e TRANSACTION=true -e APP_CONNSTRING="Server=db;Database=grate_test_db;User Id=sa;Password=gs8j4AS7h87jHg;TrustServerCertificate=True" --network grate_network erikbra/grate
+```
+
+Cleanup resources
+```sh
+
+docker kill db || docker network rm grate_network || docker rm $(docker ps -f status=exited | awk '{print $1}')
+```
+
## Dotnet Tool
grate is available as a [dotnet global tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools). Simply `dotnet tool install -g grate` to get the [package](https://www.nuget.org/packages/grate/).
diff --git a/examples/docker/build-and-run.ps1 b/examples/docker/build-and-run.ps1
index d6802897..607e5b88 100644
--- a/examples/docker/build-and-run.ps1
+++ b/examples/docker/build-and-run.ps1
@@ -1,5 +1,4 @@
#!/bin/env pwsh
# App versioning is normally provided by your CI/CD pipelines...
-docker-compose build --build-arg APP_VERSION=0.0.1
docker-compose up
\ No newline at end of file
diff --git a/examples/docker/db/runFirstAfterUp/001_greeting.sql b/examples/docker/db/runFirstAfterUp/001_greeting.sql
new file mode 100644
index 00000000..ab7b06ae
--- /dev/null
+++ b/examples/docker/db/runFirstAfterUp/001_greeting.sql
@@ -0,0 +1 @@
+INSERT INTO grate_test(name) VALUES ('Hello grate from docker !');
\ No newline at end of file
diff --git a/examples/docker/db/up/001_create_table.sql b/examples/docker/db/up/001_create_table.sql
new file mode 100644
index 00000000..b28b5d05
--- /dev/null
+++ b/examples/docker/db/up/001_create_table.sql
@@ -0,0 +1,4 @@
+ CREATE TABLE grate_test (
+ id int IDENTITY(1,1) NOT NULL PRIMARY KEY,
+ name nvarchar(255) NOT NULL
+ )
\ No newline at end of file
diff --git a/examples/docker/docker-compose.yml b/examples/docker/docker-compose.yml
index 16b3502c..822ec09d 100644
--- a/examples/docker/docker-compose.yml
+++ b/examples/docker/docker-compose.yml
@@ -1,11 +1,17 @@
+version: "3.7"
services:
db-migration:
- build: .
- image: myapp-dbmigration
+ #build: .
+ image: erikbra/grate:latest
environment:
# don't configure passwords here for real. This is just a sample!
- - APP_CONNSTRING="Server=db;Database=grate_test;User Id=sa;Password=gs8j4AS7h87jHg"
-
+ APP_CONNSTRING: "Server=db;Database=grate_test;User Id=sa;Password=gs8j4AS7h87jHg;TrustServerCertificate=True"
+ VERSION: "1.0.0.0"
+ DATABASE_TYPE: "sqlserver" # sqlite, oracle, postgresql, sqlserver, mariadb
+ volumes:
+ - ./db:/db
+ - ./output:/output
+
depends_on:
- db
db:
@@ -13,4 +19,6 @@ services:
environment:
- SA_PASSWORD=gs8j4AS7h87jHg # again, plain text passwords are bad mmkay!
- ACCEPT_EULA=Y
- - MSSQL_PID=Express
\ No newline at end of file
+ - MSSQL_PID=Express
+ ports:
+ - "1433:1433"
\ No newline at end of file
diff --git a/examples/docker/dockerfile b/examples/docker/dockerfile
deleted file mode 100644
index 995b6c32..00000000
--- a/examples/docker/dockerfile
+++ /dev/null
@@ -1,17 +0,0 @@
-FROM erikbra/grate:latest
-
-# Env Vars we need set at image runtime in order to control grate
-ENV APP_CONNSTRING=""
-
-# We set the app-version at build time, as it's the same regardless of environment
-ARG APP_VERSION
-ENV VERSION=$APP_VERSION
-
-WORKDIR /app
-
-# Get the sql scripts into the image
-COPY ./db ./db
-RUN mkdir /app/migration-output
-
-ENTRYPOINT ./grate \
--f=db --version=$VERSION --connstring="$APP_CONNSTRING" -silent --outputPath=./migration-output
diff --git a/examples/docker/output/.gitignore b/examples/docker/output/.gitignore
new file mode 100644
index 00000000..f59ec20a
--- /dev/null
+++ b/examples/docker/output/.gitignore
@@ -0,0 +1 @@
+*
\ No newline at end of file
diff --git a/examples/docker/readme.md b/examples/docker/readme.md
index 1d229dcf..abca962a 100644
--- a/examples/docker/readme.md
+++ b/examples/docker/readme.md
@@ -5,9 +5,8 @@ This directory shows a very simple way of building a docker container to apply y
## Usage
Simply `docker-compose up` to:
-- build a local `myapp-dbmigration` image that contains both grate and the migration scripts based on the published `grate` image.
- start a sql database server
-- run the `myapp-dbmigration` migration against the server
+- run the `grate` migration against the server with script locate in `db` folder and store the backup script in `output`
## Notes
diff --git a/examples/k8s/initcontainer/Dockerfile b/examples/k8s/initcontainer/Dockerfile
new file mode 100644
index 00000000..5d1b9be1
--- /dev/null
+++ b/examples/k8s/initcontainer/Dockerfile
@@ -0,0 +1,18 @@
+FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
+WORKDIR .
+
+COPY sample-service/ ./sample-service/
+RUN dotnet publish ./sample-service/*.csproj -c release -o ./publish/app
+
+FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine as runtime
+WORKDIR /app
+
+COPY --from=build /publish/app .
+
+# Add globalization support to the OS so .Net can use cultures
+RUN apk add icu-libs
+ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
+ENV ASPNETCORE_URLS=http://[::]:80
+ENV ASPNETCORE_ENVIRONMENT=Production
+
+ENTRYPOINT ["dotnet", "sample-service.dll"]
\ No newline at end of file
diff --git a/examples/k8s/initcontainer/Dockerfile-db b/examples/k8s/initcontainer/Dockerfile-db
new file mode 100644
index 00000000..e05069fb
--- /dev/null
+++ b/examples/k8s/initcontainer/Dockerfile-db
@@ -0,0 +1,11 @@
+FROM erikbra/grate:latest as base
+WORKDIR /app
+COPY sql/ /db
+RUN mkdir /output
+ENTRYPOINT ./grate \
+ --sqlfilesdirectory=/db \
+ --version=$VERSION \
+ --connstring="$APP_CONNSTRING" \
+ --silent \
+ --databasetype=sqlserver \
+ --outputPath=/output
\ No newline at end of file
diff --git a/examples/k8s/initcontainer/README.md b/examples/k8s/initcontainer/README.md
new file mode 100644
index 00000000..8ea2f7f6
--- /dev/null
+++ b/examples/k8s/initcontainer/README.md
@@ -0,0 +1,42 @@
+## Grate with k8s
+
+You can propably run grate on your production environment using k8s init container. Please see the very basic example how to config and deploy to k8s.
+
+## Prerequisite:
+
+Local k8s simulator: you can use [minikube](https://github.com/kubernetes/minikube) (my favorite) or [kind](https://github.com/kubernetes-sigs/kind)
+
+## Usage
+
+Now let's get started with your terminal (any Linux dist, MacOS or WSL2):
+ - Open your terminal and start minikube
+
+```sh
+ minikube start
+```
+
+ - Apply the deployment
+
+```sh
+ kubectl apply -f deployment.yaml
+```
+ - You can check the status with command
+ ```sh
+ kubectl get pods -w | grep grate
+ ```
+ - After the pod started, let's test the data :D
+
+```sh
+ kubectl port-forward svc/grate-k8s 5000:5000
+ # sending the http request
+ curl -sL http://localhost:5000/api/grate | jq
+```
+- Done. Remember to destroy the cluster
+```sh
+ minikube stop
+```
+
+## Notes
+
+- Curious how it works, see the `Dockerfile` and `Dockerfile-db`.
+
diff --git a/examples/k8s/initcontainer/deployment.yaml b/examples/k8s/initcontainer/deployment.yaml
new file mode 100644
index 00000000..848deb3e
--- /dev/null
+++ b/examples/k8s/initcontainer/deployment.yaml
@@ -0,0 +1,84 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mssql
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: mssql
+ template:
+ metadata:
+ labels:
+ app: mssql
+ spec:
+ containers:
+ - name: mssql
+ image: mcr.microsoft.com/mssql/server:2019-latest
+ env:
+ - name: SA_PASSWORD
+ value: "gs8j4AS7h87jHg"
+ - name: ACCEPT_EULA
+ value: "Y"
+ - name: MSSQL_PID
+ value: "Express"
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: grate-k8s-example
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: grate-k8s
+ template:
+ metadata:
+ labels:
+ app: grate-k8s
+ spec:
+ initContainers:
+ - name: db-migration
+ image: erikbra/grate-sample-service:migration-latest
+ env:
+ - name: APP_CONNSTRING
+ value: "Server=db;Database=grate_test;User Id=sa;Password=gs8j4AS7h87jHg;TrustServerCertificate=True"
+ - name: VERSION
+ value: "1.0.0"
+ containers:
+ - name: sample-service
+ image: erikbra/grate-sample-service:latest
+ env:
+ - name: ConnectionStrings__DefaultConnection
+ value: "Server=db;Database=grate_test;User Id=sa;Password=gs8j4AS7h87jHg;TrustServerCertificate=True"
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: db
+ labels:
+ app: db
+spec:
+ ports:
+ - port: 1433
+ targetPort: 1433
+ protocol: TCP
+ selector:
+ app: mssql
+ type: ClusterIP
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: grate-k8s
+ labels:
+ app: db
+spec:
+ ports:
+ - port: 5000
+ targetPort: 80
+ protocol: TCP
+ name: http
+ selector:
+ app: grate-k8s
+ type: ClusterIP
\ No newline at end of file
diff --git a/examples/k8s/initcontainer/sample-service/Controllers/GrateController.cs b/examples/k8s/initcontainer/sample-service/Controllers/GrateController.cs
new file mode 100644
index 00000000..2ca1a815
--- /dev/null
+++ b/examples/k8s/initcontainer/sample-service/Controllers/GrateController.cs
@@ -0,0 +1,24 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Data.SqlClient;
+using Dapper;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using System;
+namespace Controllers;
+
+[Route("api/[controller]")]
+[ApiController]
+public class Grate : ControllerBase
+{
+ [HttpGet]
+ public async Task Hello([FromServices] IConfiguration configuration)
+ {
+ var connectionString = configuration["ConnectionStrings:DefaultConnection"] ?? throw new Exception("No connection string found in appsettings");
+ var dbConnection = new SqlConnection(connectionString);
+ var query = "select id, name from grate_test";
+ var result = await dbConnection.QueryAsync<(int, string)>(query);
+ return new OkObjectResult(result.Select(r => new { id = r.Item1, name = r.Item2 }).ToArray());
+ }
+
+}
diff --git a/examples/k8s/initcontainer/sample-service/Program.cs b/examples/k8s/initcontainer/sample-service/Program.cs
new file mode 100644
index 00000000..8afe318b
--- /dev/null
+++ b/examples/k8s/initcontainer/sample-service/Program.cs
@@ -0,0 +1,8 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddControllers();
+var app = builder.Build();
+app.UseRouting();
+app.MapControllers();
+app.Run();
diff --git a/examples/k8s/initcontainer/sample-service/appsettings.json b/examples/k8s/initcontainer/sample-service/appsettings.json
new file mode 100644
index 00000000..d4ecd0f3
--- /dev/null
+++ b/examples/k8s/initcontainer/sample-service/appsettings.json
@@ -0,0 +1,11 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "ConnectionStrings": {
+ "DefaultConnection": "Server=localhost;Database=grate_test;User Id=sa;Password=gs8j4AS7h87jHg;TrustServerCertificate=True"
+ }
+}
\ No newline at end of file
diff --git a/examples/k8s/initcontainer/sample-service/sample-service.csproj b/examples/k8s/initcontainer/sample-service/sample-service.csproj
new file mode 100644
index 00000000..18d680c2
--- /dev/null
+++ b/examples/k8s/initcontainer/sample-service/sample-service.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+
+
+
+
+
+
+
+
diff --git a/examples/k8s/initcontainer/sql/runFirstAfterUp/001_greeting.sql b/examples/k8s/initcontainer/sql/runFirstAfterUp/001_greeting.sql
new file mode 100644
index 00000000..183228ea
--- /dev/null
+++ b/examples/k8s/initcontainer/sql/runFirstAfterUp/001_greeting.sql
@@ -0,0 +1 @@
+INSERT INTO grate_test(name) VALUES ('Hello grate from k8s!');
\ No newline at end of file
diff --git a/examples/k8s/initcontainer/sql/up/001_create_table.sql b/examples/k8s/initcontainer/sql/up/001_create_table.sql
new file mode 100644
index 00000000..b28b5d05
--- /dev/null
+++ b/examples/k8s/initcontainer/sql/up/001_create_table.sql
@@ -0,0 +1,4 @@
+ CREATE TABLE grate_test (
+ id int IDENTITY(1,1) NOT NULL PRIMARY KEY,
+ name nvarchar(255) NOT NULL
+ )
\ No newline at end of file
diff --git a/examples/k8s/initcontainer/sql/views/test.sql b/examples/k8s/initcontainer/sql/views/test.sql
new file mode 100644
index 00000000..976b1770
--- /dev/null
+++ b/examples/k8s/initcontainer/sql/views/test.sql
@@ -0,0 +1,2 @@
+create or alter view test as
+select 1 as test;
\ No newline at end of file
diff --git a/examples/k8s/multitenancy/Dockerfile b/examples/k8s/multitenancy/Dockerfile
new file mode 100644
index 00000000..70ca6708
--- /dev/null
+++ b/examples/k8s/multitenancy/Dockerfile
@@ -0,0 +1,19 @@
+FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
+WORKDIR .
+
+COPY sample-service/ ./sample-service/
+RUN dotnet publish ./sample-service/*.csproj -c release -o ./publish/app
+
+FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine as runtime
+WORKDIR /app
+
+COPY --from=build /publish/app .
+COPY sql/ /db
+
+# Add globalization support to the OS so .Net can use cultures
+RUN apk add icu-libs
+ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
+ENV ASPNETCORE_URLS=http://[::]:80
+ENV ASPNETCORE_ENVIRONMENT=Production
+
+ENTRYPOINT ["dotnet", "sample-service.dll"]
\ No newline at end of file
diff --git a/examples/k8s/multitenancy/Dockerfile-db b/examples/k8s/multitenancy/Dockerfile-db
new file mode 100644
index 00000000..b85b769c
--- /dev/null
+++ b/examples/k8s/multitenancy/Dockerfile-db
@@ -0,0 +1,7 @@
+FROM erikbra/grate:latest as base
+WORKDIR /app
+COPY sql/ /db
+COPY script/ ./
+RUN chmod +x ./migrate.sh
+RUN mkdir /output
+ENTRYPOINT ["./migrate.sh"]
\ No newline at end of file
diff --git a/examples/k8s/multitenancy/README.md b/examples/k8s/multitenancy/README.md
new file mode 100644
index 00000000..a62d8ed6
--- /dev/null
+++ b/examples/k8s/multitenancy/README.md
@@ -0,0 +1,44 @@
+## Grate with k8s
+
+You can propably run grate on your production environment using k8s init container. Please see the very basic example how to config and deploy to k8s.
+
+## Prerequisite:
+
+Local k8s simulator: you can use [minikube](https://github.com/kubernetes/minikube) (my favorite) or [kind](https://github.com/kubernetes-sigs/kind)
+
+## Usage
+
+Now let's get started with your terminal (any Linux dist, MacOS or WSL2):
+ - Open your terminal and start minikube
+
+```sh
+ minikube start
+```
+
+ - Apply the deployment
+
+```sh
+ kubectl apply -f deployment.yaml
+```
+ - You can check the status with command
+ ```sh
+ kubectl get pods -w | grep grate
+ ```
+ - After the pod started, let's test the data :D
+
+```sh
+ kubectl port-forward svc/grate-k8s 5000:5000
+ # sending the http request to create new database, take a while
+ curl --location --request POST 'http://localhost:5000/api/tenant' | jq
+ # test the database migration
+ curl -sL http://localhost:5000/api/grate?targetDatabase={databaseFromTenantRequest} | jq
+```
+- Done. Remember to destroy the cluster
+```sh
+ minikube stop
+```
+
+## Notes
+
+- Curious how it works, see the `Dockerfile` and `Dockerfile-db`.
+
diff --git a/examples/k8s/multitenancy/deployment.yaml b/examples/k8s/multitenancy/deployment.yaml
new file mode 100644
index 00000000..7c893a5d
--- /dev/null
+++ b/examples/k8s/multitenancy/deployment.yaml
@@ -0,0 +1,92 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mssql
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: mssql
+ template:
+ metadata:
+ labels:
+ app: mssql
+ spec:
+ containers:
+ - name: mssql
+ image: mcr.microsoft.com/mssql/server:2019-latest
+ env:
+ - name: SA_PASSWORD
+ value: "gs8j4AS7h87jHg"
+ - name: ACCEPT_EULA
+ value: "Y"
+ - name: MSSQL_PID
+ value: "Express"
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: grate-k8s-example
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: grate-k8s
+ template:
+ metadata:
+ labels:
+ app: grate-k8s
+ spec:
+ initContainers:
+ - name: db-migration
+ image: erikbra/grate-sample-service:migration-latest
+ env:
+ - name: ADMIN_CONNSTRING
+ value: "Server=db;Database=master;User Id=sa;Password=gs8j4AS7h87jHg;TrustServerCertificate=True"
+ - name: APP_CONNSTRING
+ value: "Server=db;Database={{targetDatabase}};User Id=sa;Password=gs8j4AS7h87jHg;TrustServerCertificate=True"
+ - name: Database__Databases
+ value: "tenant_a,tenant_b,tenant_c" # comma separate
+ - name: Database__Env
+ value: "PROD"
+ - name: Database__Type
+ value: "sqlserver" # mariadb | oracle | postgresql | sqlite | sqlserver
+ containers:
+ - name: sample-service
+ image: erikbra/grate-sample-service:latest
+ env:
+ - name: ConnectionStrings__DefaultConnection
+ value: "Server=db;Database=example;User Id=sa;Password=gs8j4AS7h87jHg;TrustServerCertificate=True"
+ - name: ConnectionStrings__AdminConnection
+ value: "Server=db;Database=master;User Id=sa;Password=gs8j4AS7h87jHg;TrustServerCertificate=True"
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: db
+ labels:
+ app: db
+spec:
+ ports:
+ - port: 1433
+ targetPort: 1433
+ protocol: TCP
+ selector:
+ app: mssql
+ type: ClusterIP
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: grate-k8s
+ labels:
+ app: db
+spec:
+ ports:
+ - port: 5000
+ targetPort: 80
+ protocol: TCP
+ name: http
+ selector:
+ app: grate-k8s
+ type: ClusterIP
\ No newline at end of file
diff --git a/examples/k8s/multitenancy/sample-service/Controllers/GrateController.cs b/examples/k8s/multitenancy/sample-service/Controllers/GrateController.cs
new file mode 100644
index 00000000..72c28a1c
--- /dev/null
+++ b/examples/k8s/multitenancy/sample-service/Controllers/GrateController.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Dapper;
+using grate.Configuration;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Data.SqlClient;
+using Microsoft.Extensions.Configuration;
+using SampleService.Extension;
+namespace Controllers;
+
+[Route("api/[controller]")]
+[ApiController]
+public class Grate : ControllerBase
+{
+ [HttpGet]
+ public async Task Hello([FromServices] GrateConfiguration grateConfiguration, [FromQuery] string databaseName)
+ {
+ grateConfiguration.SwitchDatabase(databaseName);
+ var dbConnection = new SqlConnection(grateConfiguration.ConnectionString);
+ var query = "select id, name from grate_test";
+ var result = await dbConnection.QueryAsync<(int, string)>(query);
+ return new OkObjectResult(result.Select(r => new { id = r.Item1, name = r.Item2 }).ToArray());
+ }
+}
diff --git a/examples/k8s/multitenancy/sample-service/Controllers/TenantController.cs b/examples/k8s/multitenancy/sample-service/Controllers/TenantController.cs
new file mode 100644
index 00000000..41cb5eba
--- /dev/null
+++ b/examples/k8s/multitenancy/sample-service/Controllers/TenantController.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Dapper;
+using grate.Configuration;
+using grate.Migration;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Data.SqlClient;
+using Microsoft.Extensions.Configuration;
+using SampleService.Extension;
+namespace Controllers;
+
+[Route("api/[controller]")]
+[ApiController]
+public class TenantController : ControllerBase
+{
+ [HttpPost]
+ public async Task Create([FromServices] GrateConfiguration grateConfiguration, [FromServices] IGrateMigrator grateMigrator)
+ {
+ var newDatabaseId = $"tenant_{Guid.NewGuid():N}";
+ // swith to new connectionstring
+ grateConfiguration.SwitchDatabase(newDatabaseId);
+
+ // consider to use hosted service to run this in background if you care about the performance
+ await grateMigrator.Migrate();
+ return new OkObjectResult(new { id = newDatabaseId });
+ }
+}
diff --git a/examples/k8s/multitenancy/sample-service/Extensions/ConnectionStringExtension.cs b/examples/k8s/multitenancy/sample-service/Extensions/ConnectionStringExtension.cs
new file mode 100644
index 00000000..60674d91
--- /dev/null
+++ b/examples/k8s/multitenancy/sample-service/Extensions/ConnectionStringExtension.cs
@@ -0,0 +1,14 @@
+using System.Text.RegularExpressions;
+using grate.Configuration;
+
+namespace SampleService.Extension;
+public static class ConnectionStringExtension
+{
+ public static void SwitchDatabase(this GrateConfiguration grateConfiguration, string targetDatabase)
+ {
+ var pattern = new Regex("(.*;\\s*(?:Initial Catalog|Database)=)([^;]*)(.*)");
+ var replacement = $"$1{targetDatabase}$3";
+ var replaced = pattern.Replace(grateConfiguration.ConnectionString!, replacement);
+ grateConfiguration.ConnectionString = replaced;
+ }
+}
diff --git a/examples/k8s/multitenancy/sample-service/Program.cs b/examples/k8s/multitenancy/sample-service/Program.cs
new file mode 100644
index 00000000..ef7e0fc6
--- /dev/null
+++ b/examples/k8s/multitenancy/sample-service/Program.cs
@@ -0,0 +1,18 @@
+using grate;
+using grate.SqlServer;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+var builder = WebApplication.CreateBuilder(args);
+var configuration = builder.Configuration;
+builder.Services.AddControllers();
+builder.Services.AddGrate(grateBuilder =>
+{
+ grateBuilder.WithAdminConnectionString(configuration.GetConnectionString("AdminConnection")!);
+ grateBuilder.WithConnectionString(configuration.GetConnectionString("DefaultConnection")!);
+ grateBuilder.WithSqlFilesDirectory("/db");
+ grateBuilder.UseSqlServer();
+});
+var app = builder.Build();
+app.UseRouting();
+app.MapControllers();
+app.Run();
diff --git a/examples/k8s/multitenancy/sample-service/appsettings.json b/examples/k8s/multitenancy/sample-service/appsettings.json
new file mode 100644
index 00000000..4e2b314a
--- /dev/null
+++ b/examples/k8s/multitenancy/sample-service/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "ConnectionStrings": {
+ "DefaultConnection": "Server=localhost;Database=grate_test;User Id=sa;Password=gs8j4AS7h87jHg;TrustServerCertificate=True",
+ "AdminConnection": "Server=localhost;Database=master;User Id=sa;Password=gs8j4AS7h87jHg;TrustServerCertificate=True"
+ }
+}
\ No newline at end of file
diff --git a/examples/k8s/multitenancy/sample-service/sample-service.csproj b/examples/k8s/multitenancy/sample-service/sample-service.csproj
new file mode 100644
index 00000000..2c5507c7
--- /dev/null
+++ b/examples/k8s/multitenancy/sample-service/sample-service.csproj
@@ -0,0 +1,12 @@
+
+
+
+ net8.0
+ enable
+
+
+
+
+
+
+
diff --git a/examples/k8s/multitenancy/script/migrate.sh b/examples/k8s/multitenancy/script/migrate.sh
new file mode 100644
index 00000000..020c13e3
--- /dev/null
+++ b/examples/k8s/multitenancy/script/migrate.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+# https://erikbra.github.io/grate/getting-started/
+# https://erikbra.github.io/grate/configuration-options/
+# databasetype
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
\ No newline at end of file
diff --git a/grate/Configuration/DatabaseType.cs b/grate/Configuration/DatabaseType.cs
deleted file mode 100644
index 81c4ff8b..00000000
--- a/grate/Configuration/DatabaseType.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-// ReSharper disable InconsistentNaming
-namespace grate.Configuration;
-
-public enum DatabaseType
-{
- sqlserver,
- oracle,
- postgresql,
- mariadb,
- sqlite
-}
diff --git a/grate/Infrastructure/Factory.cs b/grate/Infrastructure/Factory.cs
deleted file mode 100644
index a61f6d7d..00000000
--- a/grate/Infrastructure/Factory.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace grate.Infrastructure;
-
-public class Factory : IFactory
-{
- private readonly IServiceProvider _provider;
- private readonly Dictionary