diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..cfa292cd --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github:, [Nano-Core] diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml new file mode 100644 index 00000000..130b0217 --- /dev/null +++ b/.github/workflows/build-and-deploy.yml @@ -0,0 +1,34 @@ +name: Build and Deploy +on: + pull_request: + branches: + - master + push: + branches: + - master +env: + VERSION: 10.0.0-rc1 +jobs: + build-and-deploy: + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + concurrency: + group: ${{ github.repository }} + cancel-in-progress: false + steps: + - uses: actions/checkout@v6 + + - name: GitHub Release + if: github.ref == 'refs/heads/master' + uses: ncipollo/release-action@v1 + with: + tag: v${{ env.VERSION }}.${{ github.run_number }} + name: "Release ${{ env.VERSION }}" + body: | + Version: ${{ env.VERSION }} + Commit: ${{ github.sha }} + token: ${{ secrets.GITHUB_TOKEN }} + draft: false + prerelease: ${{ contains(env.VERSION, '-') }} \ No newline at end of file diff --git a/Api.Documentation.Nonce/.gitignore b/.gitignore similarity index 100% rename from Api.Documentation.Nonce/.gitignore rename to .gitignore diff --git a/Api.Documentation.Nonce/.docker/docker-compose.dcproj b/Api.ApiClients.Audit/.docker/docker-compose.dcproj similarity index 100% rename from Api.Documentation.Nonce/.docker/docker-compose.dcproj rename to Api.ApiClients.Audit/.docker/docker-compose.dcproj diff --git a/Api.ApiClients.Audit/.docker/docker-compose.yml b/Api.ApiClients.Audit/.docker/docker-compose.yml new file mode 100644 index 00000000..3701e77a --- /dev/null +++ b/Api.ApiClients.Audit/.docker/docker-compose.yml @@ -0,0 +1,44 @@ +services: + api.apiclients.audit: + image: api.apiclients.audit + hostname: api-apiclients-audit + restart: on-failure + ports: + - 8080:8080 + build: + context: ../Api.ApiClients.Audit + dockerfile: "Dockerfile.Local" + networks: + - network + + api.apiclients.audit.service: + image: api.apiclients.audit.service + hostname: api-apiclients-audit-service + restart: on-failure + ports: + - 8181:8181 + build: + context: ../Api.ApiClients.Audit.Service + dockerfile: "Dockerfile.Local" + depends_on: + - database + networks: + - network + + database: + image: mysql/mysql-server:latest + ports: + - 3306:3306 + networks: + - network + environment: + MYSQL_USER: sa + MYSQL_PASSWORD: myPassword_123 + MYSQL_ROOT_PASSWORD: myPassword_123 + MYSQL_DATABASE: nanoDb + MYSQL_ROOT_HOST: '%' + +networks: + network: + name: network + driver: bridge diff --git a/Api.Documentation.Nonce/.dockerignore b/Api.ApiClients.Audit/.dockerignore similarity index 100% rename from Api.Documentation.Nonce/.dockerignore rename to Api.ApiClients.Audit/.dockerignore diff --git a/Api.Documentation.Nonce/.github/config/slack.yml b/Api.ApiClients.Audit/.github/config/slack.yml similarity index 100% rename from Api.Documentation.Nonce/.github/config/slack.yml rename to Api.ApiClients.Audit/.github/config/slack.yml diff --git a/Api.Documentation.Nonce/.github/workflows/build-and-deploy.yml b/Api.ApiClients.Audit/.github/workflows/build-and-deploy.yml similarity index 64% rename from Api.Documentation.Nonce/.github/workflows/build-and-deploy.yml rename to Api.ApiClients.Audit/.github/workflows/build-and-deploy.yml index 420e282f..2e9dbcea 100644 --- a/Api.Documentation.Nonce/.github/workflows/build-and-deploy.yml +++ b/Api.ApiClients.Audit/.github/workflows/build-and-deploy.yml @@ -1,29 +1,32 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: - APP_NAME: Api.Documentation.Nonce - IMAGE_NAME: api.documentation.nonce - SERVICE_NAME: api-documentation-nonce + APP_NAME: Api.ApiClients.Audit + IMAGE_NAME: api.apiclients.audit + SERVICE_NAME: api-apiclients-audit VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -33,30 +36,31 @@ env: KUBERNETES_CPU_REQUEST: 200m KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 - CERTIFICATE_ISSUER: letsencrypt-prod - CERTIFICATE_ORGANIZATION: ${{ vars.CERTIFICATE_ORGANIZATION }} - CERTIFICATE_HOST: ${{ github.ref == 'refs/heads/master' && vars.HOST_API_SUBDOMAIN + '.' + vars.PRODUCTION_HOST || vars.HOST_API_SUBDOMAIN + '.' + vars.STAGING_HOST }} ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - NONCE_TOKEN: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_NONCE_TOKEN || secrets.STAGING_NONCE_TOKEN }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -85,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -101,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -114,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -123,43 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - Get-Content .kubernetes/certificate.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/certificate.tmp.yaml; - sudo kubectl apply -f .kubernetes/certificate.tmp.yaml; - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - Get-Content .kubernetes/ingress.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/ingress.tmp.yaml; - sudo kubectl apply -f .kubernetes/ingress.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.ApiClients.Audit/.gitignore b/Api.ApiClients.Audit/.gitignore new file mode 100644 index 00000000..9921fc06 --- /dev/null +++ b/Api.ApiClients.Audit/.gitignore @@ -0,0 +1,11 @@ +.vs +*.user +*.userprefs +*.suo +_ReSharper* +**/bin +**/obj +*.DotSettings.User +packages +.env +**/Properties/launchSettings.json \ No newline at end of file diff --git a/Api.Documentation.Nonce/.kubernetes/autoscaler.yaml b/Api.ApiClients.Audit/.kubernetes/autoscaler.yaml similarity index 100% rename from Api.Documentation.Nonce/.kubernetes/autoscaler.yaml rename to Api.ApiClients.Audit/.kubernetes/autoscaler.yaml diff --git a/Api.Documentation.Nonce/.kubernetes/configmap.yaml b/Api.ApiClients.Audit/.kubernetes/configmap.yaml similarity index 100% rename from Api.Documentation.Nonce/.kubernetes/configmap.yaml rename to Api.ApiClients.Audit/.kubernetes/configmap.yaml diff --git a/Api.Documentation.Nonce/.kubernetes/deployment.yaml b/Api.ApiClients.Audit/.kubernetes/deployment.yaml similarity index 91% rename from Api.Documentation.Nonce/.kubernetes/deployment.yaml rename to Api.ApiClients.Audit/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.Documentation.Nonce/.kubernetes/deployment.yaml +++ b/Api.ApiClients.Audit/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Documentation.Nonce/.kubernetes/service.yaml b/Api.ApiClients.Audit/.kubernetes/service.yaml similarity index 100% rename from Api.Documentation.Nonce/.kubernetes/service.yaml rename to Api.ApiClients.Audit/.kubernetes/service.yaml diff --git a/Api.Documentation.Nonce/.tests/Tests.Api.Documentation.Nonce/Properties/DoNotParallelize.cs b/Api.ApiClients.Audit/.tests/Tests.Api.ApiClients.Audit/Properties/DoNotParallelize.cs similarity index 100% rename from Api.Documentation.Nonce/.tests/Tests.Api.Documentation.Nonce/Properties/DoNotParallelize.cs rename to Api.ApiClients.Audit/.tests/Tests.Api.ApiClients.Audit/Properties/DoNotParallelize.cs diff --git a/Api.ApiClients.Audit/.tests/Tests.Api.ApiClients.Audit/Tests.Api.ApiClients.Audit.csproj b/Api.ApiClients.Audit/.tests/Tests.Api.ApiClients.Audit/Tests.Api.ApiClients.Audit.csproj new file mode 100644 index 00000000..2e895288 --- /dev/null +++ b/Api.ApiClients.Audit/.tests/Tests.Api.ApiClients.Audit/Tests.Api.ApiClients.Audit.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + false + latest + + + true + + + + + + + + + + + + + + diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Models/Api.ApiClients.Audit.Models.csproj b/Api.ApiClients.Audit/Api.ApiClients.Audit.Models/Api.ApiClients.Audit.Models.csproj new file mode 100644 index 00000000..c33a8e9a --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Models/Api.ApiClients.Audit.Models.csproj @@ -0,0 +1,75 @@ + + + + net10.0 + + 10.0.0.0 + 10.0.0.0 + 10.0.0.0 + AnyCPU + Debug;Release + latest + en-US + enable + LICENSE + true + + True + true + true + true + Michael Vivet + Michael Vivet + $(ProjectName) + Example project demonstrating a focused aspect of the Nano Library. + + A practical example highlighting a specific area or scenario of Nano for learning + and experimentation. Shows how features or patterns can be applied without + representing a full application or complete reference. + + + - Added .NET 10 support + + git + master + https://github.com/Nano-Core/Nano.Examples.git + $(GitCommitHash) + true + true + true + snupkg + true + $(Version) + nano;example;sample;framework;library;learning;showcase + https://github.com/Nano-Core/Nano.Library/$(ProjectName) + LICENSE + icon.png + README.md + true + + + + True + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/Api.ApiClients.Audit.Service.Models.csproj b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/Api.ApiClients.Audit.Service.Models.csproj new file mode 100644 index 00000000..82be9b94 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/Api.ApiClients.Audit.Service.Models.csproj @@ -0,0 +1,77 @@ + + + + net10.0 + + 10.0.0.0 + 10.0.0.0 + 10.0.0.0 + AnyCPU + Debug;Release + latest + en-US + enable + LICENSE + true + + True + true + true + true + Michael Vivet + Michael Vivet + $(ProjectName) + Example project demonstrating a focused aspect of the Nano Library. + + A practical example highlighting a specific area or scenario of Nano for learning + and experimentation. Shows how features or patterns can be applied without + representing a full application or complete reference. + + + - Added .NET 10 support + + git + master + https://github.com/Nano-Core/Nano.Examples.git + $(GitCommitHash) + true + true + true + snupkg + true + $(Version) + nano;example;sample;framework;library;learning;showcase + https://github.com/Nano-Core/Nano.Library/$(ProjectName) + LICENSE + icon.png + README.md + true + + + + True + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/ApiClient/NanoApiClient.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/ApiClient/NanoApiClient.cs new file mode 100644 index 00000000..23e08224 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/ApiClient/NanoApiClient.cs @@ -0,0 +1,8 @@ +using Nano.App.ApiClient; + +namespace Api.ApiClients.Audit.Service.Models.ApiClient; + +/// +/// Nano Api Client. +/// +public class NanoApiClient(Nano.App.ApiClient.ApiClient apiClient) : BaseApiClient(apiClient); \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/Criterias/ExampleQueryCriteria.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/Criterias/ExampleQueryCriteria.cs new file mode 100644 index 00000000..f35de66c --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/Criterias/ExampleQueryCriteria.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using DynamicExpression; +using Nano.App.Api.Controllers.Criteria; + +namespace Api.ApiClients.Audit.Service.Models.Criterias; + +/// +public class ExampleQueryCriteria : BaseQueryCriteria +{ + /// + /// Name. + /// + public virtual string? Name { get; set; } + + /// + public override IList GetExpressions() + { + var expressions = base.GetExpressions(); + + var expression = expressions.FirstOrDefault() ?? new CriteriaExpression(); + + if (!string.IsNullOrEmpty(this.Name)) + { + expression + .StartsWith(nameof(Example.Name), this.Name); + } + + expressions + .Add(expression); + + return expressions; + } +} \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/Example.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/Example.cs new file mode 100644 index 00000000..0ed5bd69 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/Example.cs @@ -0,0 +1,28 @@ +using System; +using Nano.Data.Abstractions.Annotations; +using Nano.Data.Abstractions.Models; +using Nano.Data.Abstractions.Models.Abstractions; + +namespace Api.ApiClients.Audit.Service.Models; + +/// +/// Example. +/// +public class Example : BaseEntity, IEntityAuditable, IEntitySoftDeletable +{ + /// + /// Name. + /// + public virtual string Name { get; set; } = null!; + + /// + /// Navigation Id. + /// + public virtual Guid? NavigationId { get; set; } + + /// + /// Navigation. + /// + [Include] + public virtual ExampleNavigation? Navigation { get; set; } +} \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/ExampleNavigation.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/ExampleNavigation.cs new file mode 100644 index 00000000..80fd08ef --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service.Models/ExampleNavigation.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Nano.Data.Abstractions.Models; +using Nano.Data.Abstractions.Models.Abstractions; + +namespace Api.ApiClients.Audit.Service.Models; + +/// +/// Example Navigation. +/// +public class ExampleNavigation : BaseEntity, IEntityAuditable, IEntitySoftDeletable +{ + /// + /// Navigation Name. + /// + public virtual string NavigationName { get; set; } = null!; + + /// + /// Examples. + /// + public virtual ICollection Examples { get; set; } = []; +} \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Api.ApiClients.Audit.Service.csproj b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Api.ApiClients.Audit.Service.csproj new file mode 100644 index 00000000..ffe64f93 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Api.ApiClients.Audit.Service.csproj @@ -0,0 +1,37 @@ + + + + net10.0 + + 10.0.0.0 + 10.0.0.0 + 10.0.0.0 + AnyCPU + Debug;Release + latest + en-US + enable + LICENSE + true + + false + true + false + true + Linux + ..\docker-compose.dcproj + + + + True + + + + + + + + + + + \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Controllers/AuditController.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Controllers/AuditController.cs new file mode 100644 index 00000000..bd90f558 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Controllers/AuditController.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.Logging; +using Nano.App.Api.Controllers; +using Nano.Data.Abstractions; + +namespace Api.ApiClients.Audit.Service.Controllers; + +/// +public class AuditController(ILogger logger, IRepository repository) + : BaseAuditController(logger, repository); \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Controllers/ExamplesController.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Controllers/ExamplesController.cs new file mode 100644 index 00000000..e5e8c595 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Controllers/ExamplesController.cs @@ -0,0 +1,15 @@ +using Api.ApiClients.Audit.Service.Models; +using Api.ApiClients.Audit.Service.Models.Criterias; +using Microsoft.Extensions.Logging; +using Nano.App.Api.Controllers; +using Nano.Data.Abstractions; + +namespace Api.ApiClients.Audit.Service.Controllers; + +/// +/// Controller with examples. +/// +/// The . +/// The . +public class ExamplesController(ILogger logger, IRepository repository) + : BaseEntityController(logger, repository); \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Data/Mappings/ExampleMapping.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Data/Mappings/ExampleMapping.cs new file mode 100644 index 00000000..8b61ff28 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Data/Mappings/ExampleMapping.cs @@ -0,0 +1,34 @@ +using System; +using Api.ApiClients.Audit.Service.Models; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Nano.Data.Mappings; +using Nano.Data.Mappings.Extensions; + +namespace Api.ApiClients.Audit.Service.Data.Mappings; + +/// +/// Example Mapping. +/// +public class ExampleMapping : BaseEntityMapping +{ + /// + public override void Configure(EntityTypeBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + base.Configure(builder); + + builder + .Property(x => x.Name); + + builder + .HasOne(x => x.Navigation) + .WithMany(x => x.Examples); + + builder + .OnUpdating(x => + { + x.Entity.Name += "-triggered"; + }); + } +} \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Data/Mappings/ExampleNavigationMapping.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Data/Mappings/ExampleNavigationMapping.cs new file mode 100644 index 00000000..f9269c03 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Data/Mappings/ExampleNavigationMapping.cs @@ -0,0 +1,27 @@ +using System; +using Api.ApiClients.Audit.Service.Models; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Nano.Data.Mappings; + +namespace Api.ApiClients.Audit.Service.Data.Mappings; + +/// +/// Example Navigation Mapping. +/// +public class ExampleNavigationMapping : BaseEntityMapping +{ + /// + public override void Configure(EntityTypeBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + base.Configure(builder); + + builder + .Property(x => x.NavigationName); + + builder + .HasMany(x => x.Examples) + .WithOne(x => x.Navigation); + } +} \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Data/MySqlDbContext.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Data/MySqlDbContext.cs new file mode 100644 index 00000000..3dfa9c4a --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Data/MySqlDbContext.cs @@ -0,0 +1,10 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Nano.Data; +using Nano.Data.Abstractions.Config; + +namespace Api.ApiClients.Audit.Service.Data; + +/// +public class MySqlDbContext(DbContextOptions contextOptions, IOptionsMonitor dataOptions) + : BaseDbContext(contextOptions, dataOptions); \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Data/MySqlDbContextFactory.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Data/MySqlDbContextFactory.cs new file mode 100644 index 00000000..4850809c --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Data/MySqlDbContextFactory.cs @@ -0,0 +1,7 @@ +using Nano.Data; +using Nano.Data.MySql; + +namespace Api.ApiClients.Audit.Service.Data; + +/// +public class MySqlDbContextFactory : BaseDbContextFactory; \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Dockerfile.Local b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Dockerfile.Local new file mode 100644 index 00000000..1e6d12c6 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Dockerfile.Local @@ -0,0 +1,6 @@ +ARG DOTNET_ASPNET_VERSION="10.0" +FROM mcr.microsoft.com/dotnet/aspnet:$DOTNET_ASPNET_VERSION AS base + +EXPOSE 8080 + +ENTRYPOINT ["dotnet", "Api.ApiClients.Audit.Service.dll"] \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Migrations/20260415133755_Initial.Designer.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Migrations/20260415133755_Initial.Designer.cs new file mode 100644 index 00000000..077c87f2 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Migrations/20260415133755_Initial.Designer.cs @@ -0,0 +1,728 @@ +// +using System; +using Api.ApiClients.Audit.Service.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Api.Data.Audit.Migrations +{ + [DbContext(typeof(MySqlDbContext))] + [Migration("20260415133755_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Api.Data.Audit.Models.Example", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CreatedAt")); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(0L); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NavigationId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("NavigationId"); + + b.ToTable("Example"); + }); + + modelBuilder.Entity("Api.Data.Audit.Models.ExampleNavigation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CreatedAt")); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(0L); + + b.Property("NavigationName") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsDeleted"); + + b.ToTable("ExampleNavigation"); + }); + + modelBuilder.Entity("Api.Data.Audit.Models.ExampleNoAudit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CreatedAt")); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(0L); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsDeleted"); + + b.ToTable("ExampleNoAudit"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("longtext"); + + b.Property("Xml") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("__EFDataProtectionKeys", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("__EFIdentityRole", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("__EFIdentityRoleClaim", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("__EFIdentityUserClaim", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("__EFIdentityUserLogin", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("__EFIdentityUserRole", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("__EFIdentityUserToken", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.AuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EntityKey") + .HasColumnType("char(36)"); + + b.Property("EntitySetName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EntityState") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("EntityTypeName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsDeleted") + .HasColumnType("bigint"); + + b.Property("RequestId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("EntityKey"); + + b.HasIndex("EntityState"); + + b.HasIndex("EntityTypeName"); + + b.HasIndex("RequestId"); + + b.ToTable("__EFAudit", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.AuditEntryProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsDeleted") + .HasColumnType("bigint"); + + b.Property("NewValue") + .HasColumnType("longtext"); + + b.Property("OldValue") + .HasColumnType("longtext"); + + b.Property("ParentId") + .HasColumnType("char(36)"); + + b.Property("PropertyName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RelationName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("PropertyName"); + + b.ToTable("__EFAuditProperties", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IdentityUserId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("IdentityUserId"); + + b.HasIndex("RevokedAt"); + + b.ToTable("__EFIdentityApiKey", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApiKeyId") + .HasColumnType("char(36)"); + + b.Property("ClaimType") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId", "ClaimType") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType"); + + b.ToTable("__EFIdentityApiKeyClaim", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApiKeyId") + .HasColumnType("char(36)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("ApiKeyId", "RoleId") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyRole_ApiKeyId_RoleId"); + + b.ToTable("__EFIdentityApiKeyRole", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("IdentityUserId") + .HasColumnType("char(36)"); + + b.Property("NewEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NewPhoneNumber") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("IdentityUserId") + .IsUnique() + .HasDatabaseName("UX___EFIdentityUserChangeData_IdentityUserId"); + + b.ToTable("__EFIdentityUserChangeData", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("varchar(255)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IsActive"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PhoneNumber") + .IsUnique(); + + b.ToTable("__EFIdentityUser", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("IdentityUserId") + .HasColumnType("char(36)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("ExpireAt"); + + b.HasIndex("IdentityUserId", "AppId") + .IsUnique() + .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId_AppId"); + + b.ToTable("__EFIdentityUserRefreshToken", (string)null); + }); + + modelBuilder.Entity("Api.Data.Audit.Models.Example", b => + { + b.HasOne("Api.Data.Audit.Models.ExampleNavigation", "Navigation") + .WithMany("Examples") + .HasForeignKey("NavigationId"); + + b.Navigation("Navigation"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.AuditEntryProperty", b => + { + b.HasOne("Nano.Data.Abstractions.Models.AuditEntry", "Parent") + .WithMany("Properties") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") + .WithMany() + .HasForeignKey("IdentityUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityUser"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") + .WithOne() + .HasForeignKey("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", "IdentityUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityUser"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") + .WithMany() + .HasForeignKey("IdentityUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityUser"); + }); + + modelBuilder.Entity("Api.Data.Audit.Models.ExampleNavigation", b => + { + b.Navigation("Examples"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.AuditEntry", b => + { + b.Navigation("Properties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Migrations/20260415133755_Initial.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Migrations/20260415133755_Initial.cs new file mode 100644 index 00000000..dade123c --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Migrations/20260415133755_Initial.cs @@ -0,0 +1,667 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Api.Data.Audit.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFAudit", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + CreatedBy = table.Column(type: "varchar(256)", maxLength: 256, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + EntityKey = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + EntitySetName = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + EntityTypeName = table.Column(type: "varchar(256)", maxLength: 256, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + EntityState = table.Column(type: "int", nullable: false, defaultValue: 0), + RequestId = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + IsDeleted = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK___EFAudit", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFDataProtectionKeys", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + FriendlyName = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Xml = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK___EFDataProtectionKeys", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFIdentityRole", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Name = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + NormalizedName = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ConcurrencyStamp = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityRole", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFIdentityUser", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + IsActive = table.Column(type: "tinyint(1)", nullable: false, defaultValue: true), + UserName = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + NormalizedUserName = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Email = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + NormalizedEmail = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + EmailConfirmed = table.Column(type: "tinyint(1)", nullable: false), + PasswordHash = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + SecurityStamp = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ConcurrencyStamp = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + PhoneNumber = table.Column(type: "varchar(255)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + PhoneNumberConfirmed = table.Column(type: "tinyint(1)", nullable: false), + TwoFactorEnabled = table.Column(type: "tinyint(1)", nullable: false), + LockoutEnd = table.Column(type: "datetime(6)", nullable: true), + LockoutEnabled = table.Column(type: "tinyint(1)", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityUser", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "ExampleNavigation", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + NavigationName = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + IsDeleted = table.Column(type: "bigint", nullable: false, defaultValue: 0L), + CreatedAt = table.Column(type: "datetime(6)", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn) + }, + constraints: table => + { + table.PrimaryKey("PK_ExampleNavigation", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "ExampleNoAudit", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Name = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + IsDeleted = table.Column(type: "bigint", nullable: false, defaultValue: 0L), + CreatedAt = table.Column(type: "datetime(6)", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn) + }, + constraints: table => + { + table.PrimaryKey("PK_ExampleNoAudit", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFAuditProperties", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + ParentId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + PropertyName = table.Column(type: "varchar(256)", maxLength: 256, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + RelationName = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + NewValue = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + OldValue = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + IsDeleted = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK___EFAuditProperties", x => x.Id); + table.ForeignKey( + name: "FK___EFAuditProperties___EFAudit_ParentId", + column: x => x.ParentId, + principalTable: "__EFAudit", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFIdentityRoleClaim", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + RoleId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + ClaimType = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ClaimValue = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityRoleClaim", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityRoleClaim___EFIdentityRole_RoleId", + column: x => x.RoleId, + principalTable: "__EFIdentityRole", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFIdentityApiKey", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + IdentityUserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Name = table.Column(type: "varchar(256)", maxLength: 256, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Hash = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + RevokedAt = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityApiKey", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityApiKey___EFIdentityUser_IdentityUserId", + column: x => x.IdentityUserId, + principalTable: "__EFIdentityUser", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFIdentityUserChangeData", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + IdentityUserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + NewEmail = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + NewPhoneNumber = table.Column(type: "varchar(20)", maxLength: 20, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityUserChangeData", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityUserChangeData___EFIdentityUser_IdentityUserId", + column: x => x.IdentityUserId, + principalTable: "__EFIdentityUser", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFIdentityUserClaim", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + ClaimType = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ClaimValue = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityUserClaim", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityUserClaim___EFIdentityUser_UserId", + column: x => x.UserId, + principalTable: "__EFIdentityUser", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFIdentityUserLogin", + columns: table => new + { + LoginProvider = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ProviderKey = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ProviderDisplayName = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci") + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityUserLogin", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK___EFIdentityUserLogin___EFIdentityUser_UserId", + column: x => x.UserId, + principalTable: "__EFIdentityUser", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFIdentityUserRefreshToken", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + IdentityUserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + AppId = table.Column(type: "varchar(256)", maxLength: 256, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Value = table.Column(type: "varchar(256)", maxLength: 256, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ExpireAt = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityUserRefreshToken", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityUserRefreshToken___EFIdentityUser_IdentityUserId", + column: x => x.IdentityUserId, + principalTable: "__EFIdentityUser", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFIdentityUserRole", + columns: table => new + { + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + RoleId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci") + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityUserRole", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK___EFIdentityUserRole___EFIdentityRole_RoleId", + column: x => x.RoleId, + principalTable: "__EFIdentityRole", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK___EFIdentityUserRole___EFIdentityUser_UserId", + column: x => x.UserId, + principalTable: "__EFIdentityUser", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFIdentityUserToken", + columns: table => new + { + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + LoginProvider = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Name = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Value = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityUserToken", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK___EFIdentityUserToken___EFIdentityUser_UserId", + column: x => x.UserId, + principalTable: "__EFIdentityUser", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "Example", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Name = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + NavigationId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + IsDeleted = table.Column(type: "bigint", nullable: false, defaultValue: 0L), + CreatedAt = table.Column(type: "datetime(6)", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn) + }, + constraints: table => + { + table.PrimaryKey("PK_Example", x => x.Id); + table.ForeignKey( + name: "FK_Example_ExampleNavigation_NavigationId", + column: x => x.NavigationId, + principalTable: "ExampleNavigation", + principalColumn: "Id"); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFIdentityApiKeyClaim", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + ApiKeyId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + ClaimType = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ClaimValue = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityApiKeyClaim", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityApiKeyClaim___EFIdentityApiKey_ApiKeyId", + column: x => x.ApiKeyId, + principalTable: "__EFIdentityApiKey", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFIdentityApiKeyRole", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + ApiKeyId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + RoleId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci") + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityApiKeyRole", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityApiKeyRole___EFIdentityApiKey_ApiKeyId", + column: x => x.ApiKeyId, + principalTable: "__EFIdentityApiKey", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK___EFIdentityApiKeyRole___EFIdentityRole_RoleId", + column: x => x.RoleId, + principalTable: "__EFIdentityRole", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX___EFAudit_CreatedBy", + table: "__EFAudit", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX___EFAudit_EntityKey", + table: "__EFAudit", + column: "EntityKey"); + + migrationBuilder.CreateIndex( + name: "IX___EFAudit_EntityState", + table: "__EFAudit", + column: "EntityState"); + + migrationBuilder.CreateIndex( + name: "IX___EFAudit_EntityTypeName", + table: "__EFAudit", + column: "EntityTypeName"); + + migrationBuilder.CreateIndex( + name: "IX___EFAudit_RequestId", + table: "__EFAudit", + column: "RequestId"); + + migrationBuilder.CreateIndex( + name: "IX___EFAuditProperties_ParentId", + table: "__EFAuditProperties", + column: "ParentId"); + + migrationBuilder.CreateIndex( + name: "IX___EFAuditProperties_PropertyName", + table: "__EFAuditProperties", + column: "PropertyName"); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityApiKey_IdentityUserId", + table: "__EFIdentityApiKey", + column: "IdentityUserId"); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityApiKey_RevokedAt", + table: "__EFIdentityApiKey", + column: "RevokedAt"); + + migrationBuilder.CreateIndex( + name: "UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType", + table: "__EFIdentityApiKeyClaim", + columns: new[] { "ApiKeyId", "ClaimType" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityApiKeyRole_RoleId", + table: "__EFIdentityApiKeyRole", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "UX___EFIdentityApiKeyRole_ApiKeyId_RoleId", + table: "__EFIdentityApiKeyRole", + columns: new[] { "ApiKeyId", "RoleId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "__EFIdentityRole", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityRoleClaim_RoleId", + table: "__EFIdentityRoleClaim", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "__EFIdentityUser", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityUser_Email", + table: "__EFIdentityUser", + column: "Email", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityUser_IsActive", + table: "__EFIdentityUser", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityUser_PhoneNumber", + table: "__EFIdentityUser", + column: "PhoneNumber", + unique: true); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "__EFIdentityUser", + column: "NormalizedUserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "UX___EFIdentityUserChangeData_IdentityUserId", + table: "__EFIdentityUserChangeData", + column: "IdentityUserId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityUserClaim_UserId", + table: "__EFIdentityUserClaim", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityUserLogin_UserId", + table: "__EFIdentityUserLogin", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityUserRefreshToken_ExpireAt", + table: "__EFIdentityUserRefreshToken", + column: "ExpireAt"); + + migrationBuilder.CreateIndex( + name: "UX___EFIdentityUserRefreshToken_IdentityUserId_AppId", + table: "__EFIdentityUserRefreshToken", + columns: new[] { "IdentityUserId", "AppId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityUserRole_RoleId", + table: "__EFIdentityUserRole", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_Example_CreatedAt", + table: "Example", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_Example_IsDeleted", + table: "Example", + column: "IsDeleted"); + + migrationBuilder.CreateIndex( + name: "IX_Example_NavigationId", + table: "Example", + column: "NavigationId"); + + migrationBuilder.CreateIndex( + name: "IX_ExampleNavigation_CreatedAt", + table: "ExampleNavigation", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_ExampleNavigation_IsDeleted", + table: "ExampleNavigation", + column: "IsDeleted"); + + migrationBuilder.CreateIndex( + name: "IX_ExampleNoAudit_CreatedAt", + table: "ExampleNoAudit", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_ExampleNoAudit_IsDeleted", + table: "ExampleNoAudit", + column: "IsDeleted"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "__EFAuditProperties"); + + migrationBuilder.DropTable( + name: "__EFDataProtectionKeys"); + + migrationBuilder.DropTable( + name: "__EFIdentityApiKeyClaim"); + + migrationBuilder.DropTable( + name: "__EFIdentityApiKeyRole"); + + migrationBuilder.DropTable( + name: "__EFIdentityRoleClaim"); + + migrationBuilder.DropTable( + name: "__EFIdentityUserChangeData"); + + migrationBuilder.DropTable( + name: "__EFIdentityUserClaim"); + + migrationBuilder.DropTable( + name: "__EFIdentityUserLogin"); + + migrationBuilder.DropTable( + name: "__EFIdentityUserRefreshToken"); + + migrationBuilder.DropTable( + name: "__EFIdentityUserRole"); + + migrationBuilder.DropTable( + name: "__EFIdentityUserToken"); + + migrationBuilder.DropTable( + name: "Example"); + + migrationBuilder.DropTable( + name: "ExampleNoAudit"); + + migrationBuilder.DropTable( + name: "__EFAudit"); + + migrationBuilder.DropTable( + name: "__EFIdentityApiKey"); + + migrationBuilder.DropTable( + name: "__EFIdentityRole"); + + migrationBuilder.DropTable( + name: "ExampleNavigation"); + + migrationBuilder.DropTable( + name: "__EFIdentityUser"); + } + } +} diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Migrations/MySqlDbContextModelSnapshot.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Migrations/MySqlDbContextModelSnapshot.cs new file mode 100644 index 00000000..ee5255c2 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Migrations/MySqlDbContextModelSnapshot.cs @@ -0,0 +1,725 @@ +// +using System; +using Api.ApiClients.Audit.Service.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Api.Data.Audit.Migrations +{ + [DbContext(typeof(MySqlDbContext))] + partial class MySqlDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Api.Data.Audit.Models.Example", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CreatedAt")); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(0L); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NavigationId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("NavigationId"); + + b.ToTable("Example"); + }); + + modelBuilder.Entity("Api.Data.Audit.Models.ExampleNavigation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CreatedAt")); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(0L); + + b.Property("NavigationName") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsDeleted"); + + b.ToTable("ExampleNavigation"); + }); + + modelBuilder.Entity("Api.Data.Audit.Models.ExampleNoAudit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CreatedAt")); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(0L); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsDeleted"); + + b.ToTable("ExampleNoAudit"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("longtext"); + + b.Property("Xml") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("__EFDataProtectionKeys", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("__EFIdentityRole", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("__EFIdentityRoleClaim", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("__EFIdentityUserClaim", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("__EFIdentityUserLogin", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("__EFIdentityUserRole", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("__EFIdentityUserToken", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.AuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EntityKey") + .HasColumnType("char(36)"); + + b.Property("EntitySetName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EntityState") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("EntityTypeName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsDeleted") + .HasColumnType("bigint"); + + b.Property("RequestId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("EntityKey"); + + b.HasIndex("EntityState"); + + b.HasIndex("EntityTypeName"); + + b.HasIndex("RequestId"); + + b.ToTable("__EFAudit", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.AuditEntryProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsDeleted") + .HasColumnType("bigint"); + + b.Property("NewValue") + .HasColumnType("longtext"); + + b.Property("OldValue") + .HasColumnType("longtext"); + + b.Property("ParentId") + .HasColumnType("char(36)"); + + b.Property("PropertyName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RelationName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("PropertyName"); + + b.ToTable("__EFAuditProperties", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IdentityUserId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("IdentityUserId"); + + b.HasIndex("RevokedAt"); + + b.ToTable("__EFIdentityApiKey", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApiKeyId") + .HasColumnType("char(36)"); + + b.Property("ClaimType") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId", "ClaimType") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType"); + + b.ToTable("__EFIdentityApiKeyClaim", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApiKeyId") + .HasColumnType("char(36)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("ApiKeyId", "RoleId") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyRole_ApiKeyId_RoleId"); + + b.ToTable("__EFIdentityApiKeyRole", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("IdentityUserId") + .HasColumnType("char(36)"); + + b.Property("NewEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NewPhoneNumber") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("IdentityUserId") + .IsUnique() + .HasDatabaseName("UX___EFIdentityUserChangeData_IdentityUserId"); + + b.ToTable("__EFIdentityUserChangeData", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("varchar(255)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IsActive"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PhoneNumber") + .IsUnique(); + + b.ToTable("__EFIdentityUser", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AppId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("IdentityUserId") + .HasColumnType("char(36)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("ExpireAt"); + + b.HasIndex("IdentityUserId", "AppId") + .IsUnique() + .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId_AppId"); + + b.ToTable("__EFIdentityUserRefreshToken", (string)null); + }); + + modelBuilder.Entity("Api.Data.Audit.Models.Example", b => + { + b.HasOne("Api.Data.Audit.Models.ExampleNavigation", "Navigation") + .WithMany("Examples") + .HasForeignKey("NavigationId"); + + b.Navigation("Navigation"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.AuditEntryProperty", b => + { + b.HasOne("Nano.Data.Abstractions.Models.AuditEntry", "Parent") + .WithMany("Properties") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") + .WithMany() + .HasForeignKey("IdentityUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityUser"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") + .WithOne() + .HasForeignKey("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", "IdentityUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityUser"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") + .WithMany() + .HasForeignKey("IdentityUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityUser"); + }); + + modelBuilder.Entity("Api.Data.Audit.Models.ExampleNavigation", b => + { + b.Navigation("Examples"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.AuditEntry", b => + { + b.Navigation("Properties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Program.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Program.cs new file mode 100644 index 00000000..cd141ffd --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Program.cs @@ -0,0 +1,13 @@ +using Api.ApiClients.Audit.Service.Data; +using Nano.App.Api; +using Nano.Data.Extensions; +using Nano.Data.MySql; + +NanoApiApplication + .ConfigureApp() + .ConfigureServices(x => + { + x.AddNanoData(); + }) + .Build() + .Run(); diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Properties/InternalsVisibleTo.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Properties/InternalsVisibleTo.cs new file mode 100644 index 00000000..bc18519e --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/Properties/InternalsVisibleTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Tests.Api.ApiClients.Audit")] \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/appsettings.Development.json b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/appsettings.Development.json new file mode 100644 index 00000000..34379ce9 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/appsettings.Development.json @@ -0,0 +1,6 @@ +{ + "Data": { + "StartupAction": "Migrate", + "ConnectionString": "Server=host.docker.internal;Database=nanoDb;Uid=sa;Pwd=myPassword_123" + } +} \ No newline at end of file diff --git a/Api.Documentation.Nonce/Api.Documentation.Nonce/appsettings.Production.json b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/appsettings.Production.json similarity index 100% rename from Api.Documentation.Nonce/Api.Documentation.Nonce/appsettings.Production.json rename to Api.ApiClients.Audit/Api.ApiClients.Audit.Service/appsettings.Production.json diff --git a/Api.Documentation.Nonce/Api.Documentation.Nonce/appsettings.Staging.json b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/appsettings.Staging.json similarity index 100% rename from Api.Documentation.Nonce/Api.Documentation.Nonce/appsettings.Staging.json rename to Api.ApiClients.Audit/Api.ApiClients.Audit.Service/appsettings.Staging.json diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/appsettings.json b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/appsettings.json new file mode 100644 index 00000000..dbd68122 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.Service/appsettings.json @@ -0,0 +1,16 @@ +{ + "App": { + "Version": "1.0.0.0", + "Hosting": { + "Root": "api", + "Http": { + "Ports": [ + 8181 + ] + } + } + }, + "Data": { + "ConnectionString": null + } +} \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit.sln b/Api.ApiClients.Audit/Api.ApiClients.Audit.sln new file mode 100644 index 00000000..62da1d0c --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit.sln @@ -0,0 +1,159 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.1.11312.151 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".solution", ".solution", "{F9091FFF-8BDF-4447-A4CC-19ACF2F783A3}" + ProjectSection(SolutionItems) = preProject + .dockerignore = .dockerignore + .gitignore = .gitignore + Dockerfile = Dockerfile + icon.png = icon.png + LICENSE = LICENSE + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".docker", ".docker", "{06253892-53F5-4EB1-8CBE-8558DBDD9B70}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A84-F3DE-4406-BE71-30EE6723E6D0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" + ProjectSection(SolutionItems) = preProject + .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml + .kubernetes\configmap.yaml = .kubernetes\configmap.yaml + .kubernetes\deployment.yaml = .kubernetes\deployment.yaml + .kubernetes\service.yaml = .kubernetes\service.yaml + EndProjectSection +EndProject +Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", ".docker\docker-compose.dcproj", "{557A0C48-DA6A-4D7C-8668-94F08A390F4B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nano", ".nano", "{3339F5B7-AA56-4192-B201-75B2620036B1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nano.App", "..\..\Nano.Library\Nano.App\Nano.App.csproj", "{50CBB29E-F271-4BD3-B176-6F68BB3294EE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{426FE279-89DF-4C2A-ABBB-729FDA4D893A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{5C605BF3-D5A3-4334-8AFA-4D45A6DC365E}" + ProjectSection(SolutionItems) = preProject + .github\config\slack.yml = .github\config\slack.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{F547FB13-43D4-4AAB-8629-B41FFF4A251F}" + ProjectSection(SolutionItems) = preProject + .github\workflows\build-and-deploy.yml = .github\workflows\build-and-deploy.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nano.Common", "..\..\Nano.Library\Nano.Common\Nano.Common.csproj", "{F1E2F880-3214-E0ED-03BF-4F78931C9C6E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nano.Data.Abstractions", "..\..\Nano.Library\Nano.Data.Abstractions\Nano.Data.Abstractions.csproj", "{023BB767-08ED-44DE-A6BF-9A2EDE8345B7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nano.Eventing.Abstractions", "..\..\Nano.Library\Nano.Eventing.Abstractions\Nano.Eventing.Abstractions.csproj", "{7F25283D-B5C5-8CA9-8722-E84B380905C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nano.Storage.Abstractions", "..\..\Nano.Library\Nano.Storage.Abstractions\Nano.Storage.Abstractions.csproj", "{F0FA24A3-0F7B-3962-C471-78992174D4B6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nano.App.Api", "..\..\Nano.Library\Nano.App.Api\Nano.App.Api.csproj", "{E55DCB7C-9082-3538-6288-5A6E5C8DE183}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.ApiClients.Audit.Models", "Api.ApiClients.Audit.Models\Api.ApiClients.Audit.Models.csproj", "{55CA3ADE-88B1-B763-9FAB-EE5D4F418EB8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.ApiClients.Audit", "Api.ApiClients.Audit\Api.ApiClients.Audit.csproj", "{77596F3E-C6F4-A01F-B4D9-9370C3ED3CBC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Api.ApiClients.Audit", ".tests\Tests.Api.ApiClients.Audit\Tests.Api.ApiClients.Audit.csproj", "{B008D867-12B4-7EB7-D707-1C1503E4151A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nano.Logging.Abstractions", "..\..\Nano.Library\Nano.Logging.Abstractions\Nano.Logging.Abstractions.csproj", "{6BE9DA86-B487-64ED-7BF6-08CA10A05A97}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nano.Data", "..\..\Nano.Library\Nano.Data\Nano.Data.csproj", "{A1CC2D8C-C323-51C2-2480-BFFD9DAD9F1F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nano.Data.MySql", "..\..\Nano.Library\Nano.Data.MySql\Nano.Data.MySql.csproj", "{E7DC90EE-C82A-5CC7-8F33-9EC2FEC36291}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.ApiClients.Audit.Service.Models", "Api.ApiClients.Audit.Service.Models\Api.ApiClients.Audit.Service.Models.csproj", "{72C75C0B-EACD-A113-B9A4-2600519A71E0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.ApiClients.Audit.Service", "Api.ApiClients.Audit.Service\Api.ApiClients.Audit.Service.csproj", "{37CEC825-166A-5BB7-9466-A75FFE32383F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {557A0C48-DA6A-4D7C-8668-94F08A390F4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {557A0C48-DA6A-4D7C-8668-94F08A390F4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {557A0C48-DA6A-4D7C-8668-94F08A390F4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50CBB29E-F271-4BD3-B176-6F68BB3294EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50CBB29E-F271-4BD3-B176-6F68BB3294EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50CBB29E-F271-4BD3-B176-6F68BB3294EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50CBB29E-F271-4BD3-B176-6F68BB3294EE}.Release|Any CPU.Build.0 = Release|Any CPU + {F1E2F880-3214-E0ED-03BF-4F78931C9C6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1E2F880-3214-E0ED-03BF-4F78931C9C6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1E2F880-3214-E0ED-03BF-4F78931C9C6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1E2F880-3214-E0ED-03BF-4F78931C9C6E}.Release|Any CPU.Build.0 = Release|Any CPU + {023BB767-08ED-44DE-A6BF-9A2EDE8345B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {023BB767-08ED-44DE-A6BF-9A2EDE8345B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {023BB767-08ED-44DE-A6BF-9A2EDE8345B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {023BB767-08ED-44DE-A6BF-9A2EDE8345B7}.Release|Any CPU.Build.0 = Release|Any CPU + {7F25283D-B5C5-8CA9-8722-E84B380905C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F25283D-B5C5-8CA9-8722-E84B380905C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F25283D-B5C5-8CA9-8722-E84B380905C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F25283D-B5C5-8CA9-8722-E84B380905C6}.Release|Any CPU.Build.0 = Release|Any CPU + {F0FA24A3-0F7B-3962-C471-78992174D4B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0FA24A3-0F7B-3962-C471-78992174D4B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0FA24A3-0F7B-3962-C471-78992174D4B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0FA24A3-0F7B-3962-C471-78992174D4B6}.Release|Any CPU.Build.0 = Release|Any CPU + {E55DCB7C-9082-3538-6288-5A6E5C8DE183}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E55DCB7C-9082-3538-6288-5A6E5C8DE183}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E55DCB7C-9082-3538-6288-5A6E5C8DE183}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E55DCB7C-9082-3538-6288-5A6E5C8DE183}.Release|Any CPU.Build.0 = Release|Any CPU + {55CA3ADE-88B1-B763-9FAB-EE5D4F418EB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55CA3ADE-88B1-B763-9FAB-EE5D4F418EB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55CA3ADE-88B1-B763-9FAB-EE5D4F418EB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55CA3ADE-88B1-B763-9FAB-EE5D4F418EB8}.Release|Any CPU.Build.0 = Release|Any CPU + {77596F3E-C6F4-A01F-B4D9-9370C3ED3CBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77596F3E-C6F4-A01F-B4D9-9370C3ED3CBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77596F3E-C6F4-A01F-B4D9-9370C3ED3CBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77596F3E-C6F4-A01F-B4D9-9370C3ED3CBC}.Release|Any CPU.Build.0 = Release|Any CPU + {B008D867-12B4-7EB7-D707-1C1503E4151A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B008D867-12B4-7EB7-D707-1C1503E4151A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B008D867-12B4-7EB7-D707-1C1503E4151A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B008D867-12B4-7EB7-D707-1C1503E4151A}.Release|Any CPU.Build.0 = Release|Any CPU + {6BE9DA86-B487-64ED-7BF6-08CA10A05A97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BE9DA86-B487-64ED-7BF6-08CA10A05A97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BE9DA86-B487-64ED-7BF6-08CA10A05A97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BE9DA86-B487-64ED-7BF6-08CA10A05A97}.Release|Any CPU.Build.0 = Release|Any CPU + {A1CC2D8C-C323-51C2-2480-BFFD9DAD9F1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1CC2D8C-C323-51C2-2480-BFFD9DAD9F1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1CC2D8C-C323-51C2-2480-BFFD9DAD9F1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1CC2D8C-C323-51C2-2480-BFFD9DAD9F1F}.Release|Any CPU.Build.0 = Release|Any CPU + {E7DC90EE-C82A-5CC7-8F33-9EC2FEC36291}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7DC90EE-C82A-5CC7-8F33-9EC2FEC36291}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7DC90EE-C82A-5CC7-8F33-9EC2FEC36291}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7DC90EE-C82A-5CC7-8F33-9EC2FEC36291}.Release|Any CPU.Build.0 = Release|Any CPU + {72C75C0B-EACD-A113-B9A4-2600519A71E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72C75C0B-EACD-A113-B9A4-2600519A71E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72C75C0B-EACD-A113-B9A4-2600519A71E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72C75C0B-EACD-A113-B9A4-2600519A71E0}.Release|Any CPU.Build.0 = Release|Any CPU + {37CEC825-166A-5BB7-9466-A75FFE32383F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37CEC825-166A-5BB7-9466-A75FFE32383F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37CEC825-166A-5BB7-9466-A75FFE32383F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37CEC825-166A-5BB7-9466-A75FFE32383F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {557A0C48-DA6A-4D7C-8668-94F08A390F4B} = {06253892-53F5-4EB1-8CBE-8558DBDD9B70} + {50CBB29E-F271-4BD3-B176-6F68BB3294EE} = {3339F5B7-AA56-4192-B201-75B2620036B1} + {5C605BF3-D5A3-4334-8AFA-4D45A6DC365E} = {426FE279-89DF-4C2A-ABBB-729FDA4D893A} + {F547FB13-43D4-4AAB-8629-B41FFF4A251F} = {426FE279-89DF-4C2A-ABBB-729FDA4D893A} + {F1E2F880-3214-E0ED-03BF-4F78931C9C6E} = {3339F5B7-AA56-4192-B201-75B2620036B1} + {023BB767-08ED-44DE-A6BF-9A2EDE8345B7} = {3339F5B7-AA56-4192-B201-75B2620036B1} + {7F25283D-B5C5-8CA9-8722-E84B380905C6} = {3339F5B7-AA56-4192-B201-75B2620036B1} + {F0FA24A3-0F7B-3962-C471-78992174D4B6} = {3339F5B7-AA56-4192-B201-75B2620036B1} + {E55DCB7C-9082-3538-6288-5A6E5C8DE183} = {3339F5B7-AA56-4192-B201-75B2620036B1} + {B008D867-12B4-7EB7-D707-1C1503E4151A} = {7E0D2A84-F3DE-4406-BE71-30EE6723E6D0} + {6BE9DA86-B487-64ED-7BF6-08CA10A05A97} = {3339F5B7-AA56-4192-B201-75B2620036B1} + {A1CC2D8C-C323-51C2-2480-BFFD9DAD9F1F} = {3339F5B7-AA56-4192-B201-75B2620036B1} + {E7DC90EE-C82A-5CC7-8F33-9EC2FEC36291} = {3339F5B7-AA56-4192-B201-75B2620036B1} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {72ED70A7-E1BB-4D06-B6FD-E092DA575FBE} + EndGlobalSection +EndGlobal diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit/Api.ApiClients.Audit.csproj b/Api.ApiClients.Audit/Api.ApiClients.Audit/Api.ApiClients.Audit.csproj new file mode 100644 index 00000000..73fac6b6 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit/Api.ApiClients.Audit.csproj @@ -0,0 +1,38 @@ + + + + net10.0 + + 10.0.0.0 + 10.0.0.0 + 10.0.0.0 + AnyCPU + Debug;Release + latest + en-US + enable + LICENSE + true + + false + true + false + true + Linux + ..\docker-compose.dcproj + + + + True + + + + + + + + + + + + \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit/Controllers/AuditController.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit/Controllers/AuditController.cs new file mode 100644 index 00000000..b651fe57 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit/Controllers/AuditController.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Api.ApiClients.Audit.Service.Models.ApiClient; +using Api.ApiClients.Audit.Service.Models.Criterias; +using DynamicExpression.Interfaces; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Nano.App.Api.Controllers; +using Nano.App.ApiClient.Requests; +using Nano.Common.Consts; +using Nano.Data.Abstractions.Models; + +namespace Api.ApiClients.Audit.Controllers; + +/// +/// Controller with audit. +/// +/// The . +/// The . +public class AuditController(ILogger logger, NanoApiClient nanoApiClient) + : BaseController(logger) +{ + private readonly NanoApiClient nanoApiClient = nanoApiClient ?? throw new ArgumentNullException(nameof(nanoApiClient)); + + /// + /// Gets all entities matching the specified query. + /// + /// The query used to filter entities. + /// Optional include depth for related entities. + /// The cancellation token. + /// A collection of entities matching the query. + /// Entities retrieved successfully. + /// Invalid query parameters. + /// Unauthorized access. + /// No entities found. + /// Internal server error. + [HttpGet] + [Route(ActionRoutes.INDEX)] + [Produces(HttpContentType.JSON)] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.Unauthorized)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public virtual async Task IndexAsync([FromQuery][Required] IQuery query, [FromQuery] int? includeDepth, CancellationToken cancellationToken = default) + { + var examples = await this.nanoApiClient.Audit + .IndexAsync(new IndexRequest + { + IncludeDepth = includeDepth + }, cancellationToken); + + return this.Ok(examples); + } + + /// + /// Gets a single entity by its identifier. + /// + /// The identifier of the entity. + /// Optional include depth for related entities. + /// The cancellation token. + /// The entity matching the identifier. + /// Entity retrieved successfully. + /// Invalid identifier. + /// Unauthorized access. + /// Entity not found. + /// Internal server error. + [HttpGet] + [Route(ActionRoutes.DETAILS)] + [Produces(HttpContentType.JSON)] + [ProducesResponseType(typeof(AuditEntry), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.Unauthorized)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public virtual async Task DetailsAsync([FromRoute][Required] Guid id, [FromQuery] int? includeDepth, CancellationToken cancellationToken = default) + { + var examples = await this.nanoApiClient.Audit + .DetailsAsync(new DetailsRequest + { + Id = id, + IncludeDepth = includeDepth + }, cancellationToken); + + return this.Ok(examples); + } + + /// + /// Gets multiple entities by their identifiers. + /// + /// The identifiers of the entities. + /// Optional include depth for related entities. + /// The cancellation token. + /// The entities matching the identifiers. + /// Entities retrieved successfully. + /// Invalid identifiers. + /// Unauthorized access. + /// No entities found. + /// Internal server error. + [HttpPost] + [Route(ActionRoutes.DETAILS_MANY)] + [Consumes(HttpContentType.JSON)] + [Produces(HttpContentType.JSON)] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.Unauthorized)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public virtual async Task DetailsManyPostAsync([FromBody][Required] Guid[] ids, [FromQuery] int? includeDepth, CancellationToken cancellationToken = default) + { + var examples = await this.nanoApiClient.Audit + .DetailsManyAsync(new DetailsManyRequest + { + Ids = ids, + IncludeDepth = includeDepth + }, cancellationToken); + + return this.Ok(examples); + } + + /// + /// Queries entities matching the specified criteria. + /// + /// The query model containing filters and criteria. + /// Optional include depth for related entities. + /// The cancellation token. + /// A collection of entities matching the criteria. + /// Entities retrieved successfully. + /// Invalid query parameters. + /// Unauthorized access. + /// No entities found. + /// Internal server error. + [HttpPost] + [Route(ActionRoutes.QUERY)] + [Consumes(HttpContentType.JSON)] + [Produces(HttpContentType.JSON)] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.Unauthorized)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public virtual async Task QueryPostAsync([FromBody][Required] IQuery query, [FromQuery] int? includeDepth, CancellationToken cancellationToken = default) + { + var examples = await this.nanoApiClient.Audit + .QueryAsync(new QueryRequest + { + Query = query, + IncludeDepth = includeDepth + }, cancellationToken); + + return this.Ok(examples); + } + + /// + /// Retrieves the first entity matching the specified criteria. + /// + /// The query model containing filters and criteria. + /// Optional include depth for related entities. + /// The cancellation token. + /// The first entity matching the criteria. + /// Entity retrieved successfully. + /// Invalid query parameters. + /// Unauthorized access. + /// No entity found. + /// Internal server error. + [HttpPost] + [Route(ActionRoutes.QUERY_FIRST)] + [Consumes(HttpContentType.JSON)] + [Produces(HttpContentType.JSON)] + [ProducesResponseType(typeof(AuditEntry), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.Unauthorized)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public virtual async Task QueryFirstPostAsync([FromBody][Required] IQuery query, [FromQuery] int? includeDepth, CancellationToken cancellationToken = default) + { + var example = await this.nanoApiClient.Audit + .QueryFirstAsync(new QueryFirstRequest + { + Query = query, + IncludeDepth = includeDepth + }, cancellationToken); + + return this.Ok(example); + } + + /// + /// Gets the total count of entities matching the specified criteria. + /// + /// The criteria model containing filters. + /// The cancellation token. + /// The number of entities matching the criteria. + /// Count retrieved successfully. + /// Invalid criteria parameters. + /// Unauthorized access. + /// No entities found. + /// Internal server error. + [HttpPost] + [Route(ActionRoutes.QUERY_COUNT)] + [Consumes(HttpContentType.JSON)] + [Produces(HttpContentType.JSON)] + [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.Unauthorized)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public virtual async Task QueryCountPostAsync([FromBody][Required] ExampleQueryCriteria criteria, CancellationToken cancellationToken = default) + { + var count = await this.nanoApiClient.Audit + .QueryCountAsync(new QueryCountRequest + { + Criteria = criteria + }, cancellationToken); + + return this.Ok(count); + } +} \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit/Controllers/ExamplesController.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit/Controllers/ExamplesController.cs new file mode 100644 index 00000000..2b0ad6e8 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit/Controllers/ExamplesController.cs @@ -0,0 +1,53 @@ +using Api.ApiClients.Audit.Service.Models.ApiClient; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Nano.App.Api.Controllers; +using Nano.App.ApiClient.Requests; +using Nano.Common.Consts; +using System; +using System.ComponentModel.DataAnnotations; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Api.ApiClients.Audit.Service.Models; + +namespace Api.ApiClients.Audit.Controllers; + +/// +/// Controller with examples. +/// +/// The . +/// The . +public class ExamplesController(ILogger logger, NanoApiClient nanoApiClient) : BaseController(logger) +{ + private readonly NanoApiClient nanoApiClient = nanoApiClient ?? throw new ArgumentNullException(nameof(nanoApiClient)); + + /// + /// Creates a single model instance. + /// + /// The entity to create. + /// Cancellation token. + /// The created entity. + /// Entity created. + /// Bad request. + /// Unauthorized. + /// Internal server error. + [HttpPost] + [Route(ActionRoutes.CREATE)] + [Consumes(HttpContentType.JSON)] + [Produces(HttpContentType.JSON)] + [ProducesResponseType(typeof(Example), (int)HttpStatusCode.Created)] + [ProducesResponseType((int)HttpStatusCode.Unauthorized)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public virtual async Task CreateAsync([FromBody][Required] Example entity, CancellationToken cancellationToken = default) + { + var example = await this.nanoApiClient.Entity + .CreateAsync(new CreateRequest + { + Entity = entity + }, cancellationToken); + + return this.Created(ActionRoutes.CREATE, example); + } +} \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit/Dockerfile.Local b/Api.ApiClients.Audit/Api.ApiClients.Audit/Dockerfile.Local new file mode 100644 index 00000000..f55f6733 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit/Dockerfile.Local @@ -0,0 +1,6 @@ +ARG DOTNET_ASPNET_VERSION="10.0" +FROM mcr.microsoft.com/dotnet/aspnet:$DOTNET_ASPNET_VERSION AS base + +EXPOSE 8080 + +ENTRYPOINT ["dotnet", "Api.ApiClients.Audit.dll"] \ No newline at end of file diff --git a/Api.Documentation.Nonce/Api.Documentation.Nonce/Program.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit/Program.cs similarity index 100% rename from Api.Documentation.Nonce/Api.Documentation.Nonce/Program.cs rename to Api.ApiClients.Audit/Api.ApiClients.Audit/Program.cs diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit/Properties/InternalsVisibleTo.cs b/Api.ApiClients.Audit/Api.ApiClients.Audit/Properties/InternalsVisibleTo.cs new file mode 100644 index 00000000..bc18519e --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit/Properties/InternalsVisibleTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Tests.Api.ApiClients.Audit")] \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit/appsettings.Development.json b/Api.ApiClients.Audit/Api.ApiClients.Audit/appsettings.Development.json new file mode 100644 index 00000000..8593c62d --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit/appsettings.Development.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit/appsettings.Production.json b/Api.ApiClients.Audit/Api.ApiClients.Audit/appsettings.Production.json new file mode 100644 index 00000000..8593c62d --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit/appsettings.Production.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit/appsettings.Staging.json b/Api.ApiClients.Audit/Api.ApiClients.Audit/appsettings.Staging.json new file mode 100644 index 00000000..8593c62d --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit/appsettings.Staging.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/Api.ApiClients.Audit/Api.ApiClients.Audit/appsettings.json b/Api.ApiClients.Audit/Api.ApiClients.Audit/appsettings.json new file mode 100644 index 00000000..49779fd9 --- /dev/null +++ b/Api.ApiClients.Audit/Api.ApiClients.Audit/appsettings.json @@ -0,0 +1,26 @@ +{ + "App": { + "Version": "1.0.0.0", + "Hosting": { + "Root": "api", + "Http": { + "Ports": [ + 8080 + ] + } + }, + "HealthCheck": { }, + "Apis": { + "NanoApiClient": { + "Host": "api.apiclients.audit.service", + "Root": "api", + "Port": 8181, + "UseSsl": false, + "Timeout": "00:00:30", + "HealthCheck": { + "UnhealthyStatus": "Unhealthy" + } + } + } + } +} \ No newline at end of file diff --git a/Api.Documentation.Nonce/Dockerfile b/Api.ApiClients.Audit/Dockerfile similarity index 93% rename from Api.Documentation.Nonce/Dockerfile rename to Api.ApiClients.Audit/Dockerfile index c640d06a..ae5aa0b8 100644 --- a/Api.Documentation.Nonce/Dockerfile +++ b/Api.ApiClients.Audit/Dockerfile @@ -32,4 +32,4 @@ COPY --from=publish /app . ENV COMPlus_EnableDiagnostics=0 ENV DOTNET_USE_POLLING_FILE_WATCHER=true -ENTRYPOINT ["dotnet", "Api.Documentation.Nonce.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "Api.ApiClients.Audit.dll"] \ No newline at end of file diff --git a/Api.Documentation.Nonce/LICENSE b/Api.ApiClients.Audit/LICENSE similarity index 100% rename from Api.Documentation.Nonce/LICENSE rename to Api.ApiClients.Audit/LICENSE diff --git a/Api.ApiClients.Audit/README.md b/Api.ApiClients.Audit/README.md new file mode 100644 index 00000000..4b634acf --- /dev/null +++ b/Api.ApiClients.Audit/README.md @@ -0,0 +1,42 @@ +# Api.ApiClients.Audit + +> _Nano API application with api-client audit._ +_All lessons are complete, self-contained examples that include build and deployment setup._ + +> ⚠️ _To run this solution, the **[Nano.Library](https://github.com/Nano-Core/Nano.Library)** repository must be checked out in the same root directory. +Nano is referenced directly from source (not via NuGet packages) and is expected to be located in the .nano solution folder._ + +> ⚠️ Remember to set the docker-compose project as startup project, before running the solution in Visual Studio. + +> 💡 Explore API requests for this lesson in our **[Public Nano Workspace on Postman](https://www.postman.com/nanocore/nano-lessons)**. + +*** + +## Table of Contents +* [Summary](#summary) + +## Summary +This application builds on **[Api.ApiClients](https://github.com/Nano-Core/Nano.Lessons/tree/master/Api.ApiClients)**. All the custom methods have been removed and replaced +with corresponding methods for each audit controller operation in the inner service. + +The inner application has a data provider enabled to demonstrate the generic API client integration with Nano entity models. A `NanoApiClient` implementation, derived from +`BaseApiClient`, has been added to the service application. This lesson showcases how to use the `Audit` sub-group of the `BaseApiClient` to interact with any audit data +exposed by the inner application. + +The endpoints mirror those of the `AuditController` in the inner service, allowing each entity action to be invoked through the API client for demonstration purposes. In a +real-world scenario, this structure would typically differ. The outer application would define its own request and response contracts tailored to its domain. However, for +simplicity and clarity in this example, the responses from the inner service are passed directly through the outer API. + +The following endpoint is available for testing. + +| Endpoint | Description | +| -------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `http://localhost:8080/api/examples/create` | Returns a `200 OK` response. Creates an `Example` with nested `ExampleNavigation` which is audited. | + +> 📖 Learn more about **[Nano Api Clients](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App/README.md#api-clients)**. + + + + + + diff --git a/Api.Documentation.Nonce/icon.png b/Api.ApiClients.Audit/icon.png similarity index 100% rename from Api.Documentation.Nonce/icon.png rename to Api.ApiClients.Audit/icon.png diff --git a/Api.ApiClients.Entity/.github/workflows/build-and-deploy.yml b/Api.ApiClients.Entity/.github/workflows/build-and-deploy.yml index 62afcad6..bec796fc 100644 --- a/Api.ApiClients.Entity/.github/workflows/build-and-deploy.yml +++ b/Api.ApiClients.Entity/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.ApiClients.Entity IMAGE_NAME: api.apiclients.entity @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.ApiClients.Entity/.kubernetes/deployment.yaml b/Api.ApiClients.Entity/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.ApiClients.Entity/.kubernetes/deployment.yaml +++ b/Api.ApiClients.Entity/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.ApiClients.Entity/README.md b/Api.ApiClients.Entity/README.md index eae12a82..e75c3c09 100644 --- a/Api.ApiClients.Entity/README.md +++ b/Api.ApiClients.Entity/README.md @@ -27,4 +27,4 @@ The endpoints mirror those of the entity controller in the inner service, allowi real-world scenario, this structure would typically differ. The outer application would define its own request and response contracts tailored to its domain. However, for simplicity and clarity in this example, the responses from the inner service are passed directly through the outer API. -> 📖 Learn more about **[Nano Api Clients](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App#api-clients)**. +> 📖 Learn more about **[Nano Api Clients](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App/README.md#api-clients)**. diff --git a/Api.ApiClients.RootLogIn/.github/workflows/build-and-deploy.yml b/Api.ApiClients.RootLogIn/.github/workflows/build-and-deploy.yml index ba5be9b0..1fa27e27 100644 --- a/Api.ApiClients.RootLogIn/.github/workflows/build-and-deploy.yml +++ b/Api.ApiClients.RootLogIn/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.ApiClients.RootLogIn IMAGE_NAME: api.apiclients.rootlogin @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.ApiClients.RootLogIn/.kubernetes/deployment.yaml b/Api.ApiClients.RootLogIn/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.ApiClients.RootLogIn/.kubernetes/deployment.yaml +++ b/Api.ApiClients.RootLogIn/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.ApiClients.RootLogIn/README.md b/Api.ApiClients.RootLogIn/README.md index fb33d5c9..e6cc353d 100644 --- a/Api.ApiClients.RootLogIn/README.md +++ b/Api.ApiClients.RootLogIn/README.md @@ -24,8 +24,8 @@ API client, the automatic root login mechanism is triggered. The following endpoint is available for testing. -| Endpoint | Description | -| ------------------------------------------------------------ | -------------------------------------- | +| Endpoint | Description | +| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | | `http://localhost:8080/api/examples/auto-authenticate-root` | Returns a `200 OK` response. Uses the API client’s automatic root login to obtain and return an access token. | -> 📖 Learn more about **[Nano Api Clients](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App#api-clients)**. +> 📖 Learn more about **[Nano Api Clients](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App/README.md#api-clients)**. diff --git a/Api.ApiClients/.github/workflows/build-and-deploy.yml b/Api.ApiClients/.github/workflows/build-and-deploy.yml index b51947ec..f97c3744 100644 --- a/Api.ApiClients/.github/workflows/build-and-deploy.yml +++ b/Api.ApiClients/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.ApiClients IMAGE_NAME: api.apiclients @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.ApiClients/.kubernetes/deployment.yaml b/Api.ApiClients/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.ApiClients/.kubernetes/deployment.yaml +++ b/Api.ApiClients/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.ApiClients/README.md b/Api.ApiClients/README.md index dd590d79..6e3af283 100644 --- a/Api.ApiClients/README.md +++ b/Api.ApiClients/README.md @@ -28,7 +28,7 @@ the outer application has been configured to include the API client, enabling it A health check is configured to target the application of the api-client. Open **[http://localhost:8080/healthz](http://localhost:8080/healthz)** to view the health-check status in the JSON response. -> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#health-checks)**. +> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#health-checks)**. The following endpoint is available for testing. @@ -41,7 +41,7 @@ The following endpoint is available for testing. | `http://localhost:8080/api/examples/problem-details-exception` | Returns a `417 Expectation Failed` response. A `ProblemDetailsException` is thrown to demonstrate structured error handling using Problem Details. | | `http://localhost:8080/api/examples/request-tracing` | Returns a `200 OK` response. The `X-Request-Id` header is extracted from the request and returned in the response for traceability purposes. | -> 📖 Learn more about **[Nano Api Clients](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App#api-clients)**. +> 📖 Learn more about **[Nano Api Clients](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App/README.md#api-clients)**. ## Configuration Configured the application with a connection to the `NanoApiClient`. diff --git a/Api.Auth.External.Custom/.github/workflows/build-and-deploy.yml b/Api.Auth.External.Custom/.github/workflows/build-and-deploy.yml index fe2c756a..1b1ae5fa 100644 --- a/Api.Auth.External.Custom/.github/workflows/build-and-deploy.yml +++ b/Api.Auth.External.Custom/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Auth.External.Custom IMAGE_NAME: api.auth.external.custom @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -38,23 +41,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -83,7 +91,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -99,9 +107,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -112,7 +120,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -121,35 +129,36 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic auth-jwt-secret --from-literal=jwt-public-key=$env:AUTH_JWT_PUBLIC_KEY --from-literal=jwt-private-key=$env:AUTH_JWT_PRIVATE_KEY --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + Get-Content .kubernetes/auth-jwt-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-jwt-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-jwt-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Auth.External.Custom/.kubernetes/auth-jwt-secret.yaml b/Api.Auth.External.Custom/.kubernetes/auth-jwt-secret.yaml new file mode 100644 index 00000000..3898973f --- /dev/null +++ b/Api.Auth.External.Custom/.kubernetes/auth-jwt-secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: auth-jwt-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + jwt-public-key: %AUTH_JWT_PUBLIC_KEY% + jwt-private-key: %AUTH_JWT_PRIVATE_KEY% + diff --git a/Api.Auth.External.Custom/.kubernetes/deployment.yaml b/Api.Auth.External.Custom/.kubernetes/deployment.yaml index 5041e227..aca5889f 100644 --- a/Api.Auth.External.Custom/.kubernetes/deployment.yaml +++ b/Api.Auth.External.Custom/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -84,11 +84,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Auth.External.Custom/Api.Auth.External.Custom.sln b/Api.Auth.External.Custom/Api.Auth.External.Custom.sln index 8b7813f6..94dae08c 100644 --- a/Api.Auth.External.Custom/Api.Auth.External.Custom.sln +++ b/Api.Auth.External.Custom/Api.Auth.External.Custom.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-jwt-secret.yaml = .kubernetes\auth-jwt-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Auth.External.Custom/Api.Auth.External.Custom/Authentication/ExternalProviderCustomRepository.cs b/Api.Auth.External.Custom/Api.Auth.External.Custom/Authentication/ExternalProviderCustomRepository.cs index 32b5f849..0cefcb5d 100644 --- a/Api.Auth.External.Custom/Api.Auth.External.Custom/Authentication/ExternalProviderCustomRepository.cs +++ b/Api.Auth.External.Custom/Api.Auth.External.Custom/Authentication/ExternalProviderCustomRepository.cs @@ -20,7 +20,7 @@ public override async Task AuthenticateAsync(Implici EmailAddress = "johndoe@domain.com", PhoneNumber = "+4520111112", Name = "John Doe", - ExternalToken = + ExternalToken = new ExternalAuthenticationToken { Name = this.ProviderName, Token = "token", diff --git a/Api.Auth.External.Custom/README.md b/Api.Auth.External.Custom/README.md index 2a462bbe..dc8fc4a4 100644 --- a/Api.Auth.External.Custom/README.md +++ b/Api.Auth.External.Custom/README.md @@ -31,7 +31,7 @@ provider in Nano. API documentation has been configured to make it easier to explore the available actions in the `AuthController`. Any actions that are not enabled due to omitted configuration are automatically excluded. The API documentation is available at: **http://localhost:8080/docs**. -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. The following endpoint from the auth controller is available for testing. @@ -46,7 +46,7 @@ Additionally, the following endpoint is available for testing authorization. | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `http://localhost:8080/api/examples/authenticate` | Returns a simple `200 OK` response, when JWT authorization is successful, and otherwise a `401 Unauthorized`. | -> 📖 Learn more about **[Nano Authentication](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#authentication)**. +> 📖 Learn more about **[Nano Authentication](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#authentication)**. ## Configuration Configured the application with the necessary authentication setup. @@ -114,11 +114,3 @@ env: ``` ...and created during the Kubernetes deploy step. - -```yaml -sudo kubectl create secret generic auth-jwt-secret --from-literal=jwt-public-key=$env:AUTH_JWT_PUBLIC_KEY --from-literal=jwt-private-key=$env:AUTH_JWT_PRIVATE_KEY --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; -if ($LastExitCode -ne 0) -{ - throw "error"; -}; -``` diff --git a/Api.Auth.RootLogin/.github/workflows/build-and-deploy.yml b/Api.Auth.RootLogin/.github/workflows/build-and-deploy.yml index bce63945..b33ac0de 100644 --- a/Api.Auth.RootLogin/.github/workflows/build-and-deploy.yml +++ b/Api.Auth.RootLogin/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Auth.RootLogin IMAGE_NAME: api.auth.rootlogin @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -38,23 +41,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -83,7 +91,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -99,9 +107,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -112,7 +120,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -121,35 +129,36 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic auth-jwt-secret --from-literal=jwt-public-key=$env:AUTH_JWT_PUBLIC_KEY --from-literal=jwt-private-key=$env:AUTH_JWT_PRIVATE_KEY --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + Get-Content .kubernetes/auth-jwt-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-jwt-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-jwt-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Auth.RootLogin/.kubernetes/auth-jwt-secret.yaml b/Api.Auth.RootLogin/.kubernetes/auth-jwt-secret.yaml new file mode 100644 index 00000000..3898973f --- /dev/null +++ b/Api.Auth.RootLogin/.kubernetes/auth-jwt-secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: auth-jwt-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + jwt-public-key: %AUTH_JWT_PUBLIC_KEY% + jwt-private-key: %AUTH_JWT_PRIVATE_KEY% + diff --git a/Api.Auth.RootLogin/.kubernetes/deployment.yaml b/Api.Auth.RootLogin/.kubernetes/deployment.yaml index 5041e227..aca5889f 100644 --- a/Api.Auth.RootLogin/.kubernetes/deployment.yaml +++ b/Api.Auth.RootLogin/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -84,11 +84,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Auth.RootLogin/Api.Auth.RootLogin.sln b/Api.Auth.RootLogin/Api.Auth.RootLogin.sln index 74873446..6bfe46a0 100644 --- a/Api.Auth.RootLogin/Api.Auth.RootLogin.sln +++ b/Api.Auth.RootLogin/Api.Auth.RootLogin.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-jwt-secret.yaml = .kubernetes\auth-jwt-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Auth.RootLogin/README.md b/Api.Auth.RootLogin/README.md index d750879d..a7c64602 100644 --- a/Api.Auth.RootLogin/README.md +++ b/Api.Auth.RootLogin/README.md @@ -28,7 +28,7 @@ the example controller endpoint. API documentation has been configured to make it easier to explore the available actions in the `AuthController`. Any actions that are not enabled due to omitted configuration are automatically excluded. The API documentation is available at: **http://localhost:8080/docs**. -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. The following endpoint from the auth controller is available for testing. @@ -42,7 +42,7 @@ Additionally, the following endpoint is available for testing authorization. | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `http://localhost:8080/api/examples/authenticate` | Returns a simple `200 OK` response, when JWT authorization is successful, and otherwise a `401 Unauthorized`. | -> 📖 Learn more about **[Nano Authentication](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#authentication)**. +> 📖 Learn more about **[Nano Authentication](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#authentication)**. ## Configuration Configured the application with the necessary authentication setup. diff --git a/Api.Authorization/.github/workflows/build-and-deploy.yml b/Api.Authorization/.github/workflows/build-and-deploy.yml index 91d8c254..27a37e6b 100644 --- a/Api.Authorization/.github/workflows/build-and-deploy.yml +++ b/Api.Authorization/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Authorization IMAGE_NAME: api.authorization @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -38,23 +41,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -83,7 +91,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -99,9 +107,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -112,7 +120,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -121,35 +129,36 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic auth-jwt-secret --from-literal=jwt-public-key=$env:AUTH_JWT_PUBLIC_KEY --from-literal=jwt-private-key=$env:AUTH_JWT_PRIVATE_KEY --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + Get-Content .kubernetes/auth-jwt-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-jwt-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-jwt-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Authorization/.kubernetes/auth-jwt-secret.yaml b/Api.Authorization/.kubernetes/auth-jwt-secret.yaml new file mode 100644 index 00000000..3898973f --- /dev/null +++ b/Api.Authorization/.kubernetes/auth-jwt-secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: auth-jwt-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + jwt-public-key: %AUTH_JWT_PUBLIC_KEY% + jwt-private-key: %AUTH_JWT_PRIVATE_KEY% + diff --git a/Api.Authorization/.kubernetes/deployment.yaml b/Api.Authorization/.kubernetes/deployment.yaml index 5041e227..aca5889f 100644 --- a/Api.Authorization/.kubernetes/deployment.yaml +++ b/Api.Authorization/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -84,11 +84,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Authorization/Api.Authorization.sln b/Api.Authorization/Api.Authorization.sln index 2f798b65..0deccbeb 100644 --- a/Api.Authorization/Api.Authorization.sln +++ b/Api.Authorization/Api.Authorization.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-jwt-secret.yaml = .kubernetes\auth-jwt-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Authorization/README.md b/Api.Authorization/README.md index 1fceab65..a8c62533 100644 --- a/Api.Authorization/README.md +++ b/Api.Authorization/README.md @@ -28,7 +28,7 @@ are automatically excluded. In this example, only the root login action is expos API documentation has been configured to make it easier to explore the available actions in the `AuthController`. Any actions that are not enabled due to omitted configuration are automatically excluded. The API documentation is available at: **http://localhost:8080/docs**. -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. The following endpoint from the auth controller is available for testing. @@ -43,4 +43,4 @@ Additionally, the following endpoint is available for testing authorization. | `http://localhost:8080/api/examples/authenticate` | Returns a simple `200 OK` response, when JWT authorization is successful. | | `http://localhost:8080/api/examples/forbidden` | Returns a simple `200 OK` response, when JWT authorization is successful with `CustomClaim`, otherwise returns `403 FORBIDDEN` response. | -> 📖 Learn more about **[Nano Authentication](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#authentication)**. +> 📖 Learn more about **[Nano Authentication](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#authentication)**. diff --git a/Api.ContentNegotiation/.github/workflows/build-and-deploy.yml b/Api.ContentNegotiation/.github/workflows/build-and-deploy.yml index 4420d630..2b200b6d 100644 --- a/Api.ContentNegotiation/.github/workflows/build-and-deploy.yml +++ b/Api.ContentNegotiation/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.ContentNegotiation IMAGE_NAME: api.contentnegotiation @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.ContentNegotiation/.kubernetes/deployment.yaml b/Api.ContentNegotiation/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.ContentNegotiation/.kubernetes/deployment.yaml +++ b/Api.ContentNegotiation/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.ContentNegotiation/README.md b/Api.ContentNegotiation/README.md index 1d3972f9..9aeb4898 100644 --- a/Api.ContentNegotiation/README.md +++ b/Api.ContentNegotiation/README.md @@ -27,4 +27,4 @@ The following endpoint is available for testing: | --------------------------------------------------------- | --------------------------------------------------------------------------------- | | `http://localhost:8080/api/examples/content-negotiation` | Returns a simple `200 OK` response, no matter if `Accept` header is set or not. | -> 📖 Learn more about **[Nano Content Negotiation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#content-negotiation)**. +> 📖 Learn more about **[Nano Content Negotiation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#content-negotiation)**. diff --git a/Api.Cookies/.github/workflows/build-and-deploy.yml b/Api.Cookies/.github/workflows/build-and-deploy.yml index 8574604b..3a341c47 100644 --- a/Api.Cookies/.github/workflows/build-and-deploy.yml +++ b/Api.Cookies/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Cookies IMAGE_NAME: api.cookies @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Cookies/.kubernetes/deployment.yaml b/Api.Cookies/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.Cookies/.kubernetes/deployment.yaml +++ b/Api.Cookies/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.CustomConfigSection/.github/workflows/build-and-deploy.yml b/Api.CustomConfigSection/.github/workflows/build-and-deploy.yml index 007e17b7..add53801 100644 --- a/Api.CustomConfigSection/.github/workflows/build-and-deploy.yml +++ b/Api.CustomConfigSection/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.CustomConfigSection IMAGE_NAME: api.customconfigsection @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.CustomConfigSection/.kubernetes/deployment.yaml b/Api.CustomConfigSection/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.CustomConfigSection/.kubernetes/deployment.yaml +++ b/Api.CustomConfigSection/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.CustomMiddleware/.github/workflows/build-and-deploy.yml b/Api.CustomMiddleware/.github/workflows/build-and-deploy.yml index 30521710..b1ae72ca 100644 --- a/Api.CustomMiddleware/.github/workflows/build-and-deploy.yml +++ b/Api.CustomMiddleware/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.CustomMiddleware IMAGE_NAME: api.custommiddleware @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.CustomMiddleware/.kubernetes/deployment.yaml b/Api.CustomMiddleware/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.CustomMiddleware/.kubernetes/deployment.yaml +++ b/Api.CustomMiddleware/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.CustomMiddleware/README.md b/Api.CustomMiddleware/README.md index ac0aac9d..9a1136fb 100644 --- a/Api.CustomMiddleware/README.md +++ b/Api.CustomMiddleware/README.md @@ -28,7 +28,7 @@ The following endpoint is available for testing: | ------------------------------------------------------- | ------------------------------------------------------------ | | `http://localhost:8080/api/examples/custom-middleware` | Returns a simple `200 OK` response, with the custom header. | -> 📖 Learn more about **[Nano Custom Middleware](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#custom-middleware)**. +> 📖 Learn more about **[Nano Custom Middleware](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#custom-middleware)**. ## Registration The application register custom middleware that adds a header `CustomMiddleware` to all response with the value `awesome`, as shown below. diff --git a/Api.CustomService/.github/workflows/build-and-deploy.yml b/Api.CustomService/.github/workflows/build-and-deploy.yml index 9956e837..4cb38d5a 100644 --- a/Api.CustomService/.github/workflows/build-and-deploy.yml +++ b/Api.CustomService/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.CustomService IMAGE_NAME: api.customservice @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.CustomService/.kubernetes/deployment.yaml b/Api.CustomService/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.CustomService/.kubernetes/deployment.yaml +++ b/Api.CustomService/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.CustomService/README.md b/Api.CustomService/README.md index 929ae32a..7c6a3071 100644 --- a/Api.CustomService/README.md +++ b/Api.CustomService/README.md @@ -28,7 +28,7 @@ The following endpoint is available for testing. | ---------------------------------------------------- | -------------------------------------------------------------------------------- | | `http://localhost:8080/api/examples/custom-servuce` | Returns a simple `200 OK` response, with a message from the `IExampleServuce` | -> 📖 Learn more about **[Nano Custom Services](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App#custom-services)**. +> 📖 Learn more about **[Nano Custom Services](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App/README.md#custom-services)**. ## Registration A custom service, `IExampleService` has been added and implemented. In `program.cs` the service is registered using `ConfigureService(...)` method as shown below diff --git a/Api.Data.Audit/.github/workflows/build-and-deploy.yml b/Api.Data.Audit/.github/workflows/build-and-deploy.yml index 6b9f5e43..e8e13f4a 100644 --- a/Api.Data.Audit/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Audit/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.Audit IMAGE_NAME: api.data.audit @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - MYSQL_DATABASE_NAME: nanoDb - MYSQL_DATABASE_USER: api-data-mysql-user - MYSQL_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - MYSQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - MYSQL_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - MYSQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - MYSQL_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_DATABASE_USER }};Pwd=${{ env.MYSQL_SERVICE_PASSWORD }};SslMode=Preferred; - MYSQL_MIGRATION_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_ADMIN_USER }};Pwd=${{ env.MYSQL_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,70 +122,90 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:MYSQL_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:MYSQL_DATABASE_USER');" $env:MYSQL_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:MYSQL_DATABASE_USER'@'%' IDENTIFIED BY '$env:MYSQL_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:MYSQL_DATABASE_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:MYSQL_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-secret ` --from-literal=data-connectionstring=$env:MYSQL_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.Audit/.kubernetes/auth-sql-secret.yaml b/Api.Data.Audit/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.Audit/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.Audit/.kubernetes/deployment.yaml b/Api.Data.Audit/.kubernetes/deployment.yaml index 5901ef66..7e8688ef 100644 --- a/Api.Data.Audit/.kubernetes/deployment.yaml +++ b/Api.Data.Audit/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.Audit/Api.Data.Audit.sln b/Api.Data.Audit/Api.Data.Audit.sln index 36b38b53..281a45e0 100644 --- a/Api.Data.Audit/Api.Data.Audit.sln +++ b/Api.Data.Audit/Api.Data.Audit.sln @@ -23,6 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes" .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml .kubernetes\service.yaml = .kubernetes\service.yaml + .kubernetes\sql-auth-secret.yaml = .kubernetes\sql-auth-secret.yaml EndProjectSection EndProject Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", ".docker\docker-compose.dcproj", "{557A0C48-DA6A-4D7C-8668-94F08A390F4B}" diff --git a/Api.Data.Audit/README.md b/Api.Data.Audit/README.md index e33093b3..1f95c3f1 100644 --- a/Api.Data.Audit/README.md +++ b/Api.Data.Audit/README.md @@ -32,6 +32,6 @@ we are invoking the endpoint with an unauthenticated user. Also, API documentation has been configured, in order to easier see which audit endpoints are available. It can be accessed here: **[http://localhost:8080/docs](http://localhost:8080/docs)**. -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. -> 📖 Learn more about **[Nano Data Audit](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#audit)**. +> 📖 Learn more about **[Nano Data Audit](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#audit)**. diff --git a/Api.Data.EntityEvents/.github/workflows/build-and-deploy.yml b/Api.Data.EntityEvents/.github/workflows/build-and-deploy.yml index 9377aa15..ab07e819 100644 --- a/Api.Data.EntityEvents/.github/workflows/build-and-deploy.yml +++ b/Api.Data.EntityEvents/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.EntityEvents IMAGE_NAME: api.data.entityevents @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-mysql-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_USER }};Pwd=${{ env.DATA_PASSWORD }};SslMode=Preferred; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_ADMIN_USER }};Pwd=${{ env.DATA_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,70 +122,90 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:DATA_USER');" $env:DATA_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:DATA_USER'@'%' IDENTIFIED BY '$env:DATA_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:DATA_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:DATA_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.EntityEvents/.kubernetes/auth-sql-secret.yaml b/Api.Data.EntityEvents/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.EntityEvents/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.EntityEvents/.kubernetes/deployment.yaml b/Api.Data.EntityEvents/.kubernetes/deployment.yaml index c54d54cc..ddbdae40 100644 --- a/Api.Data.EntityEvents/.kubernetes/deployment.yaml +++ b/Api.Data.EntityEvents/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,14 +41,20 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring + - name: Eventing__Credentials__Id + valueFrom: + secretKeyRef: + name: rabbitmq-auth + key: username + envFrom: - name: Eventing__Credentials__Secret valueFrom: secretKeyRef: - name: rabbitmq - key: rabbitmq-password -``` envFrom: + name: rabbitmq-auth + key: password + envFrom: - configMapRef: name: %SERVICE_NAME%-config resources: @@ -84,11 +90,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.EntityEvents/Api.Data.EntityEvents.sln b/Api.Data.EntityEvents/Api.Data.EntityEvents.sln index e024eddf..6806eb36 100644 --- a/Api.Data.EntityEvents/Api.Data.EntityEvents.sln +++ b/Api.Data.EntityEvents/Api.Data.EntityEvents.sln @@ -22,6 +22,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml @@ -163,7 +164,6 @@ Global {7F25283D-B5C5-8CA9-8722-E84B380905C6} = {3339F5B7-AA56-4192-B201-75B2620036B1} {F0FA24A3-0F7B-3962-C471-78992174D4B6} = {3339F5B7-AA56-4192-B201-75B2620036B1} {E55DCB7C-9082-3538-6288-5A6E5C8DE183} = {3339F5B7-AA56-4192-B201-75B2620036B1} - {77596F3E-C6F4-A01F-B4D9-9370C3ED3CBC} = {7E0D2A84-F3DE-4406-BE71-30EE6723E6D0} {B008D867-12B4-7EB7-D707-1C1503E4151A} = {7E0D2A84-F3DE-4406-BE71-30EE6723E6D0} {6BE9DA86-B487-64ED-7BF6-08CA10A05A97} = {3339F5B7-AA56-4192-B201-75B2620036B1} {A1CC2D8C-C323-51C2-2480-BFFD9DAD9F1F} = {3339F5B7-AA56-4192-B201-75B2620036B1} diff --git a/Api.Data.EntityEvents/Api.Data.EntityEvents/appsettings.Development.json b/Api.Data.EntityEvents/Api.Data.EntityEvents/appsettings.Development.json index 34379ce9..74c8964f 100644 --- a/Api.Data.EntityEvents/Api.Data.EntityEvents/appsettings.Development.json +++ b/Api.Data.EntityEvents/Api.Data.EntityEvents/appsettings.Development.json @@ -2,5 +2,11 @@ "Data": { "StartupAction": "Migrate", "ConnectionString": "Server=host.docker.internal;Database=nanoDb;Uid=sa;Pwd=myPassword_123" + }, + "Eventing": { + "Credentials": { + "Id": "rabbitmq_user", + "Secret": "password" + } } } \ No newline at end of file diff --git a/Api.Data.EntityEvents/Api.Data.EntityEvents/appsettings.json b/Api.Data.EntityEvents/Api.Data.EntityEvents/appsettings.json index 3b1ce56d..3402a745 100644 --- a/Api.Data.EntityEvents/Api.Data.EntityEvents/appsettings.json +++ b/Api.Data.EntityEvents/Api.Data.EntityEvents/appsettings.json @@ -14,10 +14,6 @@ "ConnectionString": null }, "Eventing": { - "Host": "rabbitmq", - "Credentials": { - "Id": "rabbitmq_user", - "Secret": "password" - } + "Host": "rabbitmq" } } \ No newline at end of file diff --git a/Api.Data.EntityEvents/README.md b/Api.Data.EntityEvents/README.md index d4991a41..f783dcec 100644 --- a/Api.Data.EntityEvents/README.md +++ b/Api.Data.EntityEvents/README.md @@ -30,7 +30,7 @@ entity relationships can be inspected directly in the codebase. Also an `OnInserting` and `OnUpdating` trigger has been mapped for `Customer`, to show how the entity events will get the updated value. -> 📖 Learn more about **[Nano Data Triggers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#triggers)**. +> 📖 Learn more about **[Nano Data Triggers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#triggers)**. Both `Person` and `Customer` are annotated with the `PublishAttribute`, and define property names that determine which fields should trigger publish events on added, modified, and deleted actions. Several controllers have also been added to support these entities, enabling various create, update, and delete operations to trigger `Customer` entity events. When @@ -40,4 +40,4 @@ for the `Customer`. It is also important to note that `Customer` inherits publishable property definitions from `Person`. To ensure all property names defined across the entire inheritance hierarchy are included, Nano aggregates the `PublishAttribute` metadata and hydrates entities both forward (for direct changes) and in reverse (via foreign key relationships) to capture deferred changes. -> 📖 Learn more about **[Nano Data Entity Events](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#entity-events)**. +> 📖 Learn more about **[Nano Data Entity Events](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#entity-events)**. diff --git a/Api.Data.Identity.Auth.ApiKey/.github/workflows/build-and-deploy.yml b/Api.Data.Identity.Auth.ApiKey/.github/workflows/build-and-deploy.yml index 059497a6..e43c9804 100644 --- a/Api.Data.Identity.Auth.ApiKey/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Identity.Auth.ApiKey/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.Identity.Auth.ApiKey IMAGE_NAME: api.data.identity.auth.apikey @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -33,37 +36,38 @@ env: KUBERNETES_CPU_REQUEST: 200m KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 - MYSQL_DATABASE_NAME: nanoDb - MYSQL_DATABASE_USER: api-data-mysql-user - MYSQL_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - MYSQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - MYSQL_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - MYSQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - MYSQL_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_DATABASE_USER }};Pwd=${{ env.MYSQL_SERVICE_PASSWORD }};SslMode=Preferred; - MYSQL_MIGRATION_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_ADMIN_USER }};Pwd=${{ env.MYSQL_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} AUTH_JWT_PUBLIC_KEY: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AUTH_JWT_PUBLIC_KEY || secrets.STAGING_AUTH_JWT_PUBLIC_KEY }} AUTH_JWT_PRIVATE_KEY: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AUTH_JWT_PRIVATE_KEY || secrets.STAGING_AUTH_JWT_PRIVATE_KEY }} AUTH_API_KEY_SECRET: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AUTH_API_KEY_SECRET || secrets.STAGING_AUTH_API_KEY_SECRET }} ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -92,7 +96,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -108,9 +112,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -121,82 +125,104 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:MYSQL_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:MYSQL_DATABASE_USER');" $env:MYSQL_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:MYSQL_DATABASE_USER'@'%' IDENTIFIED BY '$env:MYSQL_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:MYSQL_DATABASE_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:MYSQL_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-secret ` --from-literal=data-connectionstring=$env:MYSQL_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; - sudo kubectl create secret generic auth-jwt-secret --from-literal=jwt-public-key=$env:AUTH_JWT_PUBLIC_KEY --from-literal=jwt-private-key=$env:AUTH_JWT_PRIVATE_KEY --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + Get-Content .kubernetes/auth-jwt-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-jwt-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-jwt-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; - sudo kubectl create secret generic auth-api-key-secret --from-literal=apikey-secret=$env:AUTH_API_KEY_SECRET --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + Get-Content .kubernetes/auth-apikey-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-apikey-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-apikey-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.Identity.Auth.ApiKey/.kubernetes/auth-apikey-secret.yaml b/Api.Data.Identity.Auth.ApiKey/.kubernetes/auth-apikey-secret.yaml new file mode 100644 index 00000000..45db9a37 --- /dev/null +++ b/Api.Data.Identity.Auth.ApiKey/.kubernetes/auth-apikey-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: auth-api-key-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + apikey-secret: %AUTH_API_KEY_SECRET% + diff --git a/Api.Data.Identity.Auth.ApiKey/.kubernetes/auth-jwt-secret.yaml b/Api.Data.Identity.Auth.ApiKey/.kubernetes/auth-jwt-secret.yaml new file mode 100644 index 00000000..3898973f --- /dev/null +++ b/Api.Data.Identity.Auth.ApiKey/.kubernetes/auth-jwt-secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: auth-jwt-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + jwt-public-key: %AUTH_JWT_PUBLIC_KEY% + jwt-private-key: %AUTH_JWT_PRIVATE_KEY% + diff --git a/Api.Data.Identity.Auth.ApiKey/.kubernetes/auth-sql-secret.yaml b/Api.Data.Identity.Auth.ApiKey/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.Identity.Auth.ApiKey/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.Identity.Auth.ApiKey/.kubernetes/deployment.yaml b/Api.Data.Identity.Auth.ApiKey/.kubernetes/deployment.yaml index e5d985b7..c8c3659c 100644 --- a/Api.Data.Identity.Auth.ApiKey/.kubernetes/deployment.yaml +++ b/Api.Data.Identity.Auth.ApiKey/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring - name: Data__Identity__ApiKey valueFrom: @@ -94,11 +94,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.Identity.Auth.ApiKey/Api.Data.Identity.Auth.ApiKey.sln b/Api.Data.Identity.Auth.ApiKey/Api.Data.Identity.Auth.ApiKey.sln index 7acbb800..058469a3 100644 --- a/Api.Data.Identity.Auth.ApiKey/Api.Data.Identity.Auth.ApiKey.sln +++ b/Api.Data.Identity.Auth.ApiKey/Api.Data.Identity.Auth.ApiKey.sln @@ -19,6 +19,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-apikey-secret.yaml = .kubernetes\auth-apikey-secret.yaml + .kubernetes\auth-jwt-secret.yaml = .kubernetes\auth-jwt-secret.yaml + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.Identity.Auth.ApiKey/README.md b/Api.Data.Identity.Auth.ApiKey/README.md index c3be91d9..45050da2 100644 --- a/Api.Data.Identity.Auth.ApiKey/README.md +++ b/Api.Data.Identity.Auth.ApiKey/README.md @@ -29,7 +29,7 @@ Try out the different API key identity and authentication methods. API documentation has been configured to make it easier to explore the available actions in the `AuthController`. Any actions that are not enabled due to omitted configuration are automatically excluded. The API documentation is available at: **http://localhost:8080/docs**. -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. The following endpoint from the auth controller is available for testing: @@ -42,7 +42,7 @@ The following endpoint from the auth controller is available for testing: Additionally, the identity controller is also avaialble, and the actions can be used for testing authorization. -> 📖 Learn more about **[Nano API Key Authentication](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#authentication)**. +> 📖 Learn more about **[Nano API Key Authentication](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#authentication)**. ## Configuration Configured the application with the necessary authentication setup, in addition to the existing identity and jwt configuration. @@ -84,11 +84,3 @@ env: ``` ...and created during the Kubernetes deploy step. - -```yaml -sudo kubectl create secret generic auth-api-key-secret --from-literal=apikey-secret=$env:AUTH_API_KEY_SECRET --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; -if ($LastExitCode -ne 0) -{ - throw "error"; -}; -``` diff --git a/Api.Data.Identity.Auth.External.Custom/.github/workflows/build-and-deploy.yml b/Api.Data.Identity.Auth.External.Custom/.github/workflows/build-and-deploy.yml index 1f17e94f..49b0d6db 100644 --- a/Api.Data.Identity.Auth.External.Custom/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Identity.Auth.External.Custom/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.Identity.Auth.External.Custom IMAGE_NAME: api.data.identity.auth.external.custom @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -33,36 +36,37 @@ env: KUBERNETES_CPU_REQUEST: 200m KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 - MYSQL_DATABASE_NAME: nanoDb - MYSQL_DATABASE_USER: api-data-mysql-user - MYSQL_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - MYSQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - MYSQL_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - MYSQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - MYSQL_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_DATABASE_USER }};Pwd=${{ env.MYSQL_SERVICE_PASSWORD }};SslMode=Preferred; - MYSQL_MIGRATION_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_ADMIN_USER }};Pwd=${{ env.MYSQL_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} AUTH_JWT_PUBLIC_KEY: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AUTH_JWT_PUBLIC_KEY || secrets.STAGING_AUTH_JWT_PUBLIC_KEY }} AUTH_JWT_PRIVATE_KEY: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AUTH_JWT_PRIVATE_KEY || secrets.STAGING_AUTH_JWT_PRIVATE_KEY }} ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -91,7 +95,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -107,9 +111,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -120,76 +124,97 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:MYSQL_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:MYSQL_DATABASE_USER');" $env:MYSQL_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:MYSQL_DATABASE_USER'@'%' IDENTIFIED BY '$env:MYSQL_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:MYSQL_DATABASE_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:MYSQL_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-secret ` --from-literal=data-connectionstring=$env:MYSQL_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; - sudo kubectl create secret generic auth-jwt-secret --from-literal=jwt-public-key=$env:AUTH_JWT_PUBLIC_KEY --from-literal=jwt-private-key=$env:AUTH_JWT_PRIVATE_KEY --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + Get-Content .kubernetes/auth-jwt-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-jwt-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-jwt-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.Identity.Auth.External.Custom/.kubernetes/auth-jwt-secret.yaml b/Api.Data.Identity.Auth.External.Custom/.kubernetes/auth-jwt-secret.yaml new file mode 100644 index 00000000..3898973f --- /dev/null +++ b/Api.Data.Identity.Auth.External.Custom/.kubernetes/auth-jwt-secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: auth-jwt-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + jwt-public-key: %AUTH_JWT_PUBLIC_KEY% + jwt-private-key: %AUTH_JWT_PRIVATE_KEY% + diff --git a/Api.Data.Identity.Auth.External.Custom/.kubernetes/auth-sql-secret.yaml b/Api.Data.Identity.Auth.External.Custom/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.Identity.Auth.External.Custom/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.Identity.Auth.External.Custom/.kubernetes/deployment.yaml b/Api.Data.Identity.Auth.External.Custom/.kubernetes/deployment.yaml index d7fb451d..f615e1a7 100644 --- a/Api.Data.Identity.Auth.External.Custom/.kubernetes/deployment.yaml +++ b/Api.Data.Identity.Auth.External.Custom/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring - name: App__Authentication__Jwt__PublicKey valueFrom: @@ -89,11 +89,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.Identity.Auth.External.Custom/Api.Data.Identity.Auth.External.Custom.sln b/Api.Data.Identity.Auth.External.Custom/Api.Data.Identity.Auth.External.Custom.sln index b859e80a..e7f0ea76 100644 --- a/Api.Data.Identity.Auth.External.Custom/Api.Data.Identity.Auth.External.Custom.sln +++ b/Api.Data.Identity.Auth.External.Custom/Api.Data.Identity.Auth.External.Custom.sln @@ -19,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-jwt-secret.yaml = .kubernetes\auth-jwt-secret.yaml + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.Identity.Auth.External.Custom/README.md b/Api.Data.Identity.Auth.External.Custom/README.md index 0f9af004..f9e7730c 100644 --- a/Api.Data.Identity.Auth.External.Custom/README.md +++ b/Api.Data.Identity.Auth.External.Custom/README.md @@ -28,7 +28,7 @@ provider in Nano. API documentation has been configured to make it easier to explore the available actions in the `AuthController`. Any actions that are not enabled due to omitted configuration are automatically excluded. The API documentation is available at: **http://localhost:8080/docs**. -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. The following endpoint from the auth controller is available for testing: @@ -49,7 +49,7 @@ The following new endpoints related to the custom authentication provider from t | `http://localhost:8080/api/users/{id}/external-logins/add/custom` | Adds a `Custom` external login to a user account. | | `http://localhost:8080/api/users/{id}/external-logins/remove/custom` | Removes an `Custom` login from a user account. | -> 📖 Learn more about **[Nano Authentication](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#authentication)**. +> 📖 Learn more about **[Nano Authentication](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#authentication)**. ## Configuration Configured the application with the necessary authentication setup, in addition to the identity configuration. diff --git a/Api.Data.Identity.Auth.Jwt/.github/workflows/build-and-deploy.yml b/Api.Data.Identity.Auth.Jwt/.github/workflows/build-and-deploy.yml index 43761bfc..7141f0f2 100644 --- a/Api.Data.Identity.Auth.Jwt/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Identity.Auth.Jwt/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.Identity.Auth.Jwt IMAGE_NAME: api.data.identity.auth.jwt @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -33,36 +36,37 @@ env: KUBERNETES_CPU_REQUEST: 200m KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 - MYSQL_DATABASE_NAME: nanoDb - MYSQL_DATABASE_USER: api-data-mysql-user - MYSQL_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - MYSQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - MYSQL_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - MYSQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - MYSQL_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_DATABASE_USER }};Pwd=${{ env.MYSQL_SERVICE_PASSWORD }};SslMode=Preferred; - MYSQL_MIGRATION_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_ADMIN_USER }};Pwd=${{ env.MYSQL_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} AUTH_JWT_PUBLIC_KEY: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AUTH_JWT_PUBLIC_KEY || secrets.STAGING_AUTH_JWT_PUBLIC_KEY }} AUTH_JWT_PRIVATE_KEY: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AUTH_JWT_PRIVATE_KEY || secrets.STAGING_AUTH_JWT_PRIVATE_KEY }} ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -91,7 +95,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -107,9 +111,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -120,76 +124,97 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:MYSQL_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:MYSQL_DATABASE_USER');" $env:MYSQL_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:MYSQL_DATABASE_USER'@'%' IDENTIFIED BY '$env:MYSQL_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:MYSQL_DATABASE_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:MYSQL_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-secret ` --from-literal=data-connectionstring=$env:MYSQL_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; - sudo kubectl create secret generic auth-jwt-secret --from-literal=jwt-public-key=$env:AUTH_JWT_PUBLIC_KEY --from-literal=jwt-private-key=$env:AUTH_JWT_PRIVATE_KEY --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + Get-Content .kubernetes/auth-jwt-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-jwt-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-jwt-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.Identity.Auth.Jwt/.kubernetes/auth-jwt-secret.yaml b/Api.Data.Identity.Auth.Jwt/.kubernetes/auth-jwt-secret.yaml new file mode 100644 index 00000000..3898973f --- /dev/null +++ b/Api.Data.Identity.Auth.Jwt/.kubernetes/auth-jwt-secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: auth-jwt-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + jwt-public-key: %AUTH_JWT_PUBLIC_KEY% + jwt-private-key: %AUTH_JWT_PRIVATE_KEY% + diff --git a/Api.Data.Identity.Auth.Jwt/.kubernetes/auth-sql-secret.yaml b/Api.Data.Identity.Auth.Jwt/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.Identity.Auth.Jwt/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.Identity.Auth.Jwt/.kubernetes/deployment.yaml b/Api.Data.Identity.Auth.Jwt/.kubernetes/deployment.yaml index d7fb451d..f615e1a7 100644 --- a/Api.Data.Identity.Auth.Jwt/.kubernetes/deployment.yaml +++ b/Api.Data.Identity.Auth.Jwt/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring - name: App__Authentication__Jwt__PublicKey valueFrom: @@ -89,11 +89,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.Identity.Auth.Jwt/Api.Data.Identity.Auth.Jwt.sln b/Api.Data.Identity.Auth.Jwt/Api.Data.Identity.Auth.Jwt.sln index 18297930..39eeee77 100644 --- a/Api.Data.Identity.Auth.Jwt/Api.Data.Identity.Auth.Jwt.sln +++ b/Api.Data.Identity.Auth.Jwt/Api.Data.Identity.Auth.Jwt.sln @@ -19,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-jwt-secret.yaml = .kubernetes\auth-jwt-secret.yaml + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.Identity.Auth.Jwt/README.md b/Api.Data.Identity.Auth.Jwt/README.md index 9824568f..bfbaf354 100644 --- a/Api.Data.Identity.Auth.Jwt/README.md +++ b/Api.Data.Identity.Auth.Jwt/README.md @@ -27,7 +27,7 @@ the `BaseAuthController`. API documentation has been configured to make it easier to explore the available actions in the `AuthController`. Any actions that are not enabled due to omitted configuration are automatically excluded. The API documentation is available at: **http://localhost:8080/docs**. -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. The following endpoint from the auth controller is available for testing: @@ -39,7 +39,7 @@ The following endpoint from the auth controller is available for testing: Additionally, the identity controller is also avaialble, and the actions can be used for testing authorization. -> 📖 Learn more about **[Nano Authentication](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#authentication)**. +> 📖 Learn more about **[Nano Authentication](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#authentication)**. ## Configuration Configured the application with the necessary authentication setup, in addition to the identity configuration. diff --git a/Api.Data.Identity/.github/workflows/build-and-deploy.yml b/Api.Data.Identity/.github/workflows/build-and-deploy.yml index 89ba45d0..ec82c6b7 100644 --- a/Api.Data.Identity/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Identity/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.Identity IMAGE_NAME: api.data.identity @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - MYSQL_DATABASE_NAME: nanoDb - MYSQL_DATABASE_USER: api-data-mysql-user - MYSQL_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - MYSQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - MYSQL_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - MYSQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - MYSQL_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_DATABASE_USER }};Pwd=${{ env.MYSQL_SERVICE_PASSWORD }};SslMode=Preferred; - MYSQL_MIGRATION_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_ADMIN_USER }};Pwd=${{ env.MYSQL_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,70 +122,90 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:MYSQL_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:MYSQL_DATABASE_USER');" $env:MYSQL_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:MYSQL_DATABASE_USER'@'%' IDENTIFIED BY '$env:MYSQL_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:MYSQL_DATABASE_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:MYSQL_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-secret ` --from-literal=data-connectionstring=$env:MYSQL_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.Identity/.kubernetes/auth-sql-secret.yaml b/Api.Data.Identity/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.Identity/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.Identity/.kubernetes/deployment.yaml b/Api.Data.Identity/.kubernetes/deployment.yaml index 5901ef66..7e8688ef 100644 --- a/Api.Data.Identity/.kubernetes/deployment.yaml +++ b/Api.Data.Identity/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.Identity/Api.Data.Identity.sln b/Api.Data.Identity/Api.Data.Identity.sln index f87402ac..ee1f6dac 100644 --- a/Api.Data.Identity/Api.Data.Identity.sln +++ b/Api.Data.Identity/Api.Data.Identity.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.Identity/README.md b/Api.Data.Identity/README.md index 0baad855..4edd3d67 100644 --- a/Api.Data.Identity/README.md +++ b/Api.Data.Identity/README.md @@ -28,12 +28,12 @@ The application is configured to audit all identity models. Also, API documentation has been configured, in order to easier see which audit endpoints are available. It can be accessed here: **[http://localhost:8080/docs](http://localhost:8080/docs)**. -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. Not all identity actions are enabled in this example. It demonstrates identity functionality without any authentication enabled. Other lessons cover the different supported authentication methods and the corresponding identity actions to manage them. -> 📖 Learn more about **[Nano Data Identity](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#identity)**. +> 📖 Learn more about **[Nano Data Identity](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#identity)**. ## Configuration The data identity has been configured for the application. The `UseAudit` ahs been set to `All` in order to audit log all identity changes. Normally, you would probably be more diff --git a/Api.Data.InMemory/.github/workflows/build-and-deploy.yml b/Api.Data.InMemory/.github/workflows/build-and-deploy.yml index fa3b6652..50a2894a 100644 --- a/Api.Data.InMemory/.github/workflows/build-and-deploy.yml +++ b/Api.Data.InMemory/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.InMemory IMAGE_NAME: api.data.inmemory @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -120,28 +128,28 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.InMemory/.kubernetes/deployment.yaml b/Api.Data.InMemory/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.Data.InMemory/.kubernetes/deployment.yaml +++ b/Api.Data.InMemory/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.InMemory/README.md b/Api.Data.InMemory/README.md index 8ecfb045..bb283b02 100644 --- a/Api.Data.InMemory/README.md +++ b/Api.Data.InMemory/README.md @@ -22,22 +22,22 @@ This application builds on **[Api.Blank](https://github.com/Nano-Core/Nano.Lesso the Nano `BaseEntityControllerr`. The available entity endpoints are inherited, and no additional endpoints has been added. This example demonstrates how various parts of Nano data work together. All data configuration and registration have been completed, and classes have been implemented -for the data parts, including [Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-models), [Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-mappings), -and the [Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-context). As the in-memory data provider doesn't use migrations there is no need +for the data parts, including [Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-models), [Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-mappings), +and the [Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-context). As the in-memory data provider doesn't use migrations there is no need to implement the `BaseDbContextFactory`. -Additionally, the example shows how Nano [Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#repositories) works along with the corresponding -entity controllers. For more information on controllers and how they are connected with entity models, see [Nano Entity Controllers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#controllers). +Additionally, the example shows how Nano [Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#repositories) works along with the corresponding +entity controllers. For more information on controllers and how they are connected with entity models, see [Nano Entity Controllers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#controllers). Also, API documentation has been configured, in order to easier see which endpoints are available. It can be accessed here: **[http://localhost:8080/docs](http://localhost:8080/docs)**. -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. Additionally, controllers have been implemented to demonstrate controllers for creatable, updatable, creatable-and-updatable, and deletable entities. When viewing the API documentation, observe how the available endpoints differ depending on the capabilities supported by each controller. -> 📖 Learn more about **[Nano.Data.InMemory](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.InMemory)**. +> 📖 Learn more about **[Nano.Data.InMemory](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.InMemory/README#nanodatainmemory)**. ## Registration The following data provider has been registered using `ConfigureServices(...)` in `program.cs`. diff --git a/Api.Data.LazyLoading/.github/workflows/build-and-deploy.yml b/Api.Data.LazyLoading/.github/workflows/build-and-deploy.yml index b831b671..2d8de195 100644 --- a/Api.Data.LazyLoading/.github/workflows/build-and-deploy.yml +++ b/Api.Data.LazyLoading/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.LazyLoading IMAGE_NAME: api.data.lazyloading @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - MYSQL_DATABASE_NAME: nanoDb - MYSQL_DATABASE_USER: api-data-mysql-user - MYSQL_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - MYSQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - MYSQL_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - MYSQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - MYSQL_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_DATABASE_USER }};Pwd=${{ env.MYSQL_SERVICE_PASSWORD }};SslMode=Preferred; - MYSQL_MIGRATION_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_ADMIN_USER }};Pwd=${{ env.MYSQL_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,70 +122,90 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:MYSQL_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:MYSQL_DATABASE_USER');" $env:MYSQL_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:MYSQL_DATABASE_USER'@'%' IDENTIFIED BY '$env:MYSQL_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:MYSQL_DATABASE_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:MYSQL_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-secret ` --from-literal=data-connectionstring=$env:MYSQL_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.LazyLoading/.kubernetes/auth-sql-secret.yaml b/Api.Data.LazyLoading/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.LazyLoading/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.LazyLoading/.kubernetes/deployment.yaml b/Api.Data.LazyLoading/.kubernetes/deployment.yaml index 5901ef66..7e8688ef 100644 --- a/Api.Data.LazyLoading/.kubernetes/deployment.yaml +++ b/Api.Data.LazyLoading/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.LazyLoading/Api.Data.LazyLoading.sln b/Api.Data.LazyLoading/Api.Data.LazyLoading.sln index d6e19f30..4b14dc83 100644 --- a/Api.Data.LazyLoading/Api.Data.LazyLoading.sln +++ b/Api.Data.LazyLoading/Api.Data.LazyLoading.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.LazyLoading/README.md b/Api.Data.LazyLoading/README.md index 518f7972..6cb45b2f 100644 --- a/Api.Data.LazyLoading/README.md +++ b/Api.Data.LazyLoading/README.md @@ -23,7 +23,7 @@ demonstrate repository autosave. Entity controllers have been simplified to show Once the object graph is created, notice that only `IncludedRelations` appears in the response. Although `Relations` is lazy-loaded in the code, it is not included because it lacks the `Include` annotation. -> 📖 Learn more about **[Nano Data Lazy Loading](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#lazy-loading)**. +> 📖 Learn more about **[Nano Data Lazy Loading](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#lazy-loading)**. ## Configuration ```json diff --git a/Api.Data.MySql.Collation/.github/workflows/build-and-deploy.yml b/Api.Data.MySql.Collation/.github/workflows/build-and-deploy.yml index 514b42df..530f1d65 100644 --- a/Api.Data.MySql.Collation/.github/workflows/build-and-deploy.yml +++ b/Api.Data.MySql.Collation/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.MySql.Collation IMAGE_NAME: api.data.mysql.collation @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-mysql-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_USER }};Pwd=${{ env.DATA_PASSWORD }};SslMode=Preferred; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_ADMIN_USER }};Pwd=${{ env.DATA_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,70 +122,90 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:DATA_USER');" $env:DATA_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:DATA_USER'@'%' IDENTIFIED BY '$env:DATA_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:DATA_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:DATA_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.MySql.Collation/.kubernetes/auth-sql-secret.yaml b/Api.Data.MySql.Collation/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.MySql.Collation/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.MySql.Collation/.kubernetes/deployment.yaml b/Api.Data.MySql.Collation/.kubernetes/deployment.yaml index 560e3901..7e8688ef 100644 --- a/Api.Data.MySql.Collation/.kubernetes/deployment.yaml +++ b/Api.Data.MySql.Collation/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.MySql.Collation/Api.Data.MySql.Collation.sln b/Api.Data.MySql.Collation/Api.Data.MySql.Collation.sln index e2a37955..932fcc07 100644 --- a/Api.Data.MySql.Collation/Api.Data.MySql.Collation.sln +++ b/Api.Data.MySql.Collation/Api.Data.MySql.Collation.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.MySql.Collation/README.md b/Api.Data.MySql.Collation/README.md index cf09de59..9ae5aa06 100644 --- a/Api.Data.MySql.Collation/README.md +++ b/Api.Data.MySql.Collation/README.md @@ -25,7 +25,7 @@ a case-insensitive collation returns results regardless of letter casing. ⚠️ Note: Changing this setting affects only new migrations and will not modify existing tables or columns. -> 📖 Learn more about **[Nano.Data.MySql](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.MySql)**. +> 📖 Learn more about **[Nano.Data.MySql](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.MySql/README.md#nanodatamysql)**. ## Configuration The collation is set in `appsettings`. diff --git a/Api.Data.MySql.Mappings/.github/workflows/build-and-deploy.yml b/Api.Data.MySql.Mappings/.github/workflows/build-and-deploy.yml index ed044ed6..4e0460d8 100644 --- a/Api.Data.MySql.Mappings/.github/workflows/build-and-deploy.yml +++ b/Api.Data.MySql.Mappings/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.MySql.Mappings IMAGE_NAME: api.data.mysql.mappings @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-mysql-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_USER }};Pwd=${{ env.DATA_PASSWORD }};SslMode=Preferred; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_ADMIN_USER }};Pwd=${{ env.DATA_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,70 +122,90 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:DATA_USER');" $env:DATA_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:DATA_USER'@'%' IDENTIFIED BY '$env:DATA_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:DATA_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:DATA_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.MySql.Mappings/.kubernetes/auth-sql-secret.yaml b/Api.Data.MySql.Mappings/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.MySql.Mappings/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.MySql.Mappings/.kubernetes/deployment.yaml b/Api.Data.MySql.Mappings/.kubernetes/deployment.yaml index 560e3901..7e8688ef 100644 --- a/Api.Data.MySql.Mappings/.kubernetes/deployment.yaml +++ b/Api.Data.MySql.Mappings/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.MySql.Mappings/Api.Data.MySql.Mappings.sln b/Api.Data.MySql.Mappings/Api.Data.MySql.Mappings.sln index 365c8723..30021fd7 100644 --- a/Api.Data.MySql.Mappings/Api.Data.MySql.Mappings.sln +++ b/Api.Data.MySql.Mappings/Api.Data.MySql.Mappings.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.MySql.Mappings/README.md b/Api.Data.MySql.Mappings/README.md index 9f604cf4..90542d3d 100644 --- a/Api.Data.MySql.Mappings/README.md +++ b/Api.Data.MySql.Mappings/README.md @@ -27,4 +27,4 @@ used for case-insensitive searches, with the LINQ query calling `.ToUpper()` on Last, a unique index has also been added to `Example.NameNormalized`. Observe how Nano renames the index prefixing with 'UX_'. -> 📖 Learn more about **[Nano.Data.MySql](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.MySql)**. +> 📖 Learn more about **[Nano.Data.MySql](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.MySql/README.md#nanodatamysql)**. diff --git a/Api.Data.MySql.Spatial/.github/workflows/build-and-deploy.yml b/Api.Data.MySql.Spatial/.github/workflows/build-and-deploy.yml index c1658a02..956aec17 100644 --- a/Api.Data.MySql.Spatial/.github/workflows/build-and-deploy.yml +++ b/Api.Data.MySql.Spatial/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.MySql.Spatial IMAGE_NAME: api.data.mysql.spatial @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-mysql-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_USER }};Pwd=${{ env.DATA_PASSWORD }};SslMode=Preferred; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_ADMIN_USER }};Pwd=${{ env.DATA_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,70 +122,90 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:DATA_USER');" $env:DATA_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:DATA_USER'@'%' IDENTIFIED BY '$env:DATA_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:DATA_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:DATA_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.MySql.Spatial/.kubernetes/auth-sql-secret.yaml b/Api.Data.MySql.Spatial/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.MySql.Spatial/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.MySql.Spatial/.kubernetes/deployment.yaml b/Api.Data.MySql.Spatial/.kubernetes/deployment.yaml index 560e3901..7e8688ef 100644 --- a/Api.Data.MySql.Spatial/.kubernetes/deployment.yaml +++ b/Api.Data.MySql.Spatial/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.MySql.Spatial/Api.Data.MySql.Spatial.sln b/Api.Data.MySql.Spatial/Api.Data.MySql.Spatial.sln index 37592f84..c213021a 100644 --- a/Api.Data.MySql.Spatial/Api.Data.MySql.Spatial.sln +++ b/Api.Data.MySql.Spatial/Api.Data.MySql.Spatial.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.MySql.Spatial/README.md b/Api.Data.MySql.Spatial/README.md index 14b9a5ef..49b8f797 100644 --- a/Api.Data.MySql.Spatial/README.md +++ b/Api.Data.MySql.Spatial/README.md @@ -22,4 +22,4 @@ showcase spatial types; full controllers are unnecessary. The `Example` entity now includes a `Point` property from `NetTopologySuite`. A query criterion has been added to check whether points are within a 10,000 meter distance. The entity mappings for this spatial property have also been configured. Otherwise, no other changes were made. -> 📖 Learn more about **[Nano.Data.MySql](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.MySql)**. +> 📖 Learn more about **[Nano.Data.MySql](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.MySql/README.md#nanodatamysql)**. diff --git a/Api.Data.MySql.StoredProcedures/.github/workflows/build-and-deploy.yml b/Api.Data.MySql.StoredProcedures/.github/workflows/build-and-deploy.yml index 7d9126d4..d7056b63 100644 --- a/Api.Data.MySql.StoredProcedures/.github/workflows/build-and-deploy.yml +++ b/Api.Data.MySql.StoredProcedures/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.MySql.StoredProcedures IMAGE_NAME: api.data.mysql.storedprocedures @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-mysql-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_USER }};Pwd=${{ env.DATA_PASSWORD }};SslMode=Preferred; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_ADMIN_USER }};Pwd=${{ env.DATA_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,70 +122,90 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:DATA_USER');" $env:DATA_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:DATA_USER'@'%' IDENTIFIED BY '$env:DATA_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:DATA_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:DATA_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.MySql.StoredProcedures/.kubernetes/auth-sql-secret.yaml b/Api.Data.MySql.StoredProcedures/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.MySql.StoredProcedures/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.MySql.StoredProcedures/.kubernetes/deployment.yaml b/Api.Data.MySql.StoredProcedures/.kubernetes/deployment.yaml index 560e3901..7e8688ef 100644 --- a/Api.Data.MySql.StoredProcedures/.kubernetes/deployment.yaml +++ b/Api.Data.MySql.StoredProcedures/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.MySql.StoredProcedures/Api.Data.MySql.StoredProcedures.sln b/Api.Data.MySql.StoredProcedures/Api.Data.MySql.StoredProcedures.sln index 49bb7bb8..2a061f2a 100644 --- a/Api.Data.MySql.StoredProcedures/Api.Data.MySql.StoredProcedures.sln +++ b/Api.Data.MySql.StoredProcedures/Api.Data.MySql.StoredProcedures.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.MySql.StoredProcedures/README.md b/Api.Data.MySql.StoredProcedures/README.md index a0c7850a..3fec788c 100644 --- a/Api.Data.MySql.StoredProcedures/README.md +++ b/Api.Data.MySql.StoredProcedures/README.md @@ -35,4 +35,4 @@ The following endpoint is available for testing: | ------------------------------------------------------ | ------------------------------------------------------------------------------------------ | | `http://localhost:8080/api/examples/stored-procedure` | Returns a simple `200 OK` response with the result of the stored procedure as response. | -> 📖 Learn more about **[Nano.Data.MySql](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.MySql)**. +> 📖 Learn more about **[Nano.Data.MySql](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.MySql/README.md#nanodatamysql)**. diff --git a/Api.Data.MySql.Views/.github/workflows/build-and-deploy.yml b/Api.Data.MySql.Views/.github/workflows/build-and-deploy.yml index 84724148..2466d7bf 100644 --- a/Api.Data.MySql.Views/.github/workflows/build-and-deploy.yml +++ b/Api.Data.MySql.Views/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.MySql.Views IMAGE_NAME: api.data.mysql.views @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-mysql-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_USER }};Pwd=${{ env.DATA_PASSWORD }};SslMode=Preferred; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_ADMIN_USER }};Pwd=${{ env.DATA_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,70 +122,90 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:DATA_USER');" $env:DATA_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:DATA_USER'@'%' IDENTIFIED BY '$env:DATA_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:DATA_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:DATA_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.MySql.Views/.kubernetes/auth-sql-secret.yaml b/Api.Data.MySql.Views/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.MySql.Views/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.MySql.Views/.kubernetes/deployment.yaml b/Api.Data.MySql.Views/.kubernetes/deployment.yaml index 560e3901..7e8688ef 100644 --- a/Api.Data.MySql.Views/.kubernetes/deployment.yaml +++ b/Api.Data.MySql.Views/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.MySql.Views/Api.Data.MySql.Views.sln b/Api.Data.MySql.Views/Api.Data.MySql.Views.sln index 1233c8be..eb10227b 100644 --- a/Api.Data.MySql.Views/Api.Data.MySql.Views.sln +++ b/Api.Data.MySql.Views/Api.Data.MySql.Views.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.MySql.Views/README.md b/Api.Data.MySql.Views/README.md index 73f0e7a6..6706eeea 100644 --- a/Api.Data.MySql.Views/README.md +++ b/Api.Data.MySql.Views/README.md @@ -29,4 +29,4 @@ migrationBuilder Also, an `ExampleViewsController` (deriving from `BaseEntityViewController`) has been added, exposing query actions for the view. -> 📖 Learn more about **[Nano.Data.MySql](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.MySql)**. +> 📖 Learn more about **[Nano.Data.MySql](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.MySql/README.md#nanodatamysql)**. diff --git a/Api.Data.MySql/.github/workflows/build-and-deploy.yml b/Api.Data.MySql/.github/workflows/build-and-deploy.yml index 7731f00f..cba9bb65 100644 --- a/Api.Data.MySql/.github/workflows/build-and-deploy.yml +++ b/Api.Data.MySql/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.MySql IMAGE_NAME: api.data.mysql @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-mysql-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_USER }};Pwd=${{ env.DATA_PASSWORD }};SslMode=Preferred; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_ADMIN_USER }};Pwd=${{ env.DATA_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,70 +122,90 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:DATA_USER');" $env:DATA_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:DATA_USER'@'%' IDENTIFIED BY '$env:DATA_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:DATA_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:DATA_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.MySql/.kubernetes/auth-sql-secret.yaml b/Api.Data.MySql/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.MySql/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.MySql/.kubernetes/deployment.yaml b/Api.Data.MySql/.kubernetes/deployment.yaml index 560e3901..7e8688ef 100644 --- a/Api.Data.MySql/.kubernetes/deployment.yaml +++ b/Api.Data.MySql/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.MySql/Api.Data.MySql.sln b/Api.Data.MySql/Api.Data.MySql.sln index 60556ddd..7841d1e5 100644 --- a/Api.Data.MySql/Api.Data.MySql.sln +++ b/Api.Data.MySql/Api.Data.MySql.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.MySql/README.md b/Api.Data.MySql/README.md index 2459fc1d..73a6b67a 100644 --- a/Api.Data.MySql/README.md +++ b/Api.Data.MySql/README.md @@ -25,26 +25,26 @@ This application builds on **[Api.Blank](https://github.com/Nano-Core/Nano.Lesso the Nano `BaseEntityControllerr`. The available entity endpoints are inherited, and no additional endpoints has been added. This example demonstrates how various parts of Nano data work together. All data configuration and registration have been completed, and classes have been implemented -for the data parts, including **[Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-models)**, **[Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-mappings)**, -and the **[Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-context)**. +for the data parts, including **[Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-models)**, **[Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-mappings)**, +and the **[Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-context)**. -Additionally, the example shows how Nano **[Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#repositories)** works along with the corresponding -entity controllers. For more information on controllers and how they are connected with entity models, see **[Nano Entity Controllers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#controllers)**. +Additionally, the example shows how Nano **[Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#repositories)** works along with the corresponding +entity controllers. For more information on controllers and how they are connected with entity models, see **[Nano Entity Controllers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#controllers)**. A data health check is configured to target the database. Open **[http://localhost:8080/healthz](http://localhost:8080/healthz)** to view the health-check status in the JSON response. -> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#health-checks)**. +> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#health-checks)**. Also, API documentation has been configured, in order to easier see which endpoints are available. It can be accessed here: **[http://localhost:8080/docs](http://localhost:8080/docs)**. -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. Additionally, controllers have been implemented to demonstrate controllers for creatable, updatable, creatable-and-updatable, and deletable entities. When viewing the API documentation, observe how the available endpoints differ depending on the capabilities supported by each controller. -> 📖 Learn more about **[Nano.Data.MySql](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.MySql)**. +> 📖 Learn more about **[Nano.Data.MySql](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.MySql/README.md#nanodatamysql)**. ## Registration The following data provider has been registered using `ConfigureServices(...)` in `program.cs`. @@ -136,7 +136,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring ``` @@ -145,52 +145,45 @@ Add the following environment variables to the `buid-and-deply.yml`. ```yaml env: - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-mysql-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_USER }};Pwd=${{ env.DATA_PASSWORD }};SslMode=Preferred; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_ADMIN_USER }};Pwd=${{ env.DATA_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} ``` Additionally, this step has been added to ensure database migrations are applied, and the application database user has been created before the application is deployed. ```yaml -- name: Database Migration +- name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING" `; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - - sudo apt-get update - sudo apt-get install -y mysql-client + + apt-get update; + apt-get install -y mysql-client; - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:DATA_USER');" $env:DATA_MIGRATION_CONNECTIONSTRING; + $userExists = mysql --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');" $env:SQL_MIGRATION_CONNECTIONSTRING; if ($userExists -eq 0) { mysql --connect-expired-password -e " ` - CREATE USER '$env:DATA_USER'@'%' IDENTIFIED BY '$env:DATA_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:DATA_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:DATA_MIGRATION_CONNECTIONSTRING + CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; ` + GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:SQL_USER'@'%'; ` + FLUSH PRIVILEGES;" $env:SQL_MIGRATION_CONNECTIONSTRING; } ``` -Last, the application connectionstring must be added in a secret in Kuberntes. The `Kubernetes Deploy` step has been updated with the following. - -```yaml -sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; -if ($LastExitCode -ne 0) -{ - throw "error"; -}; -``` +Last, an additional template has been added to the deployment for storing the application connectionstring in a Kuberntes secret. diff --git a/Api.Data.PostgreSQL.Spatial/.github/workflows/build-and-deploy.yml b/Api.Data.PostgreSQL.Spatial/.github/workflows/build-and-deploy.yml index f4e23d90..070e0c51 100644 --- a/Api.Data.PostgreSQL.Spatial/.github/workflows/build-and-deploy.yml +++ b/Api.Data.PostgreSQL.Spatial/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.PostgreSQL.Spatial IMAGE_NAME: api.data.postgresql.spatial @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_HOST || secrets.STAGING_POSTGRE_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-postgre-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_NANO_DB_PASSWORD || secrets.STAGING_POSTGRE_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_ADMIN_USER || secrets.STAGING_POSTGRE_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_ADMIN_PASSWORD || secrets.STAGING_POSTGRE_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Host=${{ env.DATA_HOST }};Port=${{ vars.DATA_POSTGRE_PORT }};Database=${{ env.DATA_NAME }};Username=${{ env.DATA_USER }};Password=${{ env.DATA_PASSWORD }};SSL Mode=Prefer;Trust Server Certificate=true - DATA_MIGRATION_CONNECTIONSTRING: Host=${{ env.DATA_HOST }};Port=${{ vars.DATA_POSTGRE_PORT }};Database=${{ env.DATA_NAME }};Username=${{ env.DATA_ADMIN_USER }};Password=${{ env.DATA_ADMIN_PASSWORD }};SSL Mode=Prefer;Trust Server Certificate=true + SQL_NAME: nanoDb + SQL_USER: api-data-postgres-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,87 +122,98 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; + $env:SQL_PORT = "5432"; + $env:SQL_ADMIN_USER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].username" -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;SSL Mode=Prefer;Trust Server Certificate=true"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING" `; if ($LastExitCode -ne 0) { throw "error"; }; - - sudo apt-get update - sudo apt-get install -y postgresql-client + + apt-get update + apt-get install -y postgresql-client - $userExists = psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=postgres" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:DATA_USER';" + $userExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" if ($userExists -ne "1") { - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=postgres" ` - -c "CREATE ROLE $env:DATA_USER WITH LOGIN PASSWORD '$env:DATA_PASSWORD';" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "CREATE ROLE $env:SQL_USER WITH LOGIN PASSWORD '$env:SQL_PASSWORD';" } - $userDbExists = psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:DATA_USER';" + $userDbExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" if ($userDbExists -ne "1") { - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT CONNECT ON DATABASE $env:DATA_NAME TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT CONNECT ON DATABASE $env:SQL_NAME TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT USAGE ON SCHEMA public TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT USAGE ON SCHEMA public TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:SQL_USER;" } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:MYSQL_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_USER;Password=$env:SQL_PASSWORD;SSL Mode=Prefer;Trust Server Certificate=true"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.PostgreSQL.Spatial/.kubernetes/auth-sql-secret.yaml b/Api.Data.PostgreSQL.Spatial/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.PostgreSQL.Spatial/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.PostgreSQL.Spatial/.kubernetes/deployment.yaml b/Api.Data.PostgreSQL.Spatial/.kubernetes/deployment.yaml index 560e3901..7e8688ef 100644 --- a/Api.Data.PostgreSQL.Spatial/.kubernetes/deployment.yaml +++ b/Api.Data.PostgreSQL.Spatial/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.PostgreSQL.Spatial/Api.Data.PostgreSQL.Spatial.sln b/Api.Data.PostgreSQL.Spatial/Api.Data.PostgreSQL.Spatial.sln index 28698abc..ca8b0c06 100644 --- a/Api.Data.PostgreSQL.Spatial/Api.Data.PostgreSQL.Spatial.sln +++ b/Api.Data.PostgreSQL.Spatial/Api.Data.PostgreSQL.Spatial.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.PostgreSQL.Spatial/README.md b/Api.Data.PostgreSQL.Spatial/README.md index 70e5fd81..457e15ca 100644 --- a/Api.Data.PostgreSQL.Spatial/README.md +++ b/Api.Data.PostgreSQL.Spatial/README.md @@ -22,4 +22,4 @@ showcase spatial types; full controllers are unnecessary. The `Example` entity now includes a `Point` property from `NetTopologySuite`. A query criterion has been added to check whether points are within a 10,000 meter distance. The entity mappings for this spatial property have also been configured. Otherwise, no other changes were made. -> 📖 Learn more about **[Nano.Data.PostgreSQL](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.PostgreSQL)**. +> 📖 Learn more about **[Nano.Data.PostgreSQL](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.PostgreSQL/README.md#nanodatamysql)**. diff --git a/Api.Data.PostgreSQL/.github/workflows/build-and-deploy.yml b/Api.Data.PostgreSQL/.github/workflows/build-and-deploy.yml index b47da275..2e82e0f1 100644 --- a/Api.Data.PostgreSQL/.github/workflows/build-and-deploy.yml +++ b/Api.Data.PostgreSQL/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.PostgreSQL IMAGE_NAME: api.data.postgresql @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_HOST || secrets.STAGING_POSTGRE_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-postgre-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_NANO_DB_PASSWORD || secrets.STAGING_POSTGRE_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_ADMIN_USER || secrets.STAGING_POSTGRE_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_ADMIN_PASSWORD || secrets.STAGING_POSTGRE_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Host=${{ env.DATA_HOST }};Port=${{ vars.DATA_POSTGRE_PORT }};Database=${{ env.DATA_NAME }};Username=${{ env.DATA_USER }};Password=${{ env.DATA_PASSWORD }};SSL Mode=Prefer;Trust Server Certificate=true - DATA_MIGRATION_CONNECTIONSTRING: Host=${{ env.DATA_HOST }};Port=${{ vars.DATA_POSTGRE_PORT }};Database=${{ env.DATA_NAME }};Username=${{ env.DATA_ADMIN_USER }};Password=${{ env.DATA_ADMIN_PASSWORD }};SSL Mode=Prefer;Trust Server Certificate=true + SQL_NAME: nanoDb + SQL_USER: api-data-postgres-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,87 +122,98 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; + $env:SQL_PORT = "5432"; + $env:SQL_ADMIN_USER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].username" -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;SSL Mode=Prefer;Trust Server Certificate=true"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING" `; if ($LastExitCode -ne 0) { throw "error"; }; - - sudo apt-get update - sudo apt-get install -y postgresql-client + + apt-get update + apt-get install -y postgresql-client - $userExists = psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=postgres" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:DATA_USER';" + $userExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" if ($userExists -ne "1") { - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=postgres" ` - -c "CREATE ROLE $env:DATA_USER WITH LOGIN PASSWORD '$env:DATA_PASSWORD';" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "CREATE ROLE $env:SQL_USER WITH LOGIN PASSWORD '$env:SQL_PASSWORD';" } - $userDbExists = psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:DATA_USER';" + $userDbExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" if ($userDbExists -ne "1") { - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT CONNECT ON DATABASE $env:DATA_NAME TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT CONNECT ON DATABASE $env:SQL_NAME TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT USAGE ON SCHEMA public TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT USAGE ON SCHEMA public TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:SQL_USER;" } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:MYSQL_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_USER;Password=$env:SQL_PASSWORD;SSL Mode=Prefer;Trust Server Certificate=true"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.PostgreSQL/.kubernetes/auth-sql-secret.yaml b/Api.Data.PostgreSQL/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.PostgreSQL/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.PostgreSQL/.kubernetes/deployment.yaml b/Api.Data.PostgreSQL/.kubernetes/deployment.yaml index 560e3901..7e8688ef 100644 --- a/Api.Data.PostgreSQL/.kubernetes/deployment.yaml +++ b/Api.Data.PostgreSQL/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.PostgreSQL/Api.Data.PostgreSQL.sln b/Api.Data.PostgreSQL/Api.Data.PostgreSQL.sln index 2aec4ee1..6334c057 100644 --- a/Api.Data.PostgreSQL/Api.Data.PostgreSQL.sln +++ b/Api.Data.PostgreSQL/Api.Data.PostgreSQL.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.PostgreSQL/Api.Data.PostgreSQL/appsettings.json b/Api.Data.PostgreSQL/Api.Data.PostgreSQL/appsettings.json index 824b3ae1..2871c661 100644 --- a/Api.Data.PostgreSQL/Api.Data.PostgreSQL/appsettings.json +++ b/Api.Data.PostgreSQL/Api.Data.PostgreSQL/appsettings.json @@ -10,9 +10,6 @@ } }, "HealthCheck": { - "EvaluationInterval": 10, - "FailureNotificationInterval": 60, - "MaximumHistoryEntriesPerEndpoint": 50 }, "Documentation": { "Name": "Application" diff --git a/Api.Data.PostgreSQL/README.md b/Api.Data.PostgreSQL/README.md index a1cc16f0..982e730e 100644 --- a/Api.Data.PostgreSQL/README.md +++ b/Api.Data.PostgreSQL/README.md @@ -25,26 +25,26 @@ This application builds on **[Api.Blank](https://github.com/Nano-Core/Nano.Lesso the Nano `BaseEntityControllerr`. The available entity endpoints are inherited, and no additional endpoints has been added. This example demonstrates how various parts of Nano data work together. All data configuration and registration have been completed, and classes have been implemented -for the data parts, including [Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-models), [Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-mappings), -and the [Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-context). +for the data parts, including **[Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-models)**, **[Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-mappings)**, +and the **[Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-context)**. -Additionally, the example shows how Nano [Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#repositories) works along with the corresponding -entity controllers. For more information on controllers and how they are connected with entity models, see [Nano Entity Controllers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#controllers). +Additionally, the example shows how Nano [Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#repositories) works along with the corresponding +entity controllers. For more information on controllers and how they are connected with entity models, see **[Nano Entity Controllers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#controllers)**. A data health check is configured to target the database. Open **[http://localhost:8080/healthz](http://localhost:8080/healthz)** to view the health-check status in the JSON response. -> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#health-checks)**. +> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#health-checks)**. Also, API documentation has been configured, in order to easier see which endpoints are available. It can be accessed here: **[http://localhost:8080/docs](http://localhost:8080/docs)**. -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. Additionally, controllers have been implemented to demonstrate controllers for creatable, updatable, creatable-and-updatable, and deletable entities. When viewing the API documentation, observe how the available endpoints differ depending on the capabilities supported by each controller. -> 📖 Learn more about **[Nano.Data.PostgreSQL](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.PostgreSQL)**. +> 📖 Learn more about **[Nano.Data.PostgreSQL](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.PostgreSQL/README.md#nanodatamysql)**. ## Registration The following data provider has been registered using `ConfigureServices(...)` in `program.cs`. @@ -134,7 +134,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring ``` @@ -143,69 +143,62 @@ Add the following environment variables to the `buid-and-deply.yml`. ```yaml env: - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_HOST || secrets.STAGING_POSTGRE_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-postgre-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_NANO_DB_PASSWORD || secrets.STAGING_POSTGRE_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_ADMIN_USER || secrets.STAGING_POSTGRE_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_ADMIN_PASSWORD || secrets.STAGING_POSTGRE_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Host=${{ env.DATA_HOST }};Port=${{ vars.DATA_POSTGRE_PORT }};Database=${{ env.DATA_NAME }};Username=${{ env.DATA_USER }};Password=${{ env.DATA_PASSWORD }};SSL Mode=Prefer;Trust Server Certificate=true - DATA_MIGRATION_CONNECTIONSTRING: Host=${{ env.DATA_HOST }};Port=${{ vars.DATA_POSTGRE_PORT }};Database=${{ env.DATA_NAME }};Username=${{ env.DATA_ADMIN_USER }};Password=${{ env.DATA_ADMIN_PASSWORD }};SSL Mode=Prefer;Trust Server Certificate=true + SQL_NAME: nanoDb + SQL_USER: api-data-postgres-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} ``` Additionally, this step has been added to ensure database migrations are applied, and the application database user has been created before the application is deployed. ```yaml -- name: Database Migration +- name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; + $env:SQL_PORT = "5432"; + $env:SQL_ADMIN_USER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].username" -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;SSL Mode=Prefer;Trust Server Certificate=true"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING" `; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING" `; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y postgresql-client + apt-get update + apt-get install -y postgresql-client - $userExists = psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=postgres" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:DATA_USER';" + $userExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" if ($userExists -ne "1") { - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=postgres" ` - -c "CREATE ROLE $env:DATA_USER WITH LOGIN PASSWORD '$env:DATA_PASSWORD';" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "CREATE ROLE $env:SQL_USER WITH LOGIN PASSWORD '$env:SQL_PASSWORD';" } - $userDbExists = psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:DATA_USER';" + $userDbExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" if ($userDbExists -ne "1") { - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT CONNECT ON DATABASE $env:DATA_NAME TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT CONNECT ON DATABASE $env:SQL_NAME TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT USAGE ON SCHEMA public TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT USAGE ON SCHEMA public TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:SQL_USER;" } ``` -Last, the application connectionstring must be added in a secret in Kuberntes. The `Kubernetes Deploy` step has been updated with the following. - -```yaml -sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; -if ($LastExitCode -ne 0) -{ - throw "error"; -}; -``` +Last, an additional template has been added to the deployment for storing the application connectionstring in a Kuberntes secret. diff --git a/Api.Data.Repository.AutoSave/.github/workflows/build-and-deploy.yml b/Api.Data.Repository.AutoSave/.github/workflows/build-and-deploy.yml index 0548cc4a..cac9c331 100644 --- a/Api.Data.Repository.AutoSave/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Repository.AutoSave/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.Repository.AutoSave IMAGE_NAME: api.data.repository.autosave @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - MYSQL_DATABASE_NAME: nanoDb - MYSQL_DATABASE_USER: api-data-mysql-user - MYSQL_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - MYSQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - MYSQL_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - MYSQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - MYSQL_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_DATABASE_USER }};Pwd=${{ env.MYSQL_SERVICE_PASSWORD }};SslMode=Preferred; - MYSQL_MIGRATION_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_ADMIN_USER }};Pwd=${{ env.MYSQL_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,70 +122,90 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:MYSQL_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:MYSQL_DATABASE_USER');" $env:MYSQL_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:MYSQL_DATABASE_USER'@'%' IDENTIFIED BY '$env:MYSQL_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:MYSQL_DATABASE_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:MYSQL_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-secret ` --from-literal=data-connectionstring=$env:MYSQL_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.Repository.AutoSave/.kubernetes/auth-sql-secret.yaml b/Api.Data.Repository.AutoSave/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.Repository.AutoSave/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.Repository.AutoSave/.kubernetes/deployment.yaml b/Api.Data.Repository.AutoSave/.kubernetes/deployment.yaml index 5901ef66..7e8688ef 100644 --- a/Api.Data.Repository.AutoSave/.kubernetes/deployment.yaml +++ b/Api.Data.Repository.AutoSave/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.Repository.AutoSave/Api.Data.Repository.AutoSave.sln b/Api.Data.Repository.AutoSave/Api.Data.Repository.AutoSave.sln index 6a8de69c..13a77dea 100644 --- a/Api.Data.Repository.AutoSave/Api.Data.Repository.AutoSave.sln +++ b/Api.Data.Repository.AutoSave/Api.Data.Repository.AutoSave.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.Repository.AutoSave/README.md b/Api.Data.Repository.AutoSave/README.md index 2d1998da..cdd418dd 100644 --- a/Api.Data.Repository.AutoSave/README.md +++ b/Api.Data.Repository.AutoSave/README.md @@ -30,7 +30,7 @@ The following endpoint is available for testing. | ---------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `http://localhost:8080/api/examples/no-save` | Returns a simple `200 OK` response, while trying to add a new `Example`, changes are never saved. | -> 📖 Learn more about **[Nano Data Repository Autosave](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#autosave)**. +> 📖 Learn more about **[Nano Data Repository Autosave](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#autosave)**. ## Configuration Configured the application with the necessary data setup. diff --git a/Api.Data.Repository.Includes/.github/workflows/build-and-deploy.yml b/Api.Data.Repository.Includes/.github/workflows/build-and-deploy.yml index f931bd34..8638dea7 100644 --- a/Api.Data.Repository.Includes/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Repository.Includes/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.Repository.Includes IMAGE_NAME: api.data.repository.includes @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - MYSQL_DATABASE_NAME: nanoDb - MYSQL_DATABASE_USER: api-data-mysql-user - MYSQL_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - MYSQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - MYSQL_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - MYSQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - MYSQL_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_DATABASE_USER }};Pwd=${{ env.MYSQL_SERVICE_PASSWORD }};SslMode=Preferred; - MYSQL_MIGRATION_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_ADMIN_USER }};Pwd=${{ env.MYSQL_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,70 +122,90 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:MYSQL_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:MYSQL_DATABASE_USER');" $env:MYSQL_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:MYSQL_DATABASE_USER'@'%' IDENTIFIED BY '$env:MYSQL_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:MYSQL_DATABASE_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:MYSQL_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-secret ` --from-literal=data-connectionstring=$env:MYSQL_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.Repository.Includes/.kubernetes/auth-sql-secret.yaml b/Api.Data.Repository.Includes/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.Repository.Includes/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.Repository.Includes/.kubernetes/deployment.yaml b/Api.Data.Repository.Includes/.kubernetes/deployment.yaml index 5901ef66..7e8688ef 100644 --- a/Api.Data.Repository.Includes/.kubernetes/deployment.yaml +++ b/Api.Data.Repository.Includes/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.Repository.Includes/Api.Data.Repository.Includes.sln b/Api.Data.Repository.Includes/Api.Data.Repository.Includes.sln index 61bcc364..dc095e67 100644 --- a/Api.Data.Repository.Includes/Api.Data.Repository.Includes.sln +++ b/Api.Data.Repository.Includes/Api.Data.Repository.Includes.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.Repository.Includes/README.md b/Api.Data.Repository.Includes/README.md index cf9fb5d2..271b41f4 100644 --- a/Api.Data.Repository.Includes/README.md +++ b/Api.Data.Repository.Includes/README.md @@ -25,7 +25,7 @@ and observe how the returned object graph changes as Nano resolves deeper levels All the navigations in the object graph is annotated with `IncludeAttribute`, except for `Customer.Profile`. Because of this, it is not exposed during response serialization, even that the instance is already loaded in the data context. Only properties explicitly marked for inclusion are serialized in the response. You -can read more about this behavior in the [Response Serialization](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#response-serialization) section. +can read more about this behavior in the [Response Serialization](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#response-serialization) section. Observe how includes and nested includes appear in the response after the entities have been created and subsequently retrieved using `IRepository.GetAsync(...)`. This demonstrates how Nano automatically resolves and serializes the configured include graph according to the effective depth. @@ -41,7 +41,7 @@ The following endpoint is available for testing. | `http://localhost:8080/api/examples/create-and-include` | Returns a simple `200 OK` response. Creates a `Customer` entity and nested included navigation properties, and returns it. ⚠️ If request `includeDepth` is lower than configuration, serialization still exposes the depth using the confoguration. | | `http://localhost:8080/api/examples/not-include` | Returns a simple `200 OK` response with `CustomerResponse`, that is not `IEntity`, and all properties are serialzied and exposed. | -> 📖 Learn more about **[Nano Include Annotation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#include-annotation)**. +> 📖 Learn more about **[Nano Include Annotation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#include-annotation)**. ## Configuration Configured the application with the necessary data setup. diff --git a/Api.Data.SoftDelete/.github/workflows/build-and-deploy.yml b/Api.Data.SoftDelete/.github/workflows/build-and-deploy.yml index 8633fdfb..957cec57 100644 --- a/Api.Data.SoftDelete/.github/workflows/build-and-deploy.yml +++ b/Api.Data.SoftDelete/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.SoftDelete IMAGE_NAME: api.data.softdelete @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - MYSQL_DATABASE_NAME: nanoDb - MYSQL_DATABASE_USER: api-data-mysql-user - MYSQL_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - MYSQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - MYSQL_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - MYSQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - MYSQL_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_DATABASE_USER }};Pwd=${{ env.MYSQL_SERVICE_PASSWORD }};SslMode=Preferred; - MYSQL_MIGRATION_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_ADMIN_USER }};Pwd=${{ env.MYSQL_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,70 +122,90 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:MYSQL_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:MYSQL_DATABASE_USER');" $env:MYSQL_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:MYSQL_DATABASE_USER'@'%' IDENTIFIED BY '$env:MYSQL_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:MYSQL_DATABASE_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:MYSQL_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-secret ` --from-literal=data-connectionstring=$env:MYSQL_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.SoftDelete/.kubernetes/auth-sql-secret.yaml b/Api.Data.SoftDelete/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.SoftDelete/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.SoftDelete/.kubernetes/deployment.yaml b/Api.Data.SoftDelete/.kubernetes/deployment.yaml index 5901ef66..7e8688ef 100644 --- a/Api.Data.SoftDelete/.kubernetes/deployment.yaml +++ b/Api.Data.SoftDelete/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.SoftDelete/Api.Data.SoftDelete.sln b/Api.Data.SoftDelete/Api.Data.SoftDelete.sln index 07ee2aa8..6bc947ce 100644 --- a/Api.Data.SoftDelete/Api.Data.SoftDelete.sln +++ b/Api.Data.SoftDelete/Api.Data.SoftDelete.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.SoftDelete/README.md b/Api.Data.SoftDelete/README.md index 1a984ed0..4203d52b 100644 --- a/Api.Data.SoftDelete/README.md +++ b/Api.Data.SoftDelete/README.md @@ -26,4 +26,4 @@ The data mapping also includes two triggers for `OnDeleting` and `OnDeleted` to Open the database and notice that the created `Example` entity has a non-zero `IsDeleted` value, indicating it has been soft-deleted. -> 📖 Learn more about **[Nano Data Soft Delete](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#soft-delete)**. +> 📖 Learn more about **[Nano Data Soft Delete](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#soft-delete)**. diff --git a/Api.Data.SqLite/.docker/docker-compose.yml b/Api.Data.SqLite/.docker/docker-compose.yml index 53243c0b..28072671 100644 --- a/Api.Data.SqLite/.docker/docker-compose.yml +++ b/Api.Data.SqLite/.docker/docker-compose.yml @@ -1,16 +1,15 @@ services: api.data.sqlite: - image: api.data.postgresql.spatial - hostname: api-data-postgresql-spatial + image: api.data.sqlite + hostname: api-data-sqlite build: context: ../Api.Data.SqLite dockerfile: Dockerfile.Local - hostname: api-data-sqlite restart: on-failure ports: - 8080:8080 volumes: - - ./bin/data:/data + - ./bin/data:/mnt/data networks: - network diff --git a/Api.Data.SqLite/.github/workflows/build-and-deploy.yml b/Api.Data.SqLite/.github/workflows/build-and-deploy.yml index e008225d..98ad1455 100644 --- a/Api.Data.SqLite/.github/workflows/build-and-deploy.yml +++ b/Api.Data.SqLite/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.SqLite IMAGE_NAME: api.data.sqlite @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,28 +37,33 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - DATA_NAME: nanoDb - DATA_SIZE: 10Gi - DATA_MIGRATION_CONNECTIONSTRING: "Data Source=/data/{{ env.nanoDb }}.sqlite" + SQL_NAME: nanoDb + SQL_SIZE: 10Gi + SQL_MIGRATION_CONNECTIONSTRING: "Data Source=/mnt/data/{{ env.SQL_NAME }}.sqlite" jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -84,7 +92,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -100,9 +108,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -113,7 +121,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -125,7 +133,7 @@ jobs: dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING"; if ($LastExitCode -ne 0) { @@ -136,42 +144,42 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/data-storageclass.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/data-storageclass.tmp.yaml; - sudo kubectl apply -f .kubernetes/data-storageclass.tmp.yaml; + kubectl apply -f .kubernetes/data-storageclass.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/data-pvc.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/data-pvc.tmp.yaml; - sudo kubectl apply -f .kubernetes/data-pvc.tmp.yaml; + kubectl apply -f .kubernetes/data-pvc.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.SqLite/.kubernetes/deployment.yaml b/Api.Data.SqLite/.kubernetes/deployment.yaml index 0b6637e4..354c85fb 100644 --- a/Api.Data.SqLite/.kubernetes/deployment.yaml +++ b/Api.Data.SqLite/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -75,7 +75,7 @@ spec: timeoutSeconds: 2 volumeMounts: - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% + mountPath: /mnt/data volumes: - name: %SERVICE_NAME%-volume persistentVolumeClaim: diff --git a/Api.Data.SqLite/Api.Data.SqLite/Data/MySqlDbContextFactory.cs b/Api.Data.SqLite/Api.Data.SqLite/Data/SqLiteDbContextFactory.cs similarity index 51% rename from Api.Data.SqLite/Api.Data.SqLite/Data/MySqlDbContextFactory.cs rename to Api.Data.SqLite/Api.Data.SqLite/Data/SqLiteDbContextFactory.cs index 6545c502..ac245907 100644 --- a/Api.Data.SqLite/Api.Data.SqLite/Data/MySqlDbContextFactory.cs +++ b/Api.Data.SqLite/Api.Data.SqLite/Data/SqLiteDbContextFactory.cs @@ -4,4 +4,4 @@ namespace Api.Data.SqLite.Data; /// -public class MySqlDbContextFactory : BaseDbContextFactory; \ No newline at end of file +public class SqLiteDbContextFactory : BaseDbContextFactory; \ No newline at end of file diff --git a/Api.Data.SqLite/Api.Data.SqLite/Dockerfile.Local b/Api.Data.SqLite/Api.Data.SqLite/Dockerfile.Local index 542f0ed1..28ad6e11 100644 --- a/Api.Data.SqLite/Api.Data.SqLite/Dockerfile.Local +++ b/Api.Data.SqLite/Api.Data.SqLite/Dockerfile.Local @@ -1,10 +1,6 @@ ARG DOTNET_ASPNET_VERSION="10.0" FROM mcr.microsoft.com/dotnet/aspnet:$DOTNET_ASPNET_VERSION AS base -RUN apt-get update \ - && apt-get install -y libsqlite3-mod-spatialite \ - && apt-get install -y libspatialite-dev - EXPOSE 8080 ENTRYPOINT ["dotnet", "Api.Data.SqLite.dll"] \ No newline at end of file diff --git a/Api.Data.SqLite/Api.Data.SqLite/Migrations/20260415151010_Initial.Designer.cs b/Api.Data.SqLite/Api.Data.SqLite/Migrations/20260427141022_Initial.Designer.cs similarity index 99% rename from Api.Data.SqLite/Api.Data.SqLite/Migrations/20260415151010_Initial.Designer.cs rename to Api.Data.SqLite/Api.Data.SqLite/Migrations/20260427141022_Initial.Designer.cs index 61eabdcd..7416d3a1 100644 --- a/Api.Data.SqLite/Api.Data.SqLite/Migrations/20260415151010_Initial.Designer.cs +++ b/Api.Data.SqLite/Api.Data.SqLite/Migrations/20260427141022_Initial.Designer.cs @@ -11,14 +11,14 @@ namespace Api.Data.SqLite.Migrations { [DbContext(typeof(SqLiteDbContext))] - [Migration("20260415151010_Initial")] + [Migration("20260427141022_Initial")] partial class Initial { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); modelBuilder.Entity("Api.Data.SqLite.Models.Example", b => { diff --git a/Api.Data.SqLite/Api.Data.SqLite/Migrations/20260415151010_Initial.cs b/Api.Data.SqLite/Api.Data.SqLite/Migrations/20260427141022_Initial.cs similarity index 100% rename from Api.Data.SqLite/Api.Data.SqLite/Migrations/20260415151010_Initial.cs rename to Api.Data.SqLite/Api.Data.SqLite/Migrations/20260427141022_Initial.cs diff --git a/Api.Data.SqLite/Api.Data.SqLite/Migrations/SqLiteDbContextModelSnapshot.cs b/Api.Data.SqLite/Api.Data.SqLite/Migrations/SqLiteDbContextModelSnapshot.cs index bc3c35c9..71e2dd70 100644 --- a/Api.Data.SqLite/Api.Data.SqLite/Migrations/SqLiteDbContextModelSnapshot.cs +++ b/Api.Data.SqLite/Api.Data.SqLite/Migrations/SqLiteDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class SqLiteDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); modelBuilder.Entity("Api.Data.SqLite.Models.Example", b => { diff --git a/Api.Data.SqLite/Api.Data.SqLite/appsettings.Development.json b/Api.Data.SqLite/Api.Data.SqLite/appsettings.Development.json index 1e8d984a..8927ff99 100644 --- a/Api.Data.SqLite/Api.Data.SqLite/appsettings.Development.json +++ b/Api.Data.SqLite/Api.Data.SqLite/appsettings.Development.json @@ -1,5 +1,5 @@ { "Data": { - "StartupAction": "Migrate", + "StartupAction": "Migrate" } } \ No newline at end of file diff --git a/Api.Data.SqLite/Api.Data.SqLite/appsettings.json b/Api.Data.SqLite/Api.Data.SqLite/appsettings.json index 8fd62bfb..496df4fb 100644 --- a/Api.Data.SqLite/Api.Data.SqLite/appsettings.json +++ b/Api.Data.SqLite/Api.Data.SqLite/appsettings.json @@ -10,9 +10,6 @@ } }, "HealthCheck": { - "EvaluationInterval": 10, - "FailureNotificationInterval": 60, - "MaximumHistoryEntriesPerEndpoint": 50 }, "Documentation": { "Name": "Application" @@ -28,7 +25,7 @@ "UseSensitiveDataLogging": false, "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, - "ConnectionString": "Data Source=/data/nanoDb.sqlite", + "ConnectionString": "Data Source=/mnt/data/nanoDb.sqlite", "Repository": { "UseAutoSave": true, "QueryIncludeDepth": 4 diff --git a/Api.Data.SqLite/Dockerfile b/Api.Data.SqLite/Dockerfile index 93565b0d..8c1d942b 100644 --- a/Api.Data.SqLite/Dockerfile +++ b/Api.Data.SqLite/Dockerfile @@ -9,10 +9,6 @@ FROM mcr.microsoft.com/dotnet/aspnet:$DOTNET_ASPNET_VERSION AS base LABEL org.opencontainers.image.source=CONTAINER_REGISTRY_SOURCE_LABEL -RUN apt-get update \ - && apt-get install -y libsqlite3-mod-spatialite \ - && apt-get install -y libspatialite-dev - EXPOSE 8080 WORKDIR /app diff --git a/Api.Data.SqLite/README.md b/Api.Data.SqLite/README.md index b77374f6..0884a1cc 100644 --- a/Api.Data.SqLite/README.md +++ b/Api.Data.SqLite/README.md @@ -25,26 +25,26 @@ This application builds on **[Api.Blank](https://github.com/Nano-Core/Nano.Lesso the Nano `BaseEntityControllerr`. The available entity endpoints are inherited, and no additional endpoints has been added. This example demonstrates how various parts of Nano data work together. All data configuration and registration have been completed, and classes have been implemented -for the data parts, including [Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-models), [Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-mappings), -and the [Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-context). +for the data parts, including [Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-models), [Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-mappings), +and the [Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-context). -Additionally, the example shows how Nano [Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#repositories) works along with the corresponding -entity controllers. For more information on controllers and how they are connected with entity models, see [Nano Entity Controllers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#controllers). +Additionally, the example shows how Nano [Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#repositories) works along with the corresponding +entity controllers. For more information on controllers and how they are connected with entity models, see [Nano Entity Controllers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#controllers). A data health check is configured to target the database. Open **[http://localhost:8080/healthz](http://localhost:8080/healthz)** to view the health-check status in the JSON response. -> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#health-checks)**. +> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#health-checks)**. Also, API documentation has been configured, in order to easier see which endpoints are available. It can be accessed here: **[http://localhost:8080/docs](http://localhost:8080/docs)**. -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. Additionally, controllers have been implemented to demonstrate controllers for creatable, updatable, creatable-and-updatable, and deletable entities. When viewing the API documentation, observe how the available endpoints differ depending on the capabilities supported by each controller. -> 📖 Learn more about **[Nano.Data.SqLite](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.SqLite)**. +> 📖 Learn more about **[Nano.Data.SqLite](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.SqLite/README.md#nanodatamysql)**. ## Registration The following data provider has been registered using `ConfigureServices(...)` in `program.cs`. @@ -107,15 +107,7 @@ Added SqLite as a service dependency in `docker-compose.yml`. services: api.data.sqlite: volumes: - - ./bin/data:/data -``` - -Also the `Dockerfile` must have SqLite installed with spatial support. Add the following to both the `Dockerfile` and the `Dockerfile.Local`. - -```dockerfile -RUN apt-get update \ - && apt-get install -y libsqlite3-mod-spatialite \ - && apt-get install -y libspatialite-dev + - ./bin/data:/mnt/data ``` ## Kubernetes @@ -130,7 +122,7 @@ spec: containers: volumeMounts: - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% + mountPath: /mnt/data volumes: - name: %SERVICE_NAME%-volume persistentVolumeClaim: @@ -142,9 +134,9 @@ Add the following environment variables to the `buid-and-deply.yml`. ```yaml env: - DATA_NAME: nanoDb - DATA_SIZE: 10Gi - DATA_CONNECTIONSTRING: "Data Source=/data/{{ env.nanoDb }}.sqlite" + SQL_NAME: nanoDb + SQL_SIZE: 10Gi + SQL_CONNECTIONSTRING: "Data Source=/mnt/data/{{ env.nanoDb }}.sqlite" ``` Additionally, this step has been added to ensure database migrations are applied. @@ -156,7 +148,7 @@ Additionally, this step has been added to ensure database migrations are applied dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING" `; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING" `; if ($LastExitCode -ne 0) { diff --git a/Api.Data.SqlServer.Spatial/.github/workflows/build-and-deploy.yml b/Api.Data.SqlServer.Spatial/.github/workflows/build-and-deploy.yml index 827b1d7a..e7532a1d 100644 --- a/Api.Data.SqlServer.Spatial/.github/workflows/build-and-deploy.yml +++ b/Api.Data.SqlServer.Spatial/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.SqlServer.Spatial IMAGE_NAME: api.data.sqlserver.spatial @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_HOST || secrets.STAGING_SQLSERVER_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-sqlserver-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_NANO_DB_PASSWORD || secrets.STAGING_SQLSERVER_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_ADMIN_USER || secrets.STAGING_SQLSERVER_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_ADMIN_PASSWORD || secrets.STAGING_SQLSERVER_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }},${{ vars.DATA_SQLSERVER_PORT }};Database=${{ env.DATA_NAME }};User Id=${{ env.DATA_USER }};Password=${{ env.DATA_PASSWORD }};Encrypt=True;TrustServerCertificate=True; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }},${{ vars.DATA_SQLSERVER_PORT }};Database=${{ env.DATA_NAME }};User Id=${{ env.DATA_ADMIN_USER }};Password=${{ env.DATA_ADMIN_PASSWORD }};Encrypt=True;TrustServerCertificate=True; + SQL_NAME: nanoDb + SQL_USER: api-data-sqlserver-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,98 +122,109 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; + $env:SQL_PORT = "1433" + $env:SQL_ADMIN_USER = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].administratorLogin" -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING"; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mssql-tools unixodbc-dev + apt-get update + apt-get install -y mssql-tools unixodbc-dev $loginExists = sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` -d master ` -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:DATA_USER';" + -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:SQL_USER';" if ($loginExists -eq 0) { sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` -d master ` - -Q "CREATE LOGIN [$env:DATA_USER] WITH PASSWORD = '$env:DATA_PASSWORD';" + -Q "CREATE LOGIN [$env:SQL_USER] WITH PASSWORD = '$env:SQL_PASSWORD';" }; $userExists = sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d $env:DATA_NAME ` + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d $env:SQL_NAME ` -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:DATA_USER';" + -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:SQL_USER';" if ($userExists -eq 0) { sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d $env:DATA_NAME ` - -Q "CREATE USER [$env:DATA_USER] FOR LOGIN [$env:DATA_USER]; - ALTER ROLE db_datareader ADD MEMBER [$env:DATA_USER]; - ALTER ROLE db_datawriter ADD MEMBER [$env:DATA_USER];" + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d $env:SQL_NAME ` + -Q "CREATE USER [$env:SQL_USER] FOR LOGIN [$env:SQL_USER]; + ALTER ROLE db_datareader ADD MEMBER [$env:SQL_USER]; + ALTER ROLE db_datawriter ADD MEMBER [$env:SQL_USER];" }; + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_USER;Password=$env:SQL_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.SqlServer.Spatial/.kubernetes/auth-sql-secret.yaml b/Api.Data.SqlServer.Spatial/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.SqlServer.Spatial/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.SqlServer.Spatial/.kubernetes/deployment.yaml b/Api.Data.SqlServer.Spatial/.kubernetes/deployment.yaml index 560e3901..7e8688ef 100644 --- a/Api.Data.SqlServer.Spatial/.kubernetes/deployment.yaml +++ b/Api.Data.SqlServer.Spatial/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.SqlServer.Spatial/Api.Data.SqlServer.Spatial.sln b/Api.Data.SqlServer.Spatial/Api.Data.SqlServer.Spatial.sln index cd2ba3a3..1359bc5e 100644 --- a/Api.Data.SqlServer.Spatial/Api.Data.SqlServer.Spatial.sln +++ b/Api.Data.SqlServer.Spatial/Api.Data.SqlServer.Spatial.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.SqlServer.Spatial/Api.Data.SqlServer.Spatial/appsettings.json b/Api.Data.SqlServer.Spatial/Api.Data.SqlServer.Spatial/appsettings.json index a5c27531..b4341f5e 100644 --- a/Api.Data.SqlServer.Spatial/Api.Data.SqlServer.Spatial/appsettings.json +++ b/Api.Data.SqlServer.Spatial/Api.Data.SqlServer.Spatial/appsettings.json @@ -10,12 +10,8 @@ } }, "HealthCheck": { - "EvaluationInterval": 10, - "FailureNotificationInterval": 60, - "MaximumHistoryEntriesPerEndpoint": 50 }, "Documentation": { - "Name": "Application" } }, "Data": { diff --git a/Api.Data.SqlServer.Spatial/README.md b/Api.Data.SqlServer.Spatial/README.md index 7ed6c218..07691b06 100644 --- a/Api.Data.SqlServer.Spatial/README.md +++ b/Api.Data.SqlServer.Spatial/README.md @@ -32,4 +32,4 @@ migrationBuilder USING GEOGRAPHY_GRID"); ``` -> 📖 Learn more about **[Nano.Data.SqlServer](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.SqlServer)**. +> 📖 Learn more about **[Nano.Data.SqlServer](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.SqlServer/README.md#nanodatamysql)**. diff --git a/Api.Data.SqlServer/.github/workflows/build-and-deploy.yml b/Api.Data.SqlServer/.github/workflows/build-and-deploy.yml index 913a983e..370b8153 100644 --- a/Api.Data.SqlServer/.github/workflows/build-and-deploy.yml +++ b/Api.Data.SqlServer/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.SqlServer IMAGE_NAME: api.data.sqlserver @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_HOST || secrets.STAGING_SQLSERVER_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-sqlserver-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_NANO_DB_PASSWORD || secrets.STAGING_SQLSERVER_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_ADMIN_USER || secrets.STAGING_SQLSERVER_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_ADMIN_PASSWORD || secrets.STAGING_SQLSERVER_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }},${{ vars.DATA_SQLSERVER_PORT }};Database=${{ env.DATA_NAME }};User Id=${{ env.DATA_USER }};Password=${{ env.DATA_PASSWORD }};Encrypt=True;TrustServerCertificate=True; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }},${{ vars.DATA_SQLSERVER_PORT }};Database=${{ env.DATA_NAME }};User Id=${{ env.DATA_ADMIN_USER }};Password=${{ env.DATA_ADMIN_PASSWORD }};Encrypt=True;TrustServerCertificate=True; + SQL_NAME: nanoDb + SQL_USER: api-data-sqlserver-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,98 +122,109 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; + $env:SQL_PORT = "1433" + $env:SQL_ADMIN_USER = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].administratorLogin" -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING"; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mssql-tools unixodbc-dev + apt-get update + apt-get install -y mssql-tools unixodbc-dev $loginExists = sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` -d master ` -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:DATA_USER';" + -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:SQL_USER';" if ($loginExists -eq 0) { sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` -d master ` - -Q "CREATE LOGIN [$env:DATA_USER] WITH PASSWORD = '$env:DATA_PASSWORD';" + -Q "CREATE LOGIN [$env:SQL_USER] WITH PASSWORD = '$env:SQL_PASSWORD';" }; $userExists = sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d $env:DATA_NAME ` + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d $env:SQL_NAME ` -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:DATA_USER';" + -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:SQL_USER';" if ($userExists -eq 0) { sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d $env:DATA_NAME ` - -Q "CREATE USER [$env:DATA_USER] FOR LOGIN [$env:DATA_USER]; - ALTER ROLE db_datareader ADD MEMBER [$env:DATA_USER]; - ALTER ROLE db_datawriter ADD MEMBER [$env:DATA_USER];" + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d $env:SQL_NAME ` + -Q "CREATE USER [$env:SQL_USER] FOR LOGIN [$env:SQL_USER]; + ALTER ROLE db_datareader ADD MEMBER [$env:SQL_USER]; + ALTER ROLE db_datawriter ADD MEMBER [$env:SQL_USER];" }; + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_USER;Password=$env:SQL_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.SqlServer/.kubernetes/auth-sql-secret.yaml b/Api.Data.SqlServer/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.SqlServer/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.SqlServer/.kubernetes/deployment.yaml b/Api.Data.SqlServer/.kubernetes/deployment.yaml index 560e3901..7e8688ef 100644 --- a/Api.Data.SqlServer/.kubernetes/deployment.yaml +++ b/Api.Data.SqlServer/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.SqlServer/Api.Data.SqlServer.sln b/Api.Data.SqlServer/Api.Data.SqlServer.sln index 4c4fa9e3..ba1df312 100644 --- a/Api.Data.SqlServer/Api.Data.SqlServer.sln +++ b/Api.Data.SqlServer/Api.Data.SqlServer.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.SqlServer/Api.Data.SqlServer/appsettings.json b/Api.Data.SqlServer/Api.Data.SqlServer/appsettings.json index 824b3ae1..2871c661 100644 --- a/Api.Data.SqlServer/Api.Data.SqlServer/appsettings.json +++ b/Api.Data.SqlServer/Api.Data.SqlServer/appsettings.json @@ -10,9 +10,6 @@ } }, "HealthCheck": { - "EvaluationInterval": 10, - "FailureNotificationInterval": 60, - "MaximumHistoryEntriesPerEndpoint": 50 }, "Documentation": { "Name": "Application" diff --git a/Api.Data.SqlServer/README.md b/Api.Data.SqlServer/README.md index 3214928a..c1286d17 100644 --- a/Api.Data.SqlServer/README.md +++ b/Api.Data.SqlServer/README.md @@ -25,26 +25,26 @@ This application builds on **[Api.Blank](https://github.com/Nano-Core/Nano.Lesso the Nano `BaseEntityControllerr`. The available entity endpoints are inherited, and no additional endpoints has been added. This example demonstrates how various parts of Nano data work together. All data configuration and registration have been completed, and classes have been implemented -for the data parts, including [Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-models), [Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-mappings), -and the [Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-context). +for the data parts, including **[Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-models)**, **[Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-mappings)**, +and the **[Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-context)**. -Additionally, the example shows how Nano [Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#repositories) works along with the corresponding -entity controllers. For more information on controllers and how they are connected with entity models, see [Nano Entity Controllers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#controllers). +Additionally, the example shows how Nano **[Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#repositories)** works along with the corresponding +entity controllers. For more information on controllers and how they are connected with entity models, see **[Nano Entity Controllers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#controllers)**. A data health check is configured to target the database. Open **[http://localhost:8080/healthz](http://localhost:8080/healthz)** to view the health-check status in the JSON response. -> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#health-checks)**. +> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#health-checks)**. Also, API documentation has been configured, in order to easier see which endpoints are available. It can be accessed here: **[http://localhost:8080/docs](http://localhost:8080/docs)**. -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. Additionally, controllers have been implemented to demonstrate controllers for creatable, updatable, creatable-and-updatable, and deletable entities. When viewing the API documentation, observe how the available endpoints differ depending on the capabilities supported by each controller. -> 📖 Learn more about **[Nano.Data.SqlServer](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.SqlServer)**. +> 📖 Learn more about **[Nano.Data.SqlServer](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.SqlServer/README.md#nanodatamysql)**. ## Registration The following data provider has been registered using `ConfigureServices(...)` in `program.cs`. @@ -134,7 +134,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring ``` @@ -143,72 +143,73 @@ Add the following environment variables to the `buid-and-deply.yml`. ```yaml env: - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_HOST || secrets.STAGING_SQLSERVER_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-sqlserver-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_NANO_DB_PASSWORD || secrets.STAGING_SQLSERVER_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_ADMIN_USER || secrets.STAGING_SQLSERVER_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_ADMIN_PASSWORD || secrets.STAGING_SQLSERVER_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }},${{ vars.DATA_SQLSERVER_PORT }};Database=${{ env.DATA_NAME }};User Id=${{ env.DATA_USER }};Password=${{ env.DATA_PASSWORD }}; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }},${{ vars.DATA_SQLSERVER_PORT }};Database=${{ env.DATA_NAME }};User Id=${{ env.DATA_ADMIN_USER }};Password=${{ env.DATA_ADMIN_PASSWORD }}; + SQL_NAME: nanoDb + SQL_USER: api-data-sqlserver-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} ``` Additionally, this step has been added to ensure database migrations are applied, and the application database user has been created before the application is deployed. ```yaml -- name: Database Migration +- name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; + $env:SQL_PORT = "1433" + $env:SQL_ADMIN_USER = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].administratorLogin" -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING" `; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING"; if ($LastExitCode -ne 0) { throw "error"; }; - - sudo apt-get update - sudo apt-get install -y mssql-tools unixodbc-dev + + apt-get update + apt-get install -y mssql-tools unixodbc-dev $loginExists = sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d master ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:DATA_USER';" + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d master ` + -h -1 ` + -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:SQL_USER';" if ($loginExists -eq 0) { sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d master ` - -Q "CREATE LOGIN [$env:DATA_USER] WITH PASSWORD = '$env:DATA_PASSWORD';" - } + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d master ` + -Q "CREATE LOGIN [$env:SQL_USER] WITH PASSWORD = '$env:SQL_PASSWORD';" + }; $userExists = sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d $env:DATA_NAME ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:DATA_USER';" - - if ($userExists -eq 0) - { - sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d $env:DATA_NAME ` - -Q "CREATE USER [$env:DATA_USER] FOR LOGIN [$env:DATA_USER]; - ALTER ROLE db_datareader ADD MEMBER [$env:DATA_USER]; - ALTER ROLE db_datawriter ADD MEMBER [$env:DATA_USER];" - } + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d $env:SQL_NAME ` + -h -1 ` + -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:SQL_USER';" + + if ($userExists -eq 0) + { + sqlcmd ` + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d $env:SQL_NAME ` + -Q "CREATE USER [$env:SQL_USER] FOR LOGIN [$env:SQL_USER]; + ALTER ROLE db_datareader ADD MEMBER [$env:SQL_USER]; + ALTER ROLE db_datawriter ADD MEMBER [$env:SQL_USER];" + }; ``` Last, the application connectionstring must be added in a secret in Kuberntes. The `Kubernetes Deploy` step has been updated with the following. diff --git a/Api.Data.Triggers/.github/workflows/build-and-deploy.yml b/Api.Data.Triggers/.github/workflows/build-and-deploy.yml index f1727498..0cbf7483 100644 --- a/Api.Data.Triggers/.github/workflows/build-and-deploy.yml +++ b/Api.Data.Triggers/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Data.Triggers IMAGE_NAME: api.data.triggers @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_KUBERNETES_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -34,33 +37,34 @@ env: KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} - MYSQL_DATABASE_NAME: nanoDb - MYSQL_DATABASE_USER: api-data-mysql-user - MYSQL_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - MYSQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - MYSQL_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - MYSQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - MYSQL_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_DATABASE_USER }};Pwd=${{ env.MYSQL_SERVICE_PASSWORD }};SslMode=Preferred; - MYSQL_MIGRATION_CONNECTIONSTRING: Server=${{ env.MYSQL_HOST }};Port=${{ vars.MYSQL_PORT }};Database=${{ env.MYSQL_DATABASE_NAME }};Uid=${{ env.MYSQL_ADMIN_USER }};Pwd=${{ env.MYSQL_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -89,7 +93,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -105,9 +109,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -118,70 +122,90 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:MYSQL_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:MYSQL_DATABASE_USER');" $env:MYSQL_MIGRATION_CONNECTIONSTRING + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:MYSQL_DATABASE_USER'@'%' IDENTIFIED BY '$env:MYSQL_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:MYSQL_DATABASE_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:MYSQL_MIGRATION_CONNECTIONSTRING + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-secret ` --from-literal=data-connectionstring=$env:MYSQL_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Data.Triggers/.kubernetes/auth-sql-secret.yaml b/Api.Data.Triggers/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Api.Data.Triggers/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Api.Data.Triggers/.kubernetes/deployment.yaml b/Api.Data.Triggers/.kubernetes/deployment.yaml index 5901ef66..7e8688ef 100644 --- a/Api.Data.Triggers/.kubernetes/deployment.yaml +++ b/Api.Data.Triggers/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -41,7 +41,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: @@ -79,11 +79,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Data.Triggers/Api.Data.Triggers.sln b/Api.Data.Triggers/Api.Data.Triggers.sln index c8a7d06c..a31fb6e1 100644 --- a/Api.Data.Triggers/Api.Data.Triggers.sln +++ b/Api.Data.Triggers/Api.Data.Triggers.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml diff --git a/Api.Data.Triggers/README.md b/Api.Data.Triggers/README.md index 83ec1a31..02126d18 100644 --- a/Api.Data.Triggers/README.md +++ b/Api.Data.Triggers/README.md @@ -23,4 +23,4 @@ Triggers for `OnInserting`, `OnInserted`, `OnUpdating`, `OnUpdated`, `OnDeleting the `Example` entity is **added** or **updated**, the `Example.UpdatedAt` property is automatically set to `UtcNow`. Additionally, for each trigger execution, an `ExampleTrigger` entity is created and stored. This serves as a record demonstrating that the trigger was invoked. -> 📖 Learn more about **[Nano Data Triggers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#triggers)**. +> 📖 Learn more about **[Nano Data Triggers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#triggers)**. diff --git a/Api.Documentation.Csp/.docker/docker-compose.dcproj b/Api.Documentation.Csp/.docker/docker-compose.dcproj new file mode 100644 index 00000000..d9e8e500 --- /dev/null +++ b/Api.Documentation.Csp/.docker/docker-compose.dcproj @@ -0,0 +1,13 @@ + + + + 2.1 + Linux + false + http://localhost:{ServicePort}/docs + $(ProjectName) + + + + + \ No newline at end of file diff --git a/Api.Documentation.Nonce/.docker/docker-compose.yml b/Api.Documentation.Csp/.docker/docker-compose.yml similarity index 64% rename from Api.Documentation.Nonce/.docker/docker-compose.yml rename to Api.Documentation.Csp/.docker/docker-compose.yml index d63e7ade..ec265b69 100644 --- a/Api.Documentation.Nonce/.docker/docker-compose.yml +++ b/Api.Documentation.Csp/.docker/docker-compose.yml @@ -1,13 +1,13 @@ services: - api.documentation.nonce: - image: api.documentation.nonce - hostname: api-documentation.nonce + api.documentation.csp: + image: api.documentation.csp + hostname: api-documentation.csp restart: on-failure ports: - 8080:8080 - 4443:4443 build: - context: ../Api.Documentation.Nonce + context: ../Api.Documentation.Csp dockerfile: "Dockerfile.Local" volumes: - ../:/root/.dotnet/https diff --git a/Api.Documentation.Csp/.dockerignore b/Api.Documentation.Csp/.dockerignore new file mode 100644 index 00000000..e694ae21 --- /dev/null +++ b/Api.Documentation.Csp/.dockerignore @@ -0,0 +1,12 @@ +.dockerignore +.env +.git +.gitignore +.vs +.vscode +docker-compose.yml +docker-compose.*.yml +*/bin +*/obj +!obj/Docker/publish/* +!obj/Docker/empty/ diff --git a/Api.Documentation.Csp/.github/config/slack.yml b/Api.Documentation.Csp/.github/config/slack.yml new file mode 100644 index 00000000..3592affb --- /dev/null +++ b/Api.Documentation.Csp/.github/config/slack.yml @@ -0,0 +1,18 @@ +username: GitHub Actions +icon_url: https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png + +pretext: "{{icon jobStatus}} *<{{repositoryUrl}}|{{repositoryName}}>* <{{workflowRunUrl}}|`#{{runNumber}}`> triggered via {{eventName}} by ** for branch <{{refUrl}}|`{{ref}}`>." + +text: | + {{#if payload.commits}} + *Commits* + {{#each payload.commits}} + <{{this.url}}|`{{truncate this.id 8}}`> - {{this.message}} + {{/each}} + {{/if}} + +footer: >- + <{{repositoryUrl}}|{{repositoryName}}> #{{runNumber}} + +fallback: |- + [GitHub] {{workflow}} #{{runNumber}} {{jobName}} is {{jobStatus}} diff --git a/Api.Documentation.Csp/.github/workflows/build-and-deploy.yml b/Api.Documentation.Csp/.github/workflows/build-and-deploy.yml new file mode 100644 index 00000000..00b3b84b --- /dev/null +++ b/Api.Documentation.Csp/.github/workflows/build-and-deploy.yml @@ -0,0 +1,196 @@ +name: Build And Deploy +on: + pull_request: + branches: + - master + push: + branches: + - master +env: + APP_NAME: Api.Documentation.Csp + IMAGE_NAME: api.documentation.csp + SERVICE_NAME: api-documentation-csp + SUB_DOMAIN_NAME: nano + VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' + DOTNET_SDK_VERSION: 10.0 + DOTNET_ASPNET_VERSION: 10.0 + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} + AZURE_GROUP_DNS: ${{ vars.AZURE_RESOURCE_GROUP_DNS }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} + KUBERNETES_NODEPOOL_COMPUTE: cpu + KUBERNETES_NAMESPACE: apps + KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} + KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} + KUBERNETES_REPLICA_HISTORY_COUNT: 0 + KUBERNETES_MEMORY_REQUEST: 512Mi + KUBERNETES_MEMORY_LIMIT: 1536Mi + KUBERNETES_MEMORY_SCALING: 180 + KUBERNETES_CPU_REQUEST: 200m + KUBERNETES_CPU_LIMIT: 600m + KUBERNETES_CPU_SCALING: 180 + ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} +jobs: + build-and-deploy: + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} + permissions: + contents: write + packages: write + id-token: write + concurrency: + group: ${{ github.repository }} + cancel-in-progress: true + steps: + - uses: actions/checkout@v6 + + - name: Azure Login + shell: pwsh + run: | + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; + + - name: Build + shell: pwsh + run: | + dotnet nuget add source $env:NUGET_HOST -n private -u $env:NUGET_USERNAME -p $env:NUGET_PASSWORD --store-password-in-clear-text; + + dotnet build -c Release .\$env:APP_NAME.sln; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + - name: Test + shell: pwsh + run: | + dotnet test .\.tests\Tests.$env:APP_NAME\Tests.$env:APP_NAME.csproj; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + - name: Publish Image + shell: pwsh + run: | + $registryHost = $env:CONTAINER_REGISTRY_HOST.ToLower() + $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; + $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION + + docker build ` + -t $imageLatestTag ` + -t $imageVersionTag ` + --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` + --build-arg DOTNET_ASPNET_VERSION=$env:DOTNET_ASPNET_VERSION ` + --build-arg CONTAINER_REGISTRY_SOURCE_LABEL=$env:CONTAINER_REGISTRY_SOURCE_LABEL ` + --build-arg NUGET_HOST=$env:NUGET_HOST ` + --build-arg NUGET_USERNAME=$env:NUGET_USERNAME ` + --build-arg NUGET_PASSWORD=$env:NUGET_PASSWORD ` + ./; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + - name: Publish NuGet + shell: pwsh + run: | + $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; + dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + - name: Kubernetes Deploy + shell: pwsh + run: | + $zoneNames = az network dns zone list -g $env:AZURE_GROUP_DNS --query [].name; + + $env:ROUTE_HOST_NAMES = @( + foreach ($zoneName in $zoneNames) { + @" + - $env:SUB_DOMAIN_NAME.$($zoneName) + "@ + } + ) -join "`n"; + + $env:GATEWAY_NAME = kubectl get gateway -n apps -o jsonpath='{.items[0].metadata.name}' + + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; + Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; + Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; + + $command = @" + set -eu + + kubectl apply -f service.tmp.yaml || exit 1 + kubectl apply -f configmap.tmp.yaml || exit 2 + kubectl apply -f deployment.tmp.yaml || exit 3 + kubectl apply -f autoscaler.tmp.yaml || exit 4 + "@; + + $result = az aks command invoke ` + -g $env:AZURE_GROUP_KUBERNETES ` + -n $env:KUBERNETES_CLUSTER ` + --file .kubernetes/service.tmp.yaml ` + --file .kubernetes/configmap.tmp.yaml ` + --file .kubernetes/deployment.tmp.yaml ` + --file .kubernetes/autoscaler.tmp.yaml ` + --command $command ` + --output json | ConvertFrom-Json; + + echo $result; + + if ($result.exitCode -ne 0) + { + throw "error"; + } + + - name: GitHub Release + if: github.ref == 'refs/heads/master' + uses: ncipollo/release-action@v1 + with: + tag: v${{ env.VERSION }} + name: "Release ${{ env.VERSION }}" + body: | + Version: ${{ env.VERSION }} + Commit: ${{ github.sha }} + artifacts: "nupkgs/*" + token: ${{ secrets.GITHUB_TOKEN }} + draft: false + prerelease: false + + - name: Slack Notification + if: always() + uses: act10ns/slack@v2 + with: + webhook-url: ${{ secrets.SLACK_WEBHOOK }} + config: .github/config/slack.yml + status: ${{ job.status }} + channel: ${{ vars.SLACK_CHANNEL }} \ No newline at end of file diff --git a/Api.Documentation.Csp/.gitignore b/Api.Documentation.Csp/.gitignore new file mode 100644 index 00000000..9921fc06 --- /dev/null +++ b/Api.Documentation.Csp/.gitignore @@ -0,0 +1,11 @@ +.vs +*.user +*.userprefs +*.suo +_ReSharper* +**/bin +**/obj +*.DotSettings.User +packages +.env +**/Properties/launchSettings.json \ No newline at end of file diff --git a/Api.Documentation.Csp/.kubernetes/autoscaler.yaml b/Api.Documentation.Csp/.kubernetes/autoscaler.yaml new file mode 100644 index 00000000..95add8ad --- /dev/null +++ b/Api.Documentation.Csp/.kubernetes/autoscaler.yaml @@ -0,0 +1,25 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: %SERVICE_NAME%-hpa + namespace: %KUBERNETES_NAMESPACE% +spec: + minReplicas: %KUBERNETES_REPLICA_COUNT% + maxReplicas: %KUBERNETES_REPLICA_COUNT_MAX% + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: %SERVICE_NAME% + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: %KUBERNETES_CPU_SCALING% + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: %KUBERNETES_MEMORY_SCALING% diff --git a/Api.Documentation.Csp/.kubernetes/configmap.yaml b/Api.Documentation.Csp/.kubernetes/configmap.yaml new file mode 100644 index 00000000..d13013d1 --- /dev/null +++ b/Api.Documentation.Csp/.kubernetes/configmap.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: %SERVICE_NAME%-config + namespace: %KUBERNETES_NAMESPACE% +data: + App__Version: %VERSION% + ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Documentation.Csp/.kubernetes/deployment.yaml b/Api.Documentation.Csp/.kubernetes/deployment.yaml new file mode 100644 index 00000000..02882c56 --- /dev/null +++ b/Api.Documentation.Csp/.kubernetes/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: %SERVICE_NAME% + namespace: %KUBERNETES_NAMESPACE% + labels: + app: %SERVICE_NAME% +spec: + replicas: %KUBERNETES_REPLICA_COUNT% + revisionHistoryLimit: %KUBERNETES_REPLICA_HISTORY_COUNT% + selector: + matchLabels: + app: %SERVICE_NAME% + template: + metadata: + labels: + app: %SERVICE_NAME% + spec: + automountServiceAccountToken: false + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + fsGroup: 2000 + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: %SERVICE_NAME% + nodeSelector: + nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% + kubernetes.io/os: linux + containers: + - name: %SERVICE_NAME% + image: %CONTAINER_REGISTRY_HOST%/%IMAGE_NAME%:%VERSION% + ports: + - containerPort: 8080 + imagePullPolicy: Always + envFrom: + - configMapRef: + name: %SERVICE_NAME%-config + resources: + requests: + memory: %KUBERNETES_MEMORY_REQUEST% + cpu: %KUBERNETES_CPU_REQUEST% + limits: + memory: %KUBERNETES_MEMORY_LIMIT% + cpu: %KUBERNETES_CPU_LIMIT% + securityContext: + privileged: false + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 2000 + capabilities: + drop: + - ALL + livenessProbe: + httpGet: + path: /healthz + port: 8080 + scheme: HTTP + periodSeconds: 10 + initialDelaySeconds: 30 + timeoutSeconds: 2 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + scheme: HTTP + periodSeconds: 5 + initialDelaySeconds: 20 + timeoutSeconds: 2 + imagePullSecrets: + - name: ghcr-pull-secret + diff --git a/Api.Documentation.Csp/.kubernetes/service.yaml b/Api.Documentation.Csp/.kubernetes/service.yaml new file mode 100644 index 00000000..2d8e20b2 --- /dev/null +++ b/Api.Documentation.Csp/.kubernetes/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: %SERVICE_NAME% + namespace: %KUBERNETES_NAMESPACE% +spec: + ports: + - name: http + port: 8080 + selector: + app: %SERVICE_NAME% + type: ClusterIP diff --git a/Api.Documentation.Csp/.tests/Tests.Api.Documentation.Csp/Properties/DoNotParallelize.cs b/Api.Documentation.Csp/.tests/Tests.Api.Documentation.Csp/Properties/DoNotParallelize.cs new file mode 100644 index 00000000..6a076669 --- /dev/null +++ b/Api.Documentation.Csp/.tests/Tests.Api.Documentation.Csp/Properties/DoNotParallelize.cs @@ -0,0 +1,3 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[assembly: DoNotParallelize] \ No newline at end of file diff --git a/Api.Documentation.Nonce/.tests/Tests.Api.Documentation.Nonce/Tests.Api.Documentation.Nonce.csproj b/Api.Documentation.Csp/.tests/Tests.Api.Documentation.Csp/Tests.Api.Documentation.Csp.csproj similarity index 74% rename from Api.Documentation.Nonce/.tests/Tests.Api.Documentation.Nonce/Tests.Api.Documentation.Nonce.csproj rename to Api.Documentation.Csp/.tests/Tests.Api.Documentation.Csp/Tests.Api.Documentation.Csp.csproj index eff632ef..680f17ff 100644 --- a/Api.Documentation.Nonce/.tests/Tests.Api.Documentation.Nonce/Tests.Api.Documentation.Nonce.csproj +++ b/Api.Documentation.Csp/.tests/Tests.Api.Documentation.Csp/Tests.Api.Documentation.Csp.csproj @@ -16,8 +16,8 @@ - - + + diff --git a/Api.Documentation.Nonce/Api.Documentation.Nonce.Models/Api.Documentation.Nonce.Models.csproj b/Api.Documentation.Csp/Api.Documentation.Csp.Models/Api.Documentation.Csp.Models.csproj similarity index 100% rename from Api.Documentation.Nonce/Api.Documentation.Nonce.Models/Api.Documentation.Nonce.Models.csproj rename to Api.Documentation.Csp/Api.Documentation.Csp.Models/Api.Documentation.Csp.Models.csproj diff --git a/Api.Documentation.Nonce/Api.Documentation.Nonce.sln b/Api.Documentation.Csp/Api.Documentation.Csp.sln similarity index 94% rename from Api.Documentation.Nonce/Api.Documentation.Nonce.sln rename to Api.Documentation.Csp/Api.Documentation.Csp.sln index a7554d11..7f41e980 100644 --- a/Api.Documentation.Nonce/Api.Documentation.Nonce.sln +++ b/Api.Documentation.Csp/Api.Documentation.Csp.sln @@ -10,7 +10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".solution", ".solution", "{ Dockerfile = Dockerfile icon.png = icon.png LICENSE = LICENSE - localhost.pfx = localhost.pfx README.md = README.md EndProjectSection EndProject @@ -21,10 +20,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml - .kubernetes\certificate.yaml = .kubernetes\certificate.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml - .kubernetes\ingress.yaml = .kubernetes\ingress.yaml .kubernetes\service.yaml = .kubernetes\service.yaml EndProjectSection EndProject @@ -56,11 +53,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nano.Storage.Abstractions", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nano.App.Api", "..\..\Nano.Library\Nano.App.Api\Nano.App.Api.csproj", "{E55DCB7C-9082-3538-6288-5A6E5C8DE183}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.Documentation.Nonce.Models", "Api.Documentation.Nonce.Models\Api.Documentation.Nonce.Models.csproj", "{55CA3ADE-88B1-B763-9FAB-EE5D4F418EB8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.Documentation.Csp.Models", "Api.Documentation.Csp.Models\Api.Documentation.Csp.Models.csproj", "{55CA3ADE-88B1-B763-9FAB-EE5D4F418EB8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.Documentation.Nonce", "Api.Documentation.Nonce\Api.Documentation.Nonce.csproj", "{77596F3E-C6F4-A01F-B4D9-9370C3ED3CBC}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.Documentation.Csp", "Api.Documentation.Csp\Api.Documentation.Csp.csproj", "{77596F3E-C6F4-A01F-B4D9-9370C3ED3CBC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Api.Documentation.Nonce", ".tests\Tests.Api.Documentation.Nonce\Tests.Api.Documentation.Nonce.csproj", "{B008D867-12B4-7EB7-D707-1C1503E4151A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Api.Documentation.Csp", ".tests\Tests.Api.Documentation.Csp\Tests.Api.Documentation.Csp.csproj", "{B008D867-12B4-7EB7-D707-1C1503E4151A}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nano.Logging.Abstractions", "..\..\Nano.Library\Nano.Logging.Abstractions\Nano.Logging.Abstractions.csproj", "{6BE9DA86-B487-64ED-7BF6-08CA10A05A97}" EndProject diff --git a/Api.Documentation.Nonce/Api.Documentation.Nonce/Api.Documentation.Nonce.csproj b/Api.Documentation.Csp/Api.Documentation.Csp/Api.Documentation.Csp.csproj similarity index 91% rename from Api.Documentation.Nonce/Api.Documentation.Nonce/Api.Documentation.Nonce.csproj rename to Api.Documentation.Csp/Api.Documentation.Csp/Api.Documentation.Csp.csproj index 536fc1e7..cfb4bb7c 100644 --- a/Api.Documentation.Nonce/Api.Documentation.Nonce/Api.Documentation.Nonce.csproj +++ b/Api.Documentation.Csp/Api.Documentation.Csp/Api.Documentation.Csp.csproj @@ -31,7 +31,7 @@ - + \ No newline at end of file diff --git a/Api.Documentation.Nonce/Api.Documentation.Nonce/Controllers/ExamplesController.cs b/Api.Documentation.Csp/Api.Documentation.Csp/Controllers/ExamplesController.cs similarity index 100% rename from Api.Documentation.Nonce/Api.Documentation.Nonce/Controllers/ExamplesController.cs rename to Api.Documentation.Csp/Api.Documentation.Csp/Controllers/ExamplesController.cs diff --git a/Api.Documentation.Nonce/Api.Documentation.Nonce/Dockerfile.Local b/Api.Documentation.Csp/Api.Documentation.Csp/Dockerfile.Local similarity index 69% rename from Api.Documentation.Nonce/Api.Documentation.Nonce/Dockerfile.Local rename to Api.Documentation.Csp/Api.Documentation.Csp/Dockerfile.Local index a42dbcd8..8f4d7cbf 100644 --- a/Api.Documentation.Nonce/Api.Documentation.Nonce/Dockerfile.Local +++ b/Api.Documentation.Csp/Api.Documentation.Csp/Dockerfile.Local @@ -3,4 +3,4 @@ FROM mcr.microsoft.com/dotnet/aspnet:$DOTNET_ASPNET_VERSION AS base EXPOSE 8080 4443 -ENTRYPOINT ["dotnet", "Api.Documentation.Nonce.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "Api.Documentation.Csp.dll"] \ No newline at end of file diff --git a/Api.Documentation.Csp/Api.Documentation.Csp/Program.cs b/Api.Documentation.Csp/Api.Documentation.Csp/Program.cs new file mode 100644 index 00000000..32865375 --- /dev/null +++ b/Api.Documentation.Csp/Api.Documentation.Csp/Program.cs @@ -0,0 +1,10 @@ +using Nano.App.Api; + +NanoApiApplication + .ConfigureApp() + .ConfigureServices(_ => + { + // Blank + }) + .Build() + .Run(); diff --git a/Api.Documentation.Csp/Api.Documentation.Csp/Properties/InternalsVisibleTo.cs b/Api.Documentation.Csp/Api.Documentation.Csp/Properties/InternalsVisibleTo.cs new file mode 100644 index 00000000..243d1ab5 --- /dev/null +++ b/Api.Documentation.Csp/Api.Documentation.Csp/Properties/InternalsVisibleTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Tests.Api.Documentation.Csp")] \ No newline at end of file diff --git a/Api.Documentation.Csp/Api.Documentation.Csp/appsettings.Development.json b/Api.Documentation.Csp/Api.Documentation.Csp/appsettings.Development.json new file mode 100644 index 00000000..8593c62d --- /dev/null +++ b/Api.Documentation.Csp/Api.Documentation.Csp/appsettings.Development.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/Api.Documentation.Csp/Api.Documentation.Csp/appsettings.Production.json b/Api.Documentation.Csp/Api.Documentation.Csp/appsettings.Production.json new file mode 100644 index 00000000..8593c62d --- /dev/null +++ b/Api.Documentation.Csp/Api.Documentation.Csp/appsettings.Production.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/Api.Documentation.Csp/Api.Documentation.Csp/appsettings.Staging.json b/Api.Documentation.Csp/Api.Documentation.Csp/appsettings.Staging.json new file mode 100644 index 00000000..8593c62d --- /dev/null +++ b/Api.Documentation.Csp/Api.Documentation.Csp/appsettings.Staging.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/Api.Documentation.Csp/Api.Documentation.Csp/appsettings.json b/Api.Documentation.Csp/Api.Documentation.Csp/appsettings.json new file mode 100644 index 00000000..e6c4ccc8 --- /dev/null +++ b/Api.Documentation.Csp/Api.Documentation.Csp/appsettings.json @@ -0,0 +1,29 @@ +{ + "App": { + "Version": "1.0.0.0", + "Hosting": { + "Root": "api", + "Http": { + "Ports": [ + 8080 + ] + } + }, + "Documentation": { + "Name": "Application" + }, + "HttpPolicyHeaders": { + "Csp": { + "Scripts": { + "IsSelf": true + }, + "Styles": { + "IsSelf": true, + "Hashes": [ + "sha256-RL3ie0nH+Lzz2YNqQN83mnU0J1ot4QL7b99vMdIX99w=" + ] + } + } + } + } +} \ No newline at end of file diff --git a/Api.Documentation.Csp/Dockerfile b/Api.Documentation.Csp/Dockerfile new file mode 100644 index 00000000..32d43b6b --- /dev/null +++ b/Api.Documentation.Csp/Dockerfile @@ -0,0 +1,35 @@ +ARG DOTNET_SDK_VERSION +ARG DOTNET_ASPNET_VERSION +ARG CONTAINER_REGISTRY_SOURCE_LABEL +ARG NUGET_HOST +ARG NUGET_USERNAME +ARG NUGET_PASSWORD + +FROM mcr.microsoft.com/dotnet/aspnet:$DOTNET_ASPNET_VERSION AS base + +LABEL org.opencontainers.image.source=CONTAINER_REGISTRY_SOURCE_LABEL + +EXPOSE 8080 +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:$DOTNET_SDK_VERSION AS build +ARG NUGET_HOST +ARG NUGET_USERNAME +ARG NUGET_PASSWORD + +WORKDIR /src +COPY . . + +RUN dotnet nuget add source $NUGET_HOST -n private -u $NUGET_USERNAME -p $NUGET_PASSWORD --store-password-in-clear-text +RUN dotnet build -c Release -o /app + +FROM build AS publish +RUN dotnet publish -c Release -o /app + +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENV COMPlus_EnableDiagnostics=0 +ENV DOTNET_USE_POLLING_FILE_WATCHER=true + +ENTRYPOINT ["dotnet", "Api.Documentation.Csp.dll"] \ No newline at end of file diff --git a/Api.Documentation.Csp/LICENSE b/Api.Documentation.Csp/LICENSE new file mode 100644 index 00000000..006e8c21 --- /dev/null +++ b/Api.Documentation.Csp/LICENSE @@ -0,0 +1,18 @@ +The MIT License (MIT) +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Api.Documentation.Csp/README.md b/Api.Documentation.Csp/README.md new file mode 100644 index 00000000..65025793 --- /dev/null +++ b/Api.Documentation.Csp/README.md @@ -0,0 +1,54 @@ +# Api.Documentation.Csp + +> _Nano API application with api documentation and CSP._ +_All lessons are complete, self-contained examples that include build and deployment setup._ + +> ⚠️ _To run this solution, the **[Nano.Library](https://github.com/Nano-Core/Nano.Library)** repository must be checked out in the same root directory. +Nano is referenced directly from source (not via NuGet packages) and is expected to be located in the .nano solution folder._ + +> ⚠️ Remember to set the docker-compose project as startup project, before running the solution in Visual Studio. + +> 💡 Explore API requests for this lesson in our **[Public Nano Workspace on Postman](https://www.postman.com/nanocore/nano-lessons)**. + +*** + +## Table of Contents +* [Summary](#summary) +* [Configuration](#configuration) + +## Summary +This application builds on **[Api.Documenation](https://github.com/Nano-Core/Nano.Lessons/tree/master/Api.Documenation)**. + +This example shows using API documentation with strict CSP security. Run the solution and open [https://localhost:4443/docs](https://localhost:4443/docs) in your browser to +view the API documentation. + +Also a CSP hash has been added and the policy configured to allow inline styles for swagger. + +| Endpoint | Description | +| -------------------------------------------------- | -------------------- | +| `http://localhost:8080/api/examples/documentation` | Returns a `200 OK`. | + +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. + +## Configuration + +```json +"App": { + "Documentation": { + "Name": "Application" + }, + "HttpPolicyHeaders": { + "Csp": { + "Scripts": { + "IsSelf": true + }, + "Styles": { + "IsSelf": true, + "Hashes": [ + "sha256-RL3ie0nH+Lzz2YNqQN83mnU0J1ot4QL7b99vMdIX99w=" + ] + } + } + } +} +``` diff --git a/Api.Documentation.Csp/icon.png b/Api.Documentation.Csp/icon.png new file mode 100644 index 00000000..67567667 Binary files /dev/null and b/Api.Documentation.Csp/icon.png differ diff --git a/Api.Documentation.Nonce/.kubernetes/certificate.yaml b/Api.Documentation.Nonce/.kubernetes/certificate.yaml deleted file mode 100644 index a37bdadc..00000000 --- a/Api.Documentation.Nonce/.kubernetes/certificate.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: %SERVICE_NAME%-nginx-tls - namespace: %KUBERNETES_NAMESPACE% -spec: - secretName: %CERTIFICATE_HOST%-tls - duration: 2160h - renewBefore: 720h - subject: - organizations: - - %CERTIFICATE_ORGANIZATION% - dnsNames: - - %CERTIFICATE_HOST% - privateKey: - rotationPolicy: Always - issuerRef: - name: %CERTIFICATE_ISSUER% - kind: ClusterIssuer diff --git a/Api.Documentation.Nonce/.kubernetes/ingress.yaml b/Api.Documentation.Nonce/.kubernetes/ingress.yaml deleted file mode 100644 index ede6835a..00000000 --- a/Api.Documentation.Nonce/.kubernetes/ingress.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: ingress-%SERVICE_NAME% - namespace: %KUBERNETES_NAMESPACE% - annotations: - nginx.ingress.kubernetes.io/configuration-snippet: | - more_set_headers "Content-Security-Policy: script-src 'self' 'nonce-${request_id}'; style-src 'self' 'nonce-${request_id}'"; - sub_filter_once off; - sub_filter '%NONCE_TOKEN%' $request_id; - sub_filter '(]*>)(.*?)%NONCE_TOKEN%(.*?<\/body>)' '$1$2"$request_id"$3'; -spec: - ingressClassName: nginx - tls: - - hosts: - - %CERTIFICATE_HOST% - secretName: %CERTIFICATE_HOST%-tls - rules: - - host: %CERTIFICATE_HOST% - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: %SERVICE_NAME% - port: - number: 8080 diff --git a/Api.Documentation.Nonce/Api.Documentation.Nonce/Properties/InternalsVisibleTo.cs b/Api.Documentation.Nonce/Api.Documentation.Nonce/Properties/InternalsVisibleTo.cs deleted file mode 100644 index ca10fdb2..00000000 --- a/Api.Documentation.Nonce/Api.Documentation.Nonce/Properties/InternalsVisibleTo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Tests.Api.Documentation.Nonce")] \ No newline at end of file diff --git a/Api.Documentation.Nonce/Api.Documentation.Nonce/appsettings.Development.json b/Api.Documentation.Nonce/Api.Documentation.Nonce/appsettings.Development.json deleted file mode 100644 index 23bfb871..00000000 --- a/Api.Documentation.Nonce/Api.Documentation.Nonce/appsettings.Development.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "App": { - "Hosting": { - "http": { - "UseHttpsRedirection": true - }, - "Https": { - "Ports": [ - 4443 - ], - "Certificate": { - "Path": "/root/.dotnet/https/localhost.pfx", - "Password": "password" - }, - "UseHttpsRequired": true - } - } - } -} \ No newline at end of file diff --git a/Api.Documentation.Nonce/Api.Documentation.Nonce/appsettings.json b/Api.Documentation.Nonce/Api.Documentation.Nonce/appsettings.json deleted file mode 100644 index eafada5a..00000000 --- a/Api.Documentation.Nonce/Api.Documentation.Nonce/appsettings.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "App": { - "Version": "1.0.0.0", - "Hosting": { - "Root": "api", - "Http": { - "Ports": [ - 8080 - ] - } - }, - "Documentation": { - "Name": "Application", - "Description": "This is an example application", - "TermsOfService": "https://github.com/Nano-Core/Nano.Library/blob/master/LICENSE", - "Contact": { - "Name": "Nano Contributors", - "Email": "email@email.com", - "Url": "https://github.com/Nano-Core" - }, - "License": { - "Name": "MIT", - "Identifier": "MIT", - "Url": "https://github.com/Nano-Core/Nano.Library/blob/master/LICENSE" - }, - "CspNonce": "927ba207fece4379bbd32f7d3ad55675", - "HideDefaultVersion": true - }, - "HttpPolicyHeaders": { - "Csp": { - "Scripts": { - "IsSelf": true, - "Nonces": [ - "927ba207fece4379bbd32f7d3ad55675" - ] - }, - "Styles": { - "IsSelf": true, - "Nonces": [ - "927ba207fece4379bbd32f7d3ad55675" - ] - } - } - } - } -} \ No newline at end of file diff --git a/Api.Documentation.Nonce/README.md b/Api.Documentation.Nonce/README.md deleted file mode 100644 index f81dd555..00000000 --- a/Api.Documentation.Nonce/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# Api.Documentation.Nonce - -> _Nano API application with api documentation and nonce._ -_All lessons are complete, self-contained examples that include build and deployment setup._ - -> ⚠️ _To run this solution, the **[Nano.Library](https://github.com/Nano-Core/Nano.Library)** repository must be checked out in the same root directory. -Nano is referenced directly from source (not via NuGet packages) and is expected to be located in the .nano solution folder._ - -> ⚠️ Remember to set the docker-compose project as startup project, before running the solution in Visual Studio. - -> 💡 Explore API requests for this lesson in our **[Public Nano Workspace on Postman](https://www.postman.com/nanocore/nano-lessons)**. - -*** - -## Table of Contents -* [Summary](#summary) -* [Configuration](#configuration) -* [Kubernetes](#kubernetes) -* [GitHub Actions](#gitHub-actions) - -## Summary -This application builds on **[Api.Hosting.Https](https://github.com/Nano-Core/Nano.Lessons/tree/master/Api.Hosting.Https)**. - -This example shows using API documentation with CSP security using nonce. Run the solution and open -[https://localhost:4443/docs](https://localhost:4443/docs) in your browser to view the API documentation. - -Also a CSP nonce has been added and the policy configured to allow scripts and styles only with that nonce. The nonce value is available in `Documentation.Nonce`, -ensuring that Swagger scripts and styles are permitted. If you change `Documentation.Nonce`, the Swagger page will break with CSP script and style errors in the browser. -You can also remove the `HttpPolicyHeaders` from `appsettings` and set `Documentation.Nonce` to `null` to disable nonce enforcement. - -| Endpoint | Description | -| -------------------------------------------------- | -------------------- | -| `http://localhost:8080/api/examples/documentation` | Returns a `200 OK`. | - -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. - -## Configuration -```json -"App": { - "Documentation": { - "Name": "Application", - "Description": "This is an example application", - "TermsOfService": "https://github.com/Nano-Core/Nano.Library/blob/master/LICENSE", - "Contact": { - "Name": "Nano Contributors", - "Email": "email@email.com", - "Url": "https://github.com/Nano-Core" - }, - "License": { - "Name": "MIT", - "Identifier": "MIT", - "Url": "https://github.com/Nano-Core/Nano.Library/blob/master/LICENSE" - }, - "CspNonce": "927ba207fece4379bbd32f7d3ad55675", - "HideDefaultVersion": true - } -} -``` - -## Kubernetes -Annotations are configured below to automatically replace the static CSP nonce with a dynamically generated token, -ensuring that scripts and styles comply with the Content Security Policy. - -``` -kind: Ingress -metadata: - annotations: - nginx.ingress.kubernetes.io/configuration-snippet: | - more_set_headers "Content-Security-Policy: script-src 'self' 'nonce-${request_id}'; style-src 'self' 'nonce-${request_id}'"; - sub_filter_once off; - sub_filter '%NONCE_TOKEN%' $request_id; - sub_filter '(]*>)(.*?)%NONCE_TOKEN%(.*?<\/body>)' '$1$2"$request_id"$3'; -``` - -## GitHub Actions -Additional environment variables have been added to `build-and-deploy.yml` to support the new Kubernetes resources: - -```yaml -env: - NONCE_TOKEN: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_NONCE_TOKEN || secrets.STAGING_NONCE_TOKEN }} -``` diff --git a/Api.Documentation.Nonce/localhost.pfx b/Api.Documentation.Nonce/localhost.pfx deleted file mode 100644 index 22e513ca..00000000 Binary files a/Api.Documentation.Nonce/localhost.pfx and /dev/null differ diff --git a/Api.Documentation/.github/workflows/build-and-deploy.yml b/Api.Documentation/.github/workflows/build-and-deploy.yml index a4603f6c..87545d98 100644 --- a/Api.Documentation/.github/workflows/build-and-deploy.yml +++ b/Api.Documentation/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Documentation IMAGE_NAME: api.documentation @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Documentation/.kubernetes/deployment.yaml b/Api.Documentation/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.Documentation/.kubernetes/deployment.yaml +++ b/Api.Documentation/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Documentation/README.md b/Api.Documentation/README.md index 66faf75d..49195e45 100644 --- a/Api.Documentation/README.md +++ b/Api.Documentation/README.md @@ -32,7 +32,7 @@ for the same example endpoint. When set to `true`, Swagger only displays the non | -------------------------------------------------- | -------------------- | | `http://localhost:8080/api/examples/documentation` | Returns a `200 OK`. | -> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#documentation)**. +> 📖 Learn more about **[Nano API Documentation](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#documentation)**. ## Configuration ```json diff --git a/Api.ErrorHandling/.github/workflows/build-and-deploy.yml b/Api.ErrorHandling/.github/workflows/build-and-deploy.yml index ef434196..012b1be2 100644 --- a/Api.ErrorHandling/.github/workflows/build-and-deploy.yml +++ b/Api.ErrorHandling/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.ErrorHandling IMAGE_NAME: api.errorhandling @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.ErrorHandling/.kubernetes/deployment.yaml b/Api.ErrorHandling/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.ErrorHandling/.kubernetes/deployment.yaml +++ b/Api.ErrorHandling/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.ErrorHandling/Api.ErrorHandling/Controllers/ExamplesController.cs b/Api.ErrorHandling/Api.ErrorHandling/Controllers/ExamplesController.cs index 45d07121..16fd1e6b 100644 --- a/Api.ErrorHandling/Api.ErrorHandling/Controllers/ExamplesController.cs +++ b/Api.ErrorHandling/Api.ErrorHandling/Controllers/ExamplesController.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging; using Nano.App.Api.Controllers; using Nano.App.Exceptions; -using Nano.Data.Abstractions.Identity.Exceptions; +using Nano.Data.Abstractions.Exceptions; using Vivet.AspNetCore.RequestVirusScan.Exceptions; using Vivet.AspNetCore.RequestVirusScan.Models.Enums; diff --git a/Api.ErrorHandling/README.md b/Api.ErrorHandling/README.md index e52aa1cd..cec2c4c9 100644 --- a/Api.ErrorHandling/README.md +++ b/Api.ErrorHandling/README.md @@ -42,7 +42,7 @@ The following endpoint is available for testing: Alternatively, toggle the `ExposeErrors` to `false`, and observe that messages from `500 Internal Server Errors` no longer will be exposed. -> 📖 Learn more about **[Nano Error Handling](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#error-handling)**. +> 📖 Learn more about **[Nano Error Handling](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#error-handling)**. ## Configuration ```json diff --git a/Api.Eventing.RabbitMq/.github/workflows/build-and-deploy.yml b/Api.Eventing.RabbitMq/.github/workflows/build-and-deploy.yml index 77ae6c24..e4a92c54 100644 --- a/Api.Eventing.RabbitMq/.github/workflows/build-and-deploy.yml +++ b/Api.Eventing.RabbitMq/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Eventing.RabbitMq IMAGE_NAME: api.eventing.rabbitmq @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Eventing.RabbitMq/.kubernetes/deployment.yaml b/Api.Eventing.RabbitMq/.kubernetes/deployment.yaml index a1717afc..c8e22634 100644 --- a/Api.Eventing.RabbitMq/.kubernetes/deployment.yaml +++ b/Api.Eventing.RabbitMq/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -38,11 +38,17 @@ spec: - containerPort: 8080 imagePullPolicy: Always env: + - name: Eventing__Credentials__Id + valueFrom: + secretKeyRef: + name: rabbitmq-auth + key: username + envFrom: - name: Eventing__Credentials__Secret valueFrom: secretKeyRef: - name: rabbitmq - key: rabbitmq-password + name: rabbitmq-auth + key: password envFrom: - configMapRef: name: %SERVICE_NAME%-config diff --git a/Api.Eventing.RabbitMq/Api.Eventing.RabbitMq/appsettings.Development.json b/Api.Eventing.RabbitMq/Api.Eventing.RabbitMq/appsettings.Development.json index 8593c62d..b1f4b0f1 100644 --- a/Api.Eventing.RabbitMq/Api.Eventing.RabbitMq/appsettings.Development.json +++ b/Api.Eventing.RabbitMq/Api.Eventing.RabbitMq/appsettings.Development.json @@ -1,2 +1,8 @@ { + "Eventing": { + "Credentials": { + "Id": "rabbitmq_user", + "Secret": "password" + } + } } \ No newline at end of file diff --git a/Api.Eventing.RabbitMq/Api.Eventing.RabbitMq/appsettings.json b/Api.Eventing.RabbitMq/Api.Eventing.RabbitMq/appsettings.json index 8dc5251a..fd937648 100644 --- a/Api.Eventing.RabbitMq/Api.Eventing.RabbitMq/appsettings.json +++ b/Api.Eventing.RabbitMq/Api.Eventing.RabbitMq/appsettings.json @@ -21,8 +21,8 @@ "Heartbeat": 60, "PrefetchCount": 50, "Credentials": { - "Id": "rabbitmq_user", - "Secret": "password" + "Id": null, + "Secret": null }, "HealthCheck": { "UnhealthyStatus": "Unhealthy" diff --git a/Api.Eventing.RabbitMq/README.md b/Api.Eventing.RabbitMq/README.md index 537fd058..853d3e09 100644 --- a/Api.Eventing.RabbitMq/README.md +++ b/Api.Eventing.RabbitMq/README.md @@ -35,7 +35,7 @@ You can also monitor the messages via the RabbitMQ management interface: **[http An eventing health check is configured to target the RabbitMQ. Open **[http://localhost:8080/healthz](http://localhost:8080/healthz)** to view the health-check status in the JSON response. -> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#health-checks)** +> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#health-checks)** The following endpoint is available for testing: @@ -44,7 +44,7 @@ The following endpoint is available for testing: | `http://localhost:8080/api/examples/eventing` | Returns a simple `200 OK` response. Publishes a message that wiil be consumed by the `EventHandler` | | `http://localhost:8080/api/examples/eventing-routing-key` | Returns a simple `200 OK` response. Publishes a message using routing key that wiil be consumed by the `EventHandler` | -> 📖 Learn more about **[Nano.Eventing.RabbitMq](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Eventing.RabbitMq)**. +> 📖 Learn more about **[Nano.Eventing.RabbitMq](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Eventing.RabbitMq/README.md#nanoeventingrabbitmq)**. ## Registration The following eventing provider has been registered using `ConfigureServices(...)` in `program.cs`. @@ -60,7 +60,7 @@ The following eventing provider has been registered using `ConfigureServices(... ``` ## Configuration -Configured the application with the necessary eventing setup. +Configured the application `appsettings.json` with the necessary eventing setup. ```json "Eventing": { @@ -81,6 +81,17 @@ Configured the application with the necessary eventing setup. } ``` +...and the `appsettings.Development.json` eventing configuration. + +```json +"Eventing": { + "Credentials": { + "Id": "rabbitmq_user", + "Secret": "password" + } +} +``` + Additionally, application health-checks have been enabled with the configuration. ```json @@ -124,11 +135,17 @@ spec: spec: containers: env: + - name: Eventing__Credentials__Id + valueFrom: + secretKeyRef: + name: rabbitmq-auth + key: username + envFrom: - name: Eventing__Credentials__Secret valueFrom: secretKeyRef: - name: rabbitmq - key: rabbitmq-password + name: rabbitmq-auth + key: password ``` > ⚠️ The `rabbitmq` secret is created alongside the **[Nano Azure Kubernetes Eventing](https://github.com/Nano-Core/Nano.Azure.Kubernetes/tree/master/Nano.Azure.Kubernetes.RabbitMQ)** diff --git a/Api.HealthChecks/.github/workflows/build-and-deploy.yml b/Api.HealthChecks/.github/workflows/build-and-deploy.yml index b863d9ee..e09d6b24 100644 --- a/Api.HealthChecks/.github/workflows/build-and-deploy.yml +++ b/Api.HealthChecks/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.HealthChecks IMAGE_NAME: api.healthchecks @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -38,23 +41,28 @@ env: AVAILABILITY_CHECK_FREQUENCY: 300 jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -80,18 +88,18 @@ jobs: id: add-availability-check shell: pwsh run: | - sudo az extension add -n application-insights; + az extension add -n application-insights; $env:SERVICE_NAME_INSIGTHS = $env:SERVICE_NAME + "-insights"; - $env:APPLICATION_INSIGHT_ID = sudo az monitor app-insights component show --query "[?contains(name, '$env:SERVICE_NAME_INSIGTHS')].[id]" -o tsv; + $env:APPLICATION_INSIGHT_ID = az monitor app-insights component show --query "[?contains(name, '$env:SERVICE_NAME_INSIGTHS')].[id]" -o tsv; if ([string]::IsNullOrEmpty($env:APPLICATION_INSIGHT_ID)) { - $env:WORKSPACE_ID = sudo az monitor log-analytics workspace list --query "[?contains(name, 'log-analytics')].[id]" -o tsv; + $env:WORKSPACE_ID = az monitor log-analytics workspace list --query "[?contains(name, 'log-analytics')].[id]" -o tsv; if (-not [string]::IsNullOrEmpty($env:WORKSPACE_ID)) { - $env:APPLICATION_INSIGHT_ID = sudo az monitor app-insights component create ` + $env:APPLICATION_INSIGHT_ID = az monitor app-insights component create ` -a $env:SERVICE_NAME_INSIGTHS ` -l $env:AZURE_LOCATION ` -g $env:AZURE_GROUP ` @@ -106,12 +114,12 @@ jobs: }; $env:SERVICE_NAME_AVAILABILITY = $env:SERVICE_NAME + '-availability-' + $env:ASPNETCORE_ENVIRONMENT.ToLower(); - $env:AVAILABILITY_ID = sudo az monitor app-insights web-test list -g $env:AZURE_GROUP --query "[?contains(name, '$env:SERVICE_NAME_AVAILABILITY')].[id]" -o tsv; + $env:AVAILABILITY_ID = az monitor app-insights web-test list -g $env:AZURE_GROUP --query "[?contains(name, '$env:SERVICE_NAME_AVAILABILITY')].[id]" -o tsv; if ([string]::IsNullOrEmpty($env:AVAILABILITY_ID)) { $env:APPLICATION_INSIGHT_HIDDEN_LINK = 'hidden-link:' + $env:APPLICATION_INSIGHT_ID + '=Resource'; - sudo az monitor app-insights web-test create ` + az monitor app-insights web-test create ` -n $env:SERVICE_NAME_AVAILABILITY ` --defined-web-test-name $env:SERVICE_NAME_AVAILABILITY ` -g $env:AZURE_GROUP ` @@ -146,7 +154,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -162,9 +170,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -175,7 +183,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -184,29 +192,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.HealthChecks/.kubernetes/deployment.yaml b/Api.HealthChecks/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.HealthChecks/.kubernetes/deployment.yaml +++ b/Api.HealthChecks/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.HealthChecks/Api.HealthChecks/Controllers/ExamplesController.cs b/Api.HealthChecks/Api.HealthChecks/Controllers/ExamplesController.cs index b491a708..7763d523 100644 --- a/Api.HealthChecks/Api.HealthChecks/Controllers/ExamplesController.cs +++ b/Api.HealthChecks/Api.HealthChecks/Controllers/ExamplesController.cs @@ -51,20 +51,4 @@ public virtual async Task HealthCheckAsync(CancellationToken canc }) }); } - - /// - /// Webhook Action. - /// - /// The cancellation token. - /// A message. - /// Success. - [HttpPost] - [Route("webhook")] - [ProducesResponseType((int)HttpStatusCode.OK)] - public virtual async Task WebhookAsync(CancellationToken cancellationToken = default) - { - await Task.CompletedTask; - - return this.Ok(); - } } \ No newline at end of file diff --git a/Api.HealthChecks/README.md b/Api.HealthChecks/README.md index 2b65dbd6..18536976 100644 --- a/Api.HealthChecks/README.md +++ b/Api.HealthChecks/README.md @@ -25,12 +25,11 @@ This example illustrates the use of Nano API health-checks. Open [http://localhost:8080/healthz](http://localhost:8080/healthz) to view the startup health-check JSON report. -A webhook is configured for health-check that will trigger if the application becomes unhealthy. +The following endpoints are available for testing. | Endpoint | Description | | ------------------------------------------------- | ------------------------------------------------------ | | `http://localhost:8080/api/examples/health-check` | Returns a `200 OK` response with health-check status. | -| `http://localhost:8080/api/examples/webhook` | Returns a `200 OK` response. | > 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Apihealth-checks)**. diff --git a/Api.Hosting.Http/.github/workflows/build-and-deploy.yml b/Api.Hosting.Http/.github/workflows/build-and-deploy.yml index a944f6cf..730be7cd 100644 --- a/Api.Hosting.Http/.github/workflows/build-and-deploy.yml +++ b/Api.Hosting.Http/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Hosting.Http IMAGE_NAME: api.hosting.http @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Hosting.Http/.kubernetes/deployment.yaml b/Api.Hosting.Http/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.Hosting.Http/.kubernetes/deployment.yaml +++ b/Api.Hosting.Http/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Hosting.Http/README.md b/Api.Hosting.Http/README.md index a3157f70..13c5fa45 100644 --- a/Api.Hosting.Http/README.md +++ b/Api.Hosting.Http/README.md @@ -27,4 +27,4 @@ The following endpoint is available for testing: | ------------------------------------------ | -------------------------------------- | | `http://localhost:8080/api/examples/http` | Returns a simple `200 OK` response. | -> 📖 Learn more about **[Nano Hosting Http](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#http)**. +> 📖 Learn more about **[Nano Hosting Http](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#http)**. diff --git a/Api.Hosting.Https/.github/workflows/build-and-deploy.yml b/Api.Hosting.Https/.github/workflows/build-and-deploy.yml index 837b9252..629d9c6f 100644 --- a/Api.Hosting.Https/.github/workflows/build-and-deploy.yml +++ b/Api.Hosting.Https/.github/workflows/build-and-deploy.yml @@ -1,29 +1,34 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Hosting.Https IMAGE_NAME: api.hosting.https SERVICE_NAME: api-hosting-https + SUB_DOMAIN_NAME: nano VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} + AZURE_GROUP_DNS: ${{ vars.AZURE_RESOURCE_GROUP_DNS }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -33,29 +38,31 @@ env: KUBERNETES_CPU_REQUEST: 200m KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 - CERTIFICATE_ISSUER: letsencrypt-prod - CERTIFICATE_ORGANIZATION: ${{ vars.CERTIFICATE_ORGANIZATION }} - CERTIFICATE_HOST: ${{ github.ref == 'refs/heads/master' && vars.HOST_API_SUBDOMAIN + '.' + vars.PRODUCTION_HOST || vars.HOST_API_SUBDOMAIN + '.' + vars.STAGING_HOST }} ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -84,7 +91,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -100,9 +107,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -113,7 +120,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -122,47 +129,57 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; - if ($LastExitCode -ne 0) + $zoneNames = az network dns zone list -g $env:AZURE_GROUP_DNS --query "[].name" -o json | ConvertFrom-Json + + $env:ROUTE_HOST_NAMES = ( + $zoneNames | ForEach-Object { + " - $env:SUB_DOMAIN_NAME.$_" + } + ) -join "`n" + + $env:GATEWAY_NAME = kubectl get gateway -n apps -o jsonpath='{.items[0].metadata.name}' + + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; - if ($LastExitCode -ne 0) + kubectl apply -f .kubernetes/configmap.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; - if ($LastExitCode -ne 0) + kubectl apply -f .kubernetes/deployment.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; - if ($LastExitCode -ne 0) + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } - Get-Content .kubernetes/certificate.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/certificate.tmp.yaml; - sudo kubectl apply -f .kubernetes/certificate.tmp.yaml; - if ($LastExitCode -ne 0) + Get-Content .kubernetes/grafana-httproute-80.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/grafana-httproute-80.tmp.yaml; + kubectl apply -f .kubernetes/grafana-httproute-80.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } - Get-Content .kubernetes/ingress.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/ingress.tmp.yaml; - sudo kubectl apply -f .kubernetes/ingress.tmp.yaml; - if ($LastExitCode -ne 0) + Get-Content .kubernetes/grafana-httproute-443.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/grafana-httproute-443.tmp.yaml; + kubectl apply -f .kubernetes/grafana-httproute-443.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } - name: GitHub Release if: github.ref == 'refs/heads/master' diff --git a/Api.Hosting.Https/.kubernetes/certificate.yaml b/Api.Hosting.Https/.kubernetes/certificate.yaml deleted file mode 100644 index a37bdadc..00000000 --- a/Api.Hosting.Https/.kubernetes/certificate.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: %SERVICE_NAME%-nginx-tls - namespace: %KUBERNETES_NAMESPACE% -spec: - secretName: %CERTIFICATE_HOST%-tls - duration: 2160h - renewBefore: 720h - subject: - organizations: - - %CERTIFICATE_ORGANIZATION% - dnsNames: - - %CERTIFICATE_HOST% - privateKey: - rotationPolicy: Always - issuerRef: - name: %CERTIFICATE_ISSUER% - kind: ClusterIssuer diff --git a/Api.Hosting.Https/.kubernetes/deployment.yaml b/Api.Hosting.Https/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.Hosting.Https/.kubernetes/deployment.yaml +++ b/Api.Hosting.Https/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Hosting.Https/.kubernetes/httproute-443.yaml b/Api.Hosting.Https/.kubernetes/httproute-443.yaml new file mode 100644 index 00000000..65b97a51 --- /dev/null +++ b/Api.Hosting.Https/.kubernetes/httproute-443.yaml @@ -0,0 +1,18 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: %SERVICE_NAME%-route + namespace: %KUBERNETES_NAMESPACE% +spec: + parentRefs: + - name: %GATEWAY_NAME% + hostnames: +%ROUTE_HOST_NAMES% + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: %SERVICE_NAME% + port: 8080 \ No newline at end of file diff --git a/Api.Hosting.Https/.kubernetes/httproute-80.yaml b/Api.Hosting.Https/.kubernetes/httproute-80.yaml new file mode 100644 index 00000000..f29775aa --- /dev/null +++ b/Api.Hosting.Https/.kubernetes/httproute-80.yaml @@ -0,0 +1,17 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: %APP_NAME%-route-80 + namespace: %KUBERNETES_NAMESPACE% +spec: + parentRefs: + - name: %GATEWAY_NAME% + sectionName: http + hostnames: +%ROUTE_HOST_NAMES% + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 301 \ No newline at end of file diff --git a/Api.Hosting.Https/.kubernetes/ingress.yaml b/Api.Hosting.Https/.kubernetes/ingress.yaml deleted file mode 100644 index 0acc7163..00000000 --- a/Api.Hosting.Https/.kubernetes/ingress.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: ingress-%SERVICE_NAME% - namespace: %KUBERNETES_NAMESPACE% -spec: - ingressClassName: nginx - tls: - - hosts: - - %CERTIFICATE_HOST% - secretName: %CERTIFICATE_HOST%-tls - rules: - - host: %CERTIFICATE_HOST% - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: %SERVICE_NAME% - port: - number: 8080 diff --git a/Api.Hosting.Https/Api.Hosting.Https.sln b/Api.Hosting.Https/Api.Hosting.Https.sln index a0cf61cd..6024beda 100644 --- a/Api.Hosting.Https/Api.Hosting.Https.sln +++ b/Api.Hosting.Https/Api.Hosting.Https.sln @@ -21,10 +21,10 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml - .kubernetes\certificate.yaml = .kubernetes\certificate.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml - .kubernetes\ingress.yaml = .kubernetes\ingress.yaml + .kubernetes\httproute-443.yaml = .kubernetes\httproute-443.yaml + .kubernetes\httproute-80.yaml = .kubernetes\httproute-80.yaml .kubernetes\service.yaml = .kubernetes\service.yaml EndProjectSection EndProject diff --git a/Api.Hosting.Https/README.md b/Api.Hosting.Https/README.md index 019d09bd..b9bf791d 100644 --- a/Api.Hosting.Https/README.md +++ b/Api.Hosting.Https/README.md @@ -35,7 +35,7 @@ The following endpoints are available for testing: | `http://localhost:8080/api/examples/http` | Redirects to HTTPS. | | `https://localhost:4443/api/examples/https` | Returns a simple `200 OK` response. | -> 📖 Learn more about **[Nano Hosting HTTPS](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#https)**. +> 📖 Learn more about **[Nano Hosting HTTPS](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#https)**. ## Configuration For `appsettings.json`, nothing has changed - HTTP is still exposed. @@ -76,25 +76,14 @@ services: ``` ## Kubernetes -A `certificate.yaml` and a `ingress.yml` resource has been added to the `.kubernetes` folder. - -| File / Directory | Type | Description | -| -------------------- | ------- | -------------------------------------- | -| `ingress.yaml` | `yaml` | The ingress spec for Kubernetes. | -| `certificate.yaml` | `yaml` | The certificate spec for Kuberentes. | +A `httproute.yaml` resource has been added to the `.kubernetes` folder. +| File / Directory | Type | Description | +| -------------------- | ------- | -------------------------------------------- | +| `httproute.yaml` | `yaml` | The http route spec for Kubernetes Gateway. | ## GitHub Actions -Additional environment variables have been added to `build-and-deploy.yml` to support the new Kubernetes resources. - -```yaml -env: - CERTIFICATE_ISSUER: letsencrypt-prod - CERTIFICATE_ORGANIZATION: ${{ vars.CERTIFICATE_ORGANIZATION }} - CERTIFICATE_HOST: ${{ github.ref == 'refs/heads/master' && vars.HOST_API_SUBDOMAIN + '.' + vars.PRODUCTION_HOST || vars.HOST_API_SUBDOMAIN + '.' + vars.STAGING_HOST }} -``` - -Deployment commands have also been updated to apply each of the new Kubernetes templates. +Deployment commands have been updated to apply the new Kubernetes `HTTPRoute` template. ```powershell Get-Content .kubernetes/{resource-name}.yaml ` diff --git a/Api.Hosting.MultipartLimits/.github/workflows/build-and-deploy.yml b/Api.Hosting.MultipartLimits/.github/workflows/build-and-deploy.yml index 4161a759..4ca38002 100644 --- a/Api.Hosting.MultipartLimits/.github/workflows/build-and-deploy.yml +++ b/Api.Hosting.MultipartLimits/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Hosting.MultipartLimits IMAGE_NAME: api.hosting.multipartlimits @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Hosting.MultipartLimits/.kubernetes/deployment.yaml b/Api.Hosting.MultipartLimits/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.Hosting.MultipartLimits/.kubernetes/deployment.yaml +++ b/Api.Hosting.MultipartLimits/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Localization/.github/workflows/build-and-deploy.yml b/Api.Localization/.github/workflows/build-and-deploy.yml index 9c109c22..5e3244b5 100644 --- a/Api.Localization/.github/workflows/build-and-deploy.yml +++ b/Api.Localization/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Localization IMAGE_NAME: api.localization @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Localization/.kubernetes/deployment.yaml b/Api.Localization/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.Localization/.kubernetes/deployment.yaml +++ b/Api.Localization/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Localization/README.md b/Api.Localization/README.md index 8e934551..e3071475 100644 --- a/Api.Localization/README.md +++ b/Api.Localization/README.md @@ -36,7 +36,7 @@ The controller return a response in the following format: | ------------------------------------------------- | ---------------------------------------------------------------- | | `http://localhost:8080/api/examples/localization` | Returns a `200 OK` response with names of the culture langauge. | -> 📖 Learn more about **[Nano Localization](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#localization)**. +> 📖 Learn more about **[Nano Localization](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#localization)**. ## Configuration diff --git a/Api.Logging.Log4Net/.github/workflows/build-and-deploy.yml b/Api.Logging.Log4Net/.github/workflows/build-and-deploy.yml index f0652e64..51e37842 100644 --- a/Api.Logging.Log4Net/.github/workflows/build-and-deploy.yml +++ b/Api.Logging.Log4Net/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Logging.Log4Net IMAGE_NAME: api.logging.log4net @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Logging.Log4Net/.kubernetes/deployment.yaml b/Api.Logging.Log4Net/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.Logging.Log4Net/.kubernetes/deployment.yaml +++ b/Api.Logging.Log4Net/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Logging.Log4Net/README.md b/Api.Logging.Log4Net/README.md index 956a56ea..374f470e 100644 --- a/Api.Logging.Log4Net/README.md +++ b/Api.Logging.Log4Net/README.md @@ -31,7 +31,7 @@ The following endpoint is available for testing: | `http://localhost:8080/api/examples/logging` | Returns a simple `200 OK` response. Won't log the `.LogDebug(...)` due to configuration `LogLevel=Information`. | | `http://localhost:8080/api/examples/logging-exception` | Returns a simple `500 Internal Server Error` response. The exception will be logged. | -> 📖 Learn more about **[Nano.Logging.Log4Net](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.Log4Net)**. +> 📖 Learn more about **[Nano.Logging.Log4Net](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.Log4Net/README.md#nanologginglog4net)**. ## Registration The following logging has been registered using `ConfigureServices(...)` in `program.cs`. diff --git a/Api.Logging.Microsoft/.github/workflows/build-and-deploy.yml b/Api.Logging.Microsoft/.github/workflows/build-and-deploy.yml index 5cf18938..c4343328 100644 --- a/Api.Logging.Microsoft/.github/workflows/build-and-deploy.yml +++ b/Api.Logging.Microsoft/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Logging.Microsoft IMAGE_NAME: api.logging.microsoft @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Logging.Microsoft/.kubernetes/deployment.yaml b/Api.Logging.Microsoft/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.Logging.Microsoft/.kubernetes/deployment.yaml +++ b/Api.Logging.Microsoft/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Logging.Microsoft/README.md b/Api.Logging.Microsoft/README.md index 23c9e8e7..525ba003 100644 --- a/Api.Logging.Microsoft/README.md +++ b/Api.Logging.Microsoft/README.md @@ -31,7 +31,7 @@ The following endpoint is available for testing: | `http://localhost:8080/api/examples/logging` | Returns a simple `200 OK` response. Won't log the `.LogDebug(...)` due to configuration `LogLevel=Information`. | | `http://localhost:8080/api/examples/logging-exception` | Returns a simple `500 Internal Server Error` response. The exception will be logged. | -> 📖 Learn more about **[Nano.Logging.Microsoft](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.Microsoft)**. +> 📖 Learn more about **[Nano.Logging.Microsoft](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.Microsoft/README.md#nanologgingmicrosoft)**. ## Registration The following logging has been registered using `ConfigureServices(...)` in `program.cs`. diff --git a/Api.Logging.NLog/.github/workflows/build-and-deploy.yml b/Api.Logging.NLog/.github/workflows/build-and-deploy.yml index 5d0bd0d0..16faef4c 100644 --- a/Api.Logging.NLog/.github/workflows/build-and-deploy.yml +++ b/Api.Logging.NLog/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Logging.NLog IMAGE_NAME: api.logging.nlog @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Logging.NLog/.kubernetes/deployment.yaml b/Api.Logging.NLog/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.Logging.NLog/.kubernetes/deployment.yaml +++ b/Api.Logging.NLog/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Logging.NLog/README.md b/Api.Logging.NLog/README.md index 80b79ee2..df4ac65b 100644 --- a/Api.Logging.NLog/README.md +++ b/Api.Logging.NLog/README.md @@ -31,7 +31,7 @@ The following endpoint is available for testing. | `http://localhost:8080/api/examples/logging` | Returns a simple `200 OK` response. Won't log the `.LogDebug(...)` due to configuration `LogLevel=Information`. | | `http://localhost:8080/api/examples/logging-exception` | Returns a simple `500 Internal Server Error` response. The exception will be logged. | -> 📖 Learn more about **[Nano.Logging.NLog](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.NLog)**. +> 📖 Learn more about **[Nano.Logging.NLog](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.NLog/README.md#nanologgingnlog)**. ## Registration The following logging has been registered using `ConfigureServices(...)` in `program.cs`. diff --git a/Api.Logging.Serilog/.github/workflows/build-and-deploy.yml b/Api.Logging.Serilog/.github/workflows/build-and-deploy.yml index 1e9e649f..c26ccdfb 100644 --- a/Api.Logging.Serilog/.github/workflows/build-and-deploy.yml +++ b/Api.Logging.Serilog/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Logging.Serilog IMAGE_NAME: api.logging.serilog @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Logging.Serilog/.kubernetes/deployment.yaml b/Api.Logging.Serilog/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.Logging.Serilog/.kubernetes/deployment.yaml +++ b/Api.Logging.Serilog/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Logging.Serilog/README.md b/Api.Logging.Serilog/README.md index c84a62b3..e52dcf19 100644 --- a/Api.Logging.Serilog/README.md +++ b/Api.Logging.Serilog/README.md @@ -24,14 +24,14 @@ that inherits from the top-level Nano `BaseController`. This application demonstrates logging with Serilog for a API application. Also note the `LogLevelOverrides` configuration, where logs under the `Microsoft` namespace are set to `Warning`, which suppresses several informational messages during application startup. -The following endpoint is available for testing: +The following endpoint is available for testing. | Endpoint | Description | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `http://localhost:8080/api/examples/logging` | Returns a simple `200 OK` response. Won't log the `.LogDebug(...)` due to configuration `LogLevel=Information`. | | `http://localhost:8080/api/examples/logging-exception` | Returns a simple `500 Internal Server Error` response. The exception will be logged. | -> 📖 Learn more about **[Nano.Logging.Serilog](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.Serilog)**. +> 📖 Learn more about **[Nano.Logging.Serilog](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.Serilog/README.md#nanologgingserilog)**. ## Registration The following logging has been registered using `ConfigureServices(...)` in `program.cs`. diff --git a/Api.MultipartJson/.github/workflows/build-and-deploy.yml b/Api.MultipartJson/.github/workflows/build-and-deploy.yml index 5a28fde8..8f3918cf 100644 --- a/Api.MultipartJson/.github/workflows/build-and-deploy.yml +++ b/Api.MultipartJson/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.MultipartJson IMAGE_NAME: api.multipartjson @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.MultipartJson/.kubernetes/deployment.yaml b/Api.MultipartJson/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.MultipartJson/.kubernetes/deployment.yaml +++ b/Api.MultipartJson/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.MultipartJson/README.md b/Api.MultipartJson/README.md index e78b2c23..b4c35ee4 100644 --- a/Api.MultipartJson/README.md +++ b/Api.MultipartJson/README.md @@ -27,4 +27,4 @@ The following endpoint is available for testing. | ---------------------------------------------------- | -------------------------------------- | | `http://localhost:8080/api/examples/multipart-json` | Returns a simple `200 OK` response. | -> 📖 Learn more about **[Nano Request Multipart JSON](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#request-multipart-json)**. +> 📖 Learn more about **[Nano Request Multipart JSON](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#request-multipart-json)**. diff --git a/Api.PolicyHeaders.ContentSecurityPolicy/.github/workflows/build-and-deploy.yml b/Api.PolicyHeaders.ContentSecurityPolicy/.github/workflows/build-and-deploy.yml index f6d4b5e8..c97c19fa 100644 --- a/Api.PolicyHeaders.ContentSecurityPolicy/.github/workflows/build-and-deploy.yml +++ b/Api.PolicyHeaders.ContentSecurityPolicy/.github/workflows/build-and-deploy.yml @@ -1,29 +1,34 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.PolicyHeaders.ContentSecurityPolicy IMAGE_NAME: api.policyheaders.contentsecuritypolicy SERVICE_NAME: api-policyheaders-contentsecuritypolicy + SUB_DOMAIN_NAME: nano VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_DNS: ${{ vars.AZURE_RESOURCE_GROUP_DNS }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -33,29 +38,31 @@ env: KUBERNETES_CPU_REQUEST: 200m KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 - CERTIFICATE_ISSUER: letsencrypt-prod - CERTIFICATE_ORGANIZATION: ${{ vars.CERTIFICATE_ORGANIZATION }} - CERTIFICATE_HOST: ${{ github.ref == 'refs/heads/master' && vars.HOST_API_SUBDOMAIN + '.' + vars.PRODUCTION_HOST || vars.HOST_API_SUBDOMAIN + '.' + vars.STAGING_HOST }} ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -84,7 +91,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -100,9 +107,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -113,7 +120,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -122,47 +129,57 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; - if ($LastExitCode -ne 0) + $zoneNames = az network dns zone list -g $env:AZURE_GROUP_DNS --query "[].name" -o json | ConvertFrom-Json + + $env:ROUTE_HOST_NAMES = ( + $zoneNames | ForEach-Object { + " - $env:SUB_DOMAIN_NAME.$_" + } + ) -join "`n" + + $env:GATEWAY_NAME = kubectl get gateway -n apps -o jsonpath='{.items[0].metadata.name}' + + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f service.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; - if ($LastExitCode -ne 0) + kubectl apply -f configmap.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; - if ($LastExitCode -ne 0) + kubectl apply -f deployment.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; - if ($LastExitCode -ne 0) + kubectl apply -f autoscaler.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } - Get-Content .kubernetes/certificate.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/certificate.tmp.yaml; - sudo kubectl apply -f .kubernetes/certificate.tmp.yaml; - if ($LastExitCode -ne 0) + Get-Content .kubernetes/grafana-httproute-80.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/grafana-httproute-80.tmp.yaml; + kubectl apply -f .kubernetes/grafana-httproute-80.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } - Get-Content .kubernetes/ingress.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/ingress.tmp.yaml; - sudo kubectl apply -f .kubernetes/ingress.tmp.yaml; - if ($LastExitCode -ne 0) + Get-Content .kubernetes/grafana-httproute-443.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/grafana-httproute-443.tmp.yaml; + kubectl apply -f .kubernetes/grafana-httproute-443.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } - name: GitHub Release if: github.ref == 'refs/heads/master' diff --git a/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/certificate.yaml b/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/certificate.yaml deleted file mode 100644 index a37bdadc..00000000 --- a/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/certificate.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: %SERVICE_NAME%-nginx-tls - namespace: %KUBERNETES_NAMESPACE% -spec: - secretName: %CERTIFICATE_HOST%-tls - duration: 2160h - renewBefore: 720h - subject: - organizations: - - %CERTIFICATE_ORGANIZATION% - dnsNames: - - %CERTIFICATE_HOST% - privateKey: - rotationPolicy: Always - issuerRef: - name: %CERTIFICATE_ISSUER% - kind: ClusterIssuer diff --git a/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/deployment.yaml b/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/deployment.yaml +++ b/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/httproute-443.yaml b/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/httproute-443.yaml new file mode 100644 index 00000000..65b97a51 --- /dev/null +++ b/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/httproute-443.yaml @@ -0,0 +1,18 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: %SERVICE_NAME%-route + namespace: %KUBERNETES_NAMESPACE% +spec: + parentRefs: + - name: %GATEWAY_NAME% + hostnames: +%ROUTE_HOST_NAMES% + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: %SERVICE_NAME% + port: 8080 \ No newline at end of file diff --git a/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/httproute-80.yaml b/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/httproute-80.yaml new file mode 100644 index 00000000..f29775aa --- /dev/null +++ b/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/httproute-80.yaml @@ -0,0 +1,17 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: %APP_NAME%-route-80 + namespace: %KUBERNETES_NAMESPACE% +spec: + parentRefs: + - name: %GATEWAY_NAME% + sectionName: http + hostnames: +%ROUTE_HOST_NAMES% + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 301 \ No newline at end of file diff --git a/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/ingress.yaml b/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/ingress.yaml deleted file mode 100644 index 0acc7163..00000000 --- a/Api.PolicyHeaders.ContentSecurityPolicy/.kubernetes/ingress.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: ingress-%SERVICE_NAME% - namespace: %KUBERNETES_NAMESPACE% -spec: - ingressClassName: nginx - tls: - - hosts: - - %CERTIFICATE_HOST% - secretName: %CERTIFICATE_HOST%-tls - rules: - - host: %CERTIFICATE_HOST% - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: %SERVICE_NAME% - port: - number: 8080 diff --git a/Api.PolicyHeaders.ContentSecurityPolicy/Api.PolicyHeaders.ContentSecurityPolicy.sln b/Api.PolicyHeaders.ContentSecurityPolicy/Api.PolicyHeaders.ContentSecurityPolicy.sln index ed2a826c..39b83d4c 100644 --- a/Api.PolicyHeaders.ContentSecurityPolicy/Api.PolicyHeaders.ContentSecurityPolicy.sln +++ b/Api.PolicyHeaders.ContentSecurityPolicy/Api.PolicyHeaders.ContentSecurityPolicy.sln @@ -19,10 +19,10 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml - .kubernetes\certificate.yaml = .kubernetes\certificate.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml - .kubernetes\ingress.yaml = .kubernetes\ingress.yaml + .kubernetes\httproute-443.yaml = .kubernetes\httproute-443.yaml + .kubernetes\httproute-80.yaml = .kubernetes\httproute-80.yaml .kubernetes\service.yaml = .kubernetes\service.yaml EndProjectSection EndProject diff --git a/Api.PolicyHeaders.ContentSecurityPolicy/README.md b/Api.PolicyHeaders.ContentSecurityPolicy/README.md index 4b49d6fc..a8c3b1b0 100644 --- a/Api.PolicyHeaders.ContentSecurityPolicy/README.md +++ b/Api.PolicyHeaders.ContentSecurityPolicy/README.md @@ -35,7 +35,7 @@ To observe CSP violations in action, load the provided `csp-violation.html` file | ---------------------------------------- | --------------------------------------------------------------- | | `http://localhost:8080/api/examples/csp` | Returns a `200 OK` response including the CSP response header. | -> 📖 Learn more about **[Nano Content Security Options](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#content-type-options)**. +> 📖 Learn more about **[Nano Content Security Options](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#content-type-options)**. ## Configuration Added content security policy header configuration. diff --git a/Api.PolicyHeaders.ContentTypeOptions/.github/workflows/build-and-deploy.yml b/Api.PolicyHeaders.ContentTypeOptions/.github/workflows/build-and-deploy.yml index 1bd59324..4010720a 100644 --- a/Api.PolicyHeaders.ContentTypeOptions/.github/workflows/build-and-deploy.yml +++ b/Api.PolicyHeaders.ContentTypeOptions/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.PolicyHeaders.ContentTypeOptions IMAGE_NAME: api.policyheaders.contenttypeoptions @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.PolicyHeaders.ContentTypeOptions/.kubernetes/deployment.yaml b/Api.PolicyHeaders.ContentTypeOptions/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.PolicyHeaders.ContentTypeOptions/.kubernetes/deployment.yaml +++ b/Api.PolicyHeaders.ContentTypeOptions/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.PolicyHeaders.ContentTypeOptions/README.md b/Api.PolicyHeaders.ContentTypeOptions/README.md index 7d5bc816..8e11728e 100644 --- a/Api.PolicyHeaders.ContentTypeOptions/README.md +++ b/Api.PolicyHeaders.ContentTypeOptions/README.md @@ -27,7 +27,7 @@ served with an incorrect `.txt` content-type. | -------------------------------------------- | ---------------------------------------------------------------------------------- | | `http://localhost:8080/api/examples/nosniff` | Returns a `200 OK` response including the Content-Type `nosniff` response header. | -> 📖 Learn more about **[Nano Content Type Header](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#content-type-options)**. +> 📖 Learn more about **[Nano Content Type Header](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#content-type-options)**. ## Configuration ```json diff --git a/Api.PolicyHeaders.Cors/.github/workflows/build-and-deploy.yml b/Api.PolicyHeaders.Cors/.github/workflows/build-and-deploy.yml index b3a31729..e53037ec 100644 --- a/Api.PolicyHeaders.Cors/.github/workflows/build-and-deploy.yml +++ b/Api.PolicyHeaders.Cors/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.PolicyHeaders.Cors IMAGE_NAME: api.policyheaders.cors @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.PolicyHeaders.Cors/.kubernetes/deployment.yaml b/Api.PolicyHeaders.Cors/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.PolicyHeaders.Cors/.kubernetes/deployment.yaml +++ b/Api.PolicyHeaders.Cors/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.PolicyHeaders.Cors/README.md b/Api.PolicyHeaders.Cors/README.md index d9c20aa9..fbff4206 100644 --- a/Api.PolicyHeaders.Cors/README.md +++ b/Api.PolicyHeaders.Cors/README.md @@ -28,7 +28,7 @@ Also try out the endpoint, and observe how CORS returns the allowed hosts, heade | ----------------------------------------- | ----------------------------------------------------------------- | | `http://localhost:8080/api/examples/cors` | Returns a `200 OK` response including the CORS response headers. | -> 📖 Learn more about **[Nano Cors](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#cors)**. +> 📖 Learn more about **[Nano Cors](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#cors)**. ## Configuration ```json diff --git a/Api.PolicyHeaders.ForwardedHeaders/.github/workflows/build-and-deploy.yml b/Api.PolicyHeaders.ForwardedHeaders/.github/workflows/build-and-deploy.yml index d8c63ecc..2d9f7da3 100644 --- a/Api.PolicyHeaders.ForwardedHeaders/.github/workflows/build-and-deploy.yml +++ b/Api.PolicyHeaders.ForwardedHeaders/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.PolicyHeaders.ForwardedHeaders IMAGE_NAME: api.policyheaders.forwardedheaders @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.PolicyHeaders.ForwardedHeaders/.kubernetes/deployment.yaml b/Api.PolicyHeaders.ForwardedHeaders/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.PolicyHeaders.ForwardedHeaders/.kubernetes/deployment.yaml +++ b/Api.PolicyHeaders.ForwardedHeaders/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.PolicyHeaders.ForwardedHeaders/README.md b/Api.PolicyHeaders.ForwardedHeaders/README.md index b39ca72b..6aaf0b8c 100644 --- a/Api.PolicyHeaders.ForwardedHeaders/README.md +++ b/Api.PolicyHeaders.ForwardedHeaders/README.md @@ -27,7 +27,7 @@ which reflect the internal service values rather than the forwarded ones. The ex | -------------------------------------------- | ------------------------------------------------------------------------ | | `http://localhost:8080/api/examples/nosniff` | Returns a `200 OK` response with `HttpContext` forwarded header values. | -> 📖 Learn more about **[Nano Forwarded Headers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#forwarded-headers)**. +> 📖 Learn more about **[Nano Forwarded Headers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#forwarded-headers)**. ## Configuration ```json diff --git a/Api.PolicyHeaders.FrameOptions/.github/workflows/build-and-deploy.yml b/Api.PolicyHeaders.FrameOptions/.github/workflows/build-and-deploy.yml index a6d34287..504d5a7e 100644 --- a/Api.PolicyHeaders.FrameOptions/.github/workflows/build-and-deploy.yml +++ b/Api.PolicyHeaders.FrameOptions/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.PolicyHeaders.FrameOptions IMAGE_NAME: api.policyheaders.frameoptions @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.PolicyHeaders.FrameOptions/.kubernetes/deployment.yaml b/Api.PolicyHeaders.FrameOptions/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.PolicyHeaders.FrameOptions/.kubernetes/deployment.yaml +++ b/Api.PolicyHeaders.FrameOptions/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.PolicyHeaders.FrameOptions/README.md b/Api.PolicyHeaders.FrameOptions/README.md index 487fe6b4..af5298c6 100644 --- a/Api.PolicyHeaders.FrameOptions/README.md +++ b/Api.PolicyHeaders.FrameOptions/README.md @@ -26,7 +26,7 @@ To observe X-Frame-Options enforcement, load the `frame-options-violation.html` | ------------------------------------------------- | ----------------------------------------------------------------------------- | | `http://localhost:8080/api/examples/frameoptions` | Returns a `200 OK` response including the `X-Frame-Options` response header. | -> 📖 Learn more about **[Nano Frame Options Header](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#frame-options)**. +> 📖 Learn more about **[Nano Frame Options Header](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#frame-options)**. ## Configuration ```json diff --git a/Api.PolicyHeaders.Hsts/.github/workflows/build-and-deploy.yml b/Api.PolicyHeaders.Hsts/.github/workflows/build-and-deploy.yml index d73d6dc5..2a01df3e 100644 --- a/Api.PolicyHeaders.Hsts/.github/workflows/build-and-deploy.yml +++ b/Api.PolicyHeaders.Hsts/.github/workflows/build-and-deploy.yml @@ -1,29 +1,34 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.PolicyHeaders.Hsts IMAGE_NAME: api.policyheaders.hsts SERVICE_NAME: api-policyheaders-hsts + SUB_DOMAIN_NAME: nano VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} + AZURE_GROUP_DNS: ${{ vars.AZURE_RESOURCE_GROUP_DNS }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -33,29 +38,31 @@ env: KUBERNETES_CPU_REQUEST: 200m KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 - CERTIFICATE_ISSUER: letsencrypt-prod - CERTIFICATE_ORGANIZATION: ${{ vars.CERTIFICATE_ORGANIZATION }} - CERTIFICATE_HOST: ${{ github.ref == 'refs/heads/master' && vars.HOST_API_SUBDOMAIN + '.' + vars.PRODUCTION_HOST || vars.HOST_API_SUBDOMAIN + '.' + vars.STAGING_HOST }} ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -84,7 +91,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -100,9 +107,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -113,7 +120,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -122,47 +129,57 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; - if ($LastExitCode -ne 0) + $zoneNames = az network dns zone list -g $env:AZURE_GROUP_DNS --query "[].name" -o json | ConvertFrom-Json + + $env:ROUTE_HOST_NAMES = ( + $zoneNames | ForEach-Object { + " - $env:SUB_DOMAIN_NAME.$_" + } + ) -join "`n" + + $env:GATEWAY_NAME = kubectl get gateway -n apps -o jsonpath='{.items[0].metadata.name}' + + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f service.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; - if ($LastExitCode -ne 0) + kubectl apply -f configmap.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; - if ($LastExitCode -ne 0) + kubectl apply -f deployment.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; - if ($LastExitCode -ne 0) + kubectl apply -f autoscaler.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } - Get-Content .kubernetes/certificate.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/certificate.tmp.yaml; - sudo kubectl apply -f .kubernetes/certificate.tmp.yaml; - if ($LastExitCode -ne 0) + Get-Content .kubernetes/grafana-httproute-80.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/grafana-httproute-80.tmp.yaml; + kubectl apply -f .kubernetes/grafana-httproute-80.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } - Get-Content .kubernetes/ingress.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/ingress.tmp.yaml; - sudo kubectl apply -f .kubernetes/ingress.tmp.yaml; - if ($LastExitCode -ne 0) + Get-Content .kubernetes/grafana-httproute-443.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/grafana-httproute-443.tmp.yaml; + kubectl apply -f .kubernetes/grafana-httproute-443.tmp.yaml; + if ($LastExitCode -ne 0) { throw "error"; - }; + } - name: GitHub Release if: github.ref == 'refs/heads/master' diff --git a/Api.PolicyHeaders.Hsts/.kubernetes/certificate.yaml b/Api.PolicyHeaders.Hsts/.kubernetes/certificate.yaml deleted file mode 100644 index a37bdadc..00000000 --- a/Api.PolicyHeaders.Hsts/.kubernetes/certificate.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: %SERVICE_NAME%-nginx-tls - namespace: %KUBERNETES_NAMESPACE% -spec: - secretName: %CERTIFICATE_HOST%-tls - duration: 2160h - renewBefore: 720h - subject: - organizations: - - %CERTIFICATE_ORGANIZATION% - dnsNames: - - %CERTIFICATE_HOST% - privateKey: - rotationPolicy: Always - issuerRef: - name: %CERTIFICATE_ISSUER% - kind: ClusterIssuer diff --git a/Api.PolicyHeaders.Hsts/.kubernetes/deployment.yaml b/Api.PolicyHeaders.Hsts/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.PolicyHeaders.Hsts/.kubernetes/deployment.yaml +++ b/Api.PolicyHeaders.Hsts/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.PolicyHeaders.Hsts/.kubernetes/httproute-443.yaml b/Api.PolicyHeaders.Hsts/.kubernetes/httproute-443.yaml new file mode 100644 index 00000000..65b97a51 --- /dev/null +++ b/Api.PolicyHeaders.Hsts/.kubernetes/httproute-443.yaml @@ -0,0 +1,18 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: %SERVICE_NAME%-route + namespace: %KUBERNETES_NAMESPACE% +spec: + parentRefs: + - name: %GATEWAY_NAME% + hostnames: +%ROUTE_HOST_NAMES% + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: %SERVICE_NAME% + port: 8080 \ No newline at end of file diff --git a/Api.PolicyHeaders.Hsts/.kubernetes/httproute-80.yaml b/Api.PolicyHeaders.Hsts/.kubernetes/httproute-80.yaml new file mode 100644 index 00000000..f29775aa --- /dev/null +++ b/Api.PolicyHeaders.Hsts/.kubernetes/httproute-80.yaml @@ -0,0 +1,17 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: %APP_NAME%-route-80 + namespace: %KUBERNETES_NAMESPACE% +spec: + parentRefs: + - name: %GATEWAY_NAME% + sectionName: http + hostnames: +%ROUTE_HOST_NAMES% + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 301 \ No newline at end of file diff --git a/Api.PolicyHeaders.Hsts/.kubernetes/ingress.yaml b/Api.PolicyHeaders.Hsts/.kubernetes/ingress.yaml deleted file mode 100644 index 0acc7163..00000000 --- a/Api.PolicyHeaders.Hsts/.kubernetes/ingress.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: ingress-%SERVICE_NAME% - namespace: %KUBERNETES_NAMESPACE% -spec: - ingressClassName: nginx - tls: - - hosts: - - %CERTIFICATE_HOST% - secretName: %CERTIFICATE_HOST%-tls - rules: - - host: %CERTIFICATE_HOST% - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: %SERVICE_NAME% - port: - number: 8080 diff --git a/Api.PolicyHeaders.Hsts/Api.PolicyHeaders.Hsts.sln b/Api.PolicyHeaders.Hsts/Api.PolicyHeaders.Hsts.sln index 4ff64a43..c3c334db 100644 --- a/Api.PolicyHeaders.Hsts/Api.PolicyHeaders.Hsts.sln +++ b/Api.PolicyHeaders.Hsts/Api.PolicyHeaders.Hsts.sln @@ -19,10 +19,10 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject .kubernetes\autoscaler.yaml = .kubernetes\autoscaler.yaml - .kubernetes\certificate.yaml = .kubernetes\certificate.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml - .kubernetes\ingress.yaml = .kubernetes\ingress.yaml + .kubernetes\httproute-443.yaml = .kubernetes\httproute-443.yaml + .kubernetes\httproute-80.yaml = .kubernetes\httproute-80.yaml .kubernetes\service.yaml = .kubernetes\service.yaml EndProjectSection EndProject diff --git a/Api.PolicyHeaders.Hsts/README.md b/Api.PolicyHeaders.Hsts/README.md index 47f8c078..0863fd81 100644 --- a/Api.PolicyHeaders.Hsts/README.md +++ b/Api.PolicyHeaders.Hsts/README.md @@ -27,7 +27,7 @@ To observe HSTS enforcement in action, load the `hsts-violation.html` file and s | ----------------------------------------- | --------------------------------------------------------------------------------------- | | `http://localhost:8080/api/examples/hsts` | Returns a `200 OK` response including the `Strict-Transform-Security` response header. | -> 📖 Learn more about **[Nano Strict Transport Security (HSTS)](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#strict-transport-security-hsts)**. +> 📖 Learn more about **[Nano Strict Transport Security (HSTS)](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#strict-transport-security-hsts)**. ## Configuration ```json diff --git a/Api.PolicyHeaders.ReferrerPolicy/.github/workflows/build-and-deploy.yml b/Api.PolicyHeaders.ReferrerPolicy/.github/workflows/build-and-deploy.yml index 0cc47f35..9ae23278 100644 --- a/Api.PolicyHeaders.ReferrerPolicy/.github/workflows/build-and-deploy.yml +++ b/Api.PolicyHeaders.ReferrerPolicy/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.PolicyHeaders.ReferrerPolicy IMAGE_NAME: api.policyheaders.referrerpolicy @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.PolicyHeaders.ReferrerPolicy/.kubernetes/deployment.yaml b/Api.PolicyHeaders.ReferrerPolicy/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.PolicyHeaders.ReferrerPolicy/.kubernetes/deployment.yaml +++ b/Api.PolicyHeaders.ReferrerPolicy/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.PolicyHeaders.ReferrerPolicy/README.md b/Api.PolicyHeaders.ReferrerPolicy/README.md index 59834f2e..2418a357 100644 --- a/Api.PolicyHeaders.ReferrerPolicy/README.md +++ b/Api.PolicyHeaders.ReferrerPolicy/README.md @@ -27,7 +27,7 @@ in the resulting request. | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `http://localhost:8080/api/examples/referrer-policy` | Returns a `200 OK` response including the `Referrer-Policy` response header set to `same-origin`. | -> 📖 Learn more about **[Nano Referrer Policy Header](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#referrer-policy)**. +> 📖 Learn more about **[Nano Referrer Policy Header](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#referrer-policy)**. ## Configuration ```json diff --git a/Api.PolicyHeaders.Robots/.github/workflows/build-and-deploy.yml b/Api.PolicyHeaders.Robots/.github/workflows/build-and-deploy.yml index 3c059778..3699c699 100644 --- a/Api.PolicyHeaders.Robots/.github/workflows/build-and-deploy.yml +++ b/Api.PolicyHeaders.Robots/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.PolicyHeaders.Robots IMAGE_NAME: api.policyheaders.robots @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.PolicyHeaders.Robots/.kubernetes/deployment.yaml b/Api.PolicyHeaders.Robots/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.PolicyHeaders.Robots/.kubernetes/deployment.yaml +++ b/Api.PolicyHeaders.Robots/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.PolicyHeaders.Robots/README.md b/Api.PolicyHeaders.Robots/README.md index 7aa12a4e..db3d38e3 100644 --- a/Api.PolicyHeaders.Robots/README.md +++ b/Api.PolicyHeaders.Robots/README.md @@ -28,7 +28,7 @@ The following endpoint is available for testing. | -------------------------------------------- | -------------------------------------------------------------------------- | | `http://localhost:8080/api/examples/nosniff` | Returns a `200 OK` response including the `X-Robots-Tag` response header. | -> 📖 Learn more about **[Nano Content Type Header](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#robots)**. +> 📖 Learn more about **[Nano Content Type Header](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#robots)**. ## Configuration ```json diff --git a/Api.PolicyHeaders.XssProtection/.github/workflows/build-and-deploy.yml b/Api.PolicyHeaders.XssProtection/.github/workflows/build-and-deploy.yml index 17384a75..8673f1f9 100644 --- a/Api.PolicyHeaders.XssProtection/.github/workflows/build-and-deploy.yml +++ b/Api.PolicyHeaders.XssProtection/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.PolicyHeaders.XssProtection IMAGE_NAME: api.policyheaders.xssprotection @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.PolicyHeaders.XssProtection/.kubernetes/deployment.yaml b/Api.PolicyHeaders.XssProtection/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.PolicyHeaders.XssProtection/.kubernetes/deployment.yaml +++ b/Api.PolicyHeaders.XssProtection/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.PolicyHeaders.XssProtection/README.md b/Api.PolicyHeaders.XssProtection/README.md index 0b4af4e4..4070cb9f 100644 --- a/Api.PolicyHeaders.XssProtection/README.md +++ b/Api.PolicyHeaders.XssProtection/README.md @@ -35,7 +35,7 @@ The following endpoint is available for testing. | ---------------------------------------- | ------------------------------------------------------------------------------ | | `http://localhost:8080/api/examples/xss` | Returns a `200 OK` response including the `X-XSS-Protection` response header. | -> 📖 Learn more about **[Nano Xxs Protection Header](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#xxs-protection)**. +> 📖 Learn more about **[Nano Xxs Protection Header](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#xxs-protection)**. ## Configuration ```json diff --git a/Api.RequestTracing/.github/workflows/build-and-deploy.yml b/Api.RequestTracing/.github/workflows/build-and-deploy.yml index 545c2221..0f92a439 100644 --- a/Api.RequestTracing/.github/workflows/build-and-deploy.yml +++ b/Api.RequestTracing/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.RequestTracing IMAGE_NAME: api.requesttracing @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.RequestTracing/.kubernetes/deployment.yaml b/Api.RequestTracing/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.RequestTracing/.kubernetes/deployment.yaml +++ b/Api.RequestTracing/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.RequestTracing/README.md b/Api.RequestTracing/README.md index 6d4393b2..60eb9dd7 100644 --- a/Api.RequestTracing/README.md +++ b/Api.RequestTracing/README.md @@ -28,4 +28,4 @@ The following endpoint is available for testing. | ----------------------------------------------------- | -------------------------------------- | | `http://localhost:8080/api/examples/request-tracing` | Returns a simple `200 OK` response. | -> 📖 Learn more about **[Nano Request Tracing](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#request-tracing)**. +> 📖 Learn more about **[Nano Request Tracing](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#request-tracing)**. diff --git a/Api.ResponseCache/.github/workflows/build-and-deploy.yml b/Api.ResponseCache/.github/workflows/build-and-deploy.yml index 28fe868d..b01ba103 100644 --- a/Api.ResponseCache/.github/workflows/build-and-deploy.yml +++ b/Api.ResponseCache/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.ResponseCache IMAGE_NAME: api.responsecache @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.ResponseCache/.kubernetes/deployment.yaml b/Api.ResponseCache/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.ResponseCache/.kubernetes/deployment.yaml +++ b/Api.ResponseCache/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.ResponseCache/README.md b/Api.ResponseCache/README.md index 891d1367..9f7cecfe 100644 --- a/Api.ResponseCache/README.md +++ b/Api.ResponseCache/README.md @@ -29,7 +29,7 @@ The following endpoint is available for testing. | `http://localhost:8080/api/examples/response-cache` | Returns a `200 OK` response cached. Header: `Cache-Control=public, max-age=1200` | | `http://localhost:8080/api/examples/no-response-cache` | Returns a `200 OK` response with no cache using Header: `[ResponseCache]`. `Cache-Control=no-store,no-cache` | -> 📖 Learn more about **[Nano Response Cache](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#response-cache)**. +> 📖 Learn more about **[Nano Response Cache](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#response-cache)**. ## Configuration ```json diff --git a/Api.ResponseCompression/.github/workflows/build-and-deploy.yml b/Api.ResponseCompression/.github/workflows/build-and-deploy.yml index f3d94087..5006980a 100644 --- a/Api.ResponseCompression/.github/workflows/build-and-deploy.yml +++ b/Api.ResponseCompression/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.ResponseCompression IMAGE_NAME: api.responsecompression @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.ResponseCompression/.kubernetes/deployment.yaml b/Api.ResponseCompression/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.ResponseCompression/.kubernetes/deployment.yaml +++ b/Api.ResponseCompression/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.ResponseCompression/README.md b/Api.ResponseCompression/README.md index befe3eb6..fa87296c 100644 --- a/Api.ResponseCompression/README.md +++ b/Api.ResponseCompression/README.md @@ -29,7 +29,7 @@ The following endpoint is available for testing. | --------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `http://localhost:8080/api/examples/response-compression` | Returns a `200 OK` response. Headers: `Content-Encoding: gzip` and `Vary: Accept-Encoding` | -> 📖 Learn more about **[Nano Response Compression](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#response-compression)**. +> 📖 Learn more about **[Nano Response Compression](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#response-compression)**. ## Configuration ```json diff --git a/Api.Session/.github/workflows/build-and-deploy.yml b/Api.Session/.github/workflows/build-and-deploy.yml index e923dbb2..37ef11b0 100644 --- a/Api.Session/.github/workflows/build-and-deploy.yml +++ b/Api.Session/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Session IMAGE_NAME: api.session @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Session/.kubernetes/deployment.yaml b/Api.Session/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.Session/.kubernetes/deployment.yaml +++ b/Api.Session/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Session/README.md b/Api.Session/README.md index 1fcd95b4..bb7269a1 100644 --- a/Api.Session/README.md +++ b/Api.Session/README.md @@ -30,7 +30,7 @@ The following endpoints is available for testing. | `http://localhost:8080/api/examples/get-session` | Gets the session variable if set and returns a `200 OK`. | | `http://localhost:8080/api/examples/clear-session` | Clear the session and returns a `200 OK`. | -> 📖 Learn more about **[Nano Session](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#session)**. +> 📖 Learn more about **[Nano Session](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#session)**. ## Configuration ```json diff --git a/Api.StartupTasks/.github/workflows/build-and-deploy.yml b/Api.StartupTasks/.github/workflows/build-and-deploy.yml index 2c7519ac..07fe4bdc 100644 --- a/Api.StartupTasks/.github/workflows/build-and-deploy.yml +++ b/Api.StartupTasks/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.StartupTasks IMAGE_NAME: api.startuptasks @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.StartupTasks/.kubernetes/deployment.yaml b/Api.StartupTasks/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.StartupTasks/.kubernetes/deployment.yaml +++ b/Api.StartupTasks/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.StartupTasks/Api.StartupTasks/Startup/ExampleStartupTask.cs b/Api.StartupTasks/Api.StartupTasks/Startup/ExampleStartupTask.cs index fd82827a..83be829c 100644 --- a/Api.StartupTasks/Api.StartupTasks/Startup/ExampleStartupTask.cs +++ b/Api.StartupTasks/Api.StartupTasks/Startup/ExampleStartupTask.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Nano.App.Startup; +using Nano.App.StartUp; namespace Api.StartupTasks.Startup; diff --git a/Api.StaticFiles/.github/workflows/build-and-deploy.yml b/Api.StaticFiles/.github/workflows/build-and-deploy.yml index 670fae49..cdc4dcb8 100644 --- a/Api.StaticFiles/.github/workflows/build-and-deploy.yml +++ b/Api.StaticFiles/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.StaticFiles IMAGE_NAME: api.staticfiles @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.StaticFiles/.kubernetes/deployment.yaml b/Api.StaticFiles/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.StaticFiles/.kubernetes/deployment.yaml +++ b/Api.StaticFiles/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.StaticFiles/README.md b/Api.StaticFiles/README.md index 79e676a1..aec8ceff 100644 --- a/Api.StaticFiles/README.md +++ b/Api.StaticFiles/README.md @@ -28,4 +28,4 @@ The following endpoint is available for testing: | -------------------------------------------------- | -------------------------------------- | | `http://localhost:8080/api/examples/static-files` | Returns a simple `200 OK` response. | -> 📖 Learn more about **[Nano Static Files](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#static-files)**. +> 📖 Learn more about **[Nano Static Files](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#static-files)**. diff --git a/Api.Storage.Azure/.github/workflows/build-and-deploy.yml b/Api.Storage.Azure/.github/workflows/build-and-deploy.yml index f2ad4d85..2b882124 100644 --- a/Api.Storage.Azure/.github/workflows/build-and-deploy.yml +++ b/Api.Storage.Azure/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Storage.Azure IMAGE_NAME: api.storage.azure @@ -8,22 +13,22 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} + AZURE_GROUP_STORAGE: ${{ vars.AZURE_STORAGE_RESOURCE_GROUP }} + AZURE_GROUP_BACKUP: ${{ vars.AZURE_BACKUP_RESOURCE_GROUP }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -33,30 +38,33 @@ env: KUBERNETES_CPU_REQUEST: 200m KUBERNETES_CPU_LIMIT: 600m KUBERNETES_CPU_SCALING: 180 - STORAGE_SHARE_NAME: nano-storage-azure - STORAGE_CREDENTIALS_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_STORAGE_CREDENTIALS_ID || secrets.STAGING_STORAGE_CREDENTIALS_ID }} - STORAGE_CREDENTIALS_SECRET: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_STORAGE_CREDENTIALS_SECRET || secrets.STAGING_STORAGE_CREDENTIALS_SECRET }} STORAGE_SIZE: 1000 + STORAGE_SHARE_NAME: nano-storage-azure ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,14 +89,41 @@ jobs: - name: Create Fileshare shell: pwsh run: | - $env:EXISTING_FILE_SHARE = sudo az storage share list --account-name $env:STORAGE_CREDENTIALS_ID --account-key $env:STORAGE_CREDENTIALS_SECRET --query "[?contains(name, '$env:STORAGE_SHARE_NAME')].[name]" -o tsv; - if ([string]::IsNullOrEmpty($env:EXISTING_FILE_SHARE)) + $env:STORAGE_ACCOUNT_NAME = az storage account list -g $env:AZURE_GROUP_STORAGE --query [0].name -o tsv; + + $env:FILE_SHARE_EXISTS = az storage share-rm exists ` + -g $env:AZURE_GROUP_STORAGE ` + -n $env:STORAGE_SHARE_NAME ` + --storage-account $env:STORAGE_ACCOUNT_NAME ` + --query exists; + + if ($env:FILE_SHARE_EXISTS -eq "false") { - sudo az storage share create -n $env:STORAGE_SHARE_NAME --account-name $env:STORAGE_CREDENTIALS_ID --account-key $env:STORAGE_CREDENTIALS_SECRET --quota $env:STORAGE_SIZE; + az storage share-rm create ` + -g $env:AZURE_GROUP_STORAGE ` + -n $env:STORAGE_SHARE_NAME ` + --storage-account $env:STORAGE_ACCOUNT_NAME ` + --access-tier TransactionOptimized ` + --quota $env:STORAGE_SIZE; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:BACKUP_VAULT_NAME = az backup vault list -g $env:AZURE_GROUP_BACKUP --query [0].name -o tsv; + + az backup protection enable-for-azurefileshare ` + -g $env:AZURE_GROUP_BACKUP ` + -v $env:BACKUP_VAULT_NAME ` + -p $env:STORAGE_ACCOUNT_NAME-backup-policy ` + --storage-account $env:STORAGE_ACCOUNT_NAME ` + --azure-file-share $env:STORAGE_SHARE_NAME; + if ($LastExitCode -ne 0) { throw "error"; - }; + }; } - name: Publish Image @@ -98,7 +133,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -114,9 +149,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -127,7 +162,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -136,29 +171,43 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/storage-pv.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/storage-pv.tmp.yaml; + kubectl apply -f .kubernetes/storage-pv.tmp.yaml; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + Get-Content .kubernetes/storage-pvc.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/storage-pvc.tmp.yaml; + kubectl apply -f .kubernetes/storage-pvc.tmp.yaml; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Storage.Azure/.kubernetes/configmap.yaml b/Api.Storage.Azure/.kubernetes/configmap.yaml index 3977c0ee..b64a515d 100644 --- a/Api.Storage.Azure/.kubernetes/configmap.yaml +++ b/Api.Storage.Azure/.kubernetes/configmap.yaml @@ -5,4 +5,5 @@ metadata: namespace: %KUBERNETES_NAMESPACE% data: App__Version: %VERSION% + Storage__HealthCheck__AccountName: %STORAGE_ACCOUNT_NAME% ASPNETCORE_ENVIRONMENT: %ASPNETCORE_ENVIRONMENT% diff --git a/Api.Storage.Azure/.kubernetes/deployment.yaml b/Api.Storage.Azure/.kubernetes/deployment.yaml index a9ada48b..9eb44e22 100644 --- a/Api.Storage.Azure/.kubernetes/deployment.yaml +++ b/Api.Storage.Azure/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -37,17 +37,6 @@ spec: ports: - containerPort: 8080 imagePullPolicy: Always - env: - - name: Storage__Credentials__id - valueFrom: - secretKeyRef: - name: storage-account-secret - key: azurestorageaccountname - - name: Storage__Credentials__Secret - valueFrom: - secretKeyRef: - name: storage-account-secret - key: azurestorageaccountkey volumeMounts: - name: tmp mountPath: /tmp @@ -96,10 +85,8 @@ spec: mountPath: /tmp volumes: - name: %SERVICE_NAME%-volume - azureFile: - secretName: storage-account-secret - shareName: %STORAGE_SHARE_NAME% - readOnly: false + persistentVolumeClaim: + claimName: %SERVICE_NAME%-azurefile-pvc - name: tmp emptyDir: {} imagePullSecrets: diff --git a/Api.Storage.Azure/.kubernetes/storage-pv.yaml b/Api.Storage.Azure/.kubernetes/storage-pv.yaml new file mode 100644 index 00000000..b6281fb8 --- /dev/null +++ b/Api.Storage.Azure/.kubernetes/storage-pv.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: %SERVICE_NAME%-azurefile-pv +spec: + capacity: + storage: %STORAGE_SIZE% + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: azurefile-static + csi: + driver: file.csi.azure.com + volumeHandle: %STORAGE_SHARE_NAME% + volumeAttributes: + shareName: %STORAGE_SHARE_NAME% + storageAccount: %STORAGE_ACCOUNT_NAME% \ No newline at end of file diff --git a/Api.Storage.Azure/.kubernetes/storage-pvc.yaml b/Api.Storage.Azure/.kubernetes/storage-pvc.yaml new file mode 100644 index 00000000..252c7857 --- /dev/null +++ b/Api.Storage.Azure/.kubernetes/storage-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: %SERVICE_NAME%-azurefile-pvc + namespace: %KUBERNETES_NAMESPACE% +spec: + accessModes: + - ReadWriteMany + storageClassName: azurefile-static + resources: + requests: + storage: %STORAGE_SIZE% \ No newline at end of file diff --git a/Api.Storage.Azure/Api.Storage.Azure.sln b/Api.Storage.Azure/Api.Storage.Azure.sln index 12674d65..a75a6568 100644 --- a/Api.Storage.Azure/Api.Storage.Azure.sln +++ b/Api.Storage.Azure/Api.Storage.Azure.sln @@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes" .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\deployment.yaml = .kubernetes\deployment.yaml .kubernetes\service.yaml = .kubernetes\service.yaml + .kubernetes\storage-pv.yaml = .kubernetes\storage-pv.yaml + .kubernetes\storage-pvc.yaml = .kubernetes\storage-pvc.yaml EndProjectSection EndProject Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", ".docker\docker-compose.dcproj", "{557A0C48-DA6A-4D7C-8668-94F08A390F4B}" diff --git a/Api.Storage.Azure/Api.Storage.Azure/appsettings.json b/Api.Storage.Azure/Api.Storage.Azure/appsettings.json index bfbfee6d..645e4243 100644 --- a/Api.Storage.Azure/Api.Storage.Azure/appsettings.json +++ b/Api.Storage.Azure/Api.Storage.Azure/appsettings.json @@ -14,11 +14,8 @@ }, "Storage": { "ShareName": "nano-storage-azure", - "Credentials": { - "Id": null, - "Secret": null - }, "HealthCheck": { + "AccountName": null, "UnhealthyStatus": "Degraded" } } diff --git a/Api.Storage.Azure/README.md b/Api.Storage.Azure/README.md index a64706ca..b4382605 100644 --- a/Api.Storage.Azure/README.md +++ b/Api.Storage.Azure/README.md @@ -33,7 +33,7 @@ credentials are omitted from the configuration, the application will still run, Open [http://localhost:8080/healthz](http://localhost:8080/healthz) to view the storage health-check JSON response. -> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#health-checks)**. +> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#health-checks)**. The following endpoint is available for testing. @@ -41,7 +41,7 @@ The following endpoint is available for testing. | --------------------------------------------------- | ------------------------------------------------------------------------------- | | `http://localhost:8080/api/examples/storage` | Returns a simple `200 OK` response. Saves the uploaded file to the fileshare. | -> 📖 Learn more about **[Nano.Storage.Azure](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Storage.Azure)**. +> 📖 Learn more about **[Nano.Storage.Azure](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Storage.Azure/README#nanostorageazure)**. ## Registration The following storage provider has been registered using `ConfigureServices(...)` in `program.cs`. @@ -61,11 +61,8 @@ Add the storage configuration. ```json "Storage": { "ShareName": "nano-storage-azure", - "Credentials": { - "Id": "id", - "Secret": "secret" - }, "HealthCheck": { + "AccountName": null, "UnhealthyStatus": "Degraded" } } @@ -76,9 +73,6 @@ Additionally, application health-checks have been enabled with the configuration ```json "App": { "HealthCheck": { - "EvaluationInterval": 10, - "FailureNotificationInterval": 60, - "MaximumHistoryEntriesPerEndpoint": 50 } } ``` @@ -93,48 +87,37 @@ docker ``` ## Kubernetes -Added the volumes, volume mounts and secrets to the `deployment.yaml`. +Added two new kubernetes templaets, the `storage-pv.yaml` and `storage-pvc.yaml`. Updated the `deployment.yaml` mounting the volume. -```json +```yaml spec: template: spec: containers: - env: - - name: Storage__Credentials__Id - valueFrom: - secretKeyRef: - name: storage-account-secret - key: azurestorageaccountname - - name: Storage__Credentials__Secret - valueFrom: - secretKeyRef: - name: storage-account-secret - key: azurestorageaccountkey volumeMounts: - - name: tmp - mountPath: /tmp - name: %SERVICE_NAME%-volume mountPath: /mnt/%STORAGE_SHARE_NAME% + - name: tmp + mountPath: /tmp volumes: + - name: %SERVICE_NAME%-volume + persistentVolumeClaim: + claimName: %SERVICE_NAME%-azurefile-pvc - name: tmp emptyDir: {} - - name: %SERVICE_NAME%-volume - azureFile: - secretName: storage-account-secret - shareName: %STORAGE_SHARE_NAME% - readOnly: false ``` +Additionally, the `configmap.yaml` file stores the `$env:STORAGE_ACCOUNT_NAME` value, which is used by the TCP-based health check to validate connectivity to the Azure File Share endpoint. + ## GitHub Actions Add the following environment variables to the `buid-and-deply.yml`. ```yaml env: - STORAGE_SHARE_NAME: nano-storage-azure - STORAGE_CREDENTIALS_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_STORAGE_CREDENTIALS_ID || secrets.STAGING_STORAGE_CREDENTIALS_ID }} - STORAGE_CREDENTIALS_SECRET: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_STORAGE_CREDENTIALS_SECRET || secrets.STAGING_STORAGE_CREDENTIALS_SECRET }} + AZURE_GROUP_BACKUP: ${{ vars.AZURE_BACKUP_RESOURCE_GROUP }} + AZURE_GROUP_STORAGE: ${{ vars.AZURE_STORAGE_RESOURCE_GROUP }} STORAGE_SIZE: 1000 + STORAGE_SHARE_NAME: nano-storage-azure ``` And add this step below as well, ensuring that the fileshare gets created before the application is deployed. @@ -143,13 +126,40 @@ And add this step below as well, ensuring that the fileshare gets created before - name: Create Fileshare shell: pwsh run: | - $env:EXISTING_FILE_SHARE = sudo az storage share list --account-name $env:STORAGE_CREDENTIALS_ID --account-key $env:STORAGE_CREDENTIALS_SECRET --query "[?contains(name, '$env:STORAGE_SHARE_NAME')].[name]" -o tsv; - if ([string]::IsNullOrEmpty($env:EXISTING_FILE_SHARE)) + $env:STORAGE_ACCOUNT_NAME = sudo az storage account list -g $env:AZURE_GROUP_STORAGE --query [0].name -o tsv; + + $env:FILE_SHARE_EXISTS = sudo az storage share-rm exists ` + -g $env:AZURE_GROUP_STORAGE ` + -n $env:STORAGE_SHARE_NAME ` + --storage-account $env:STORAGE_ACCOUNT_NAME ` + --query exists; + + if ($env:FILE_SHARE_EXISTS -eq "false") { - sudo az storage share create -n $env:STORAGE_SHARE_NAME --account-name $env:STORAGE_CREDENTIALS_ID --account-key $env:STORAGE_CREDENTIALS_SECRET --quota $env:STORAGE_SIZE; + sudo az storage share-rm create ` + -g $env:AZURE_GROUP_STORAGE ` + -n $env:STORAGE_SHARE_NAME ` + --storage-account $env:STORAGE_ACCOUNT_NAME ` + --access-tier TransactionOptimized ` + --quota $env:STORAGE_SIZE; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:BACKUP_VAULT_NAME = sudo az backup vault list -g $env:AZURE_GROUP_BACKUP --query [0].name -o tsv; + + sudo az backup protection enable-for-azurefileshare ` + -g $env:AZURE_GROUP_BACKUP ` + -v $env:BACKUP_VAULT_NAME ` + -p $env:STORAGE_ACCOUNT_NAME-backup-policy ` + --storage-account $env:STORAGE_ACCOUNT_NAME ` + --azure-file-share $env:STORAGE_SHARE_NAME; + if ($LastExitCode -ne 0) { throw "error"; - }; + }; } ``` \ No newline at end of file diff --git a/Api.Storage.Local/.github/workflows/build-and-deploy.yml b/Api.Storage.Local/.github/workflows/build-and-deploy.yml index b5c5102d..3044ba11 100644 --- a/Api.Storage.Local/.github/workflows/build-and-deploy.yml +++ b/Api.Storage.Local/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Storage.Local IMAGE_NAME: api.storage.local @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -32,27 +35,33 @@ env: KUBERNETES_MEMORY_SCALING: 180 KUBERNETES_CPU_REQUEST: 200m KUBERNETES_CPU_LIMIT: 600m + STORAGE_SIZE: 1000 STORAGE_SHARE_NAME: nano-storage-local ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +90,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +106,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +119,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -120,42 +129,42 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/storage-storageclass.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/storage-storageclass.tmp.yaml; - sudo kubectl apply -f .kubernetes/storage-storageclass.tmp.yaml; + kubectl apply -f .kubernetes/storage-storageclass.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/storage-pvc.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/storage-pvc.tmp.yaml; - sudo kubectl apply -f .kubernetes/storage-pvc.tmp.yaml; + kubectl apply -f .kubernetes/storage-pvc.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; - }; + }; Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Storage.Local/.kubernetes/deployment.yaml b/Api.Storage.Local/.kubernetes/deployment.yaml index 9dd316cd..eb1c9093 100644 --- a/Api.Storage.Local/.kubernetes/deployment.yaml +++ b/Api.Storage.Local/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux diff --git a/Api.Storage.Local/.kubernetes/storage-pvc.yaml b/Api.Storage.Local/.kubernetes/storage-pvc.yaml index 2285607e..ca5fdd40 100644 --- a/Api.Storage.Local/.kubernetes/storage-pvc.yaml +++ b/Api.Storage.Local/.kubernetes/storage-pvc.yaml @@ -2,6 +2,7 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: %SERVICE_NAME%-pvc + namespace: %KUBERNETES_NAMESPACE% spec: accessModes: - ReadWriteOnce diff --git a/Api.Storage.Local/README.md b/Api.Storage.Local/README.md index 5cf3390e..c243734f 100644 --- a/Api.Storage.Local/README.md +++ b/Api.Storage.Local/README.md @@ -36,7 +36,7 @@ The following endpoint is available for testing: | --------------------------------------------- | ------------------------------------------------------------------------------- | | `http://localhost:8080/api/examples/storage` | Returns a simple `200 OK` response. Saves the uploaded file to the fileshare. | -> 📖 Learn more about **[Nano.Storage.Local](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Storage.Local)**. +> 📖 Learn more about **[Nano.Storage.Local](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Storage.Local/README#nanostoragelocal)**. ## Registration The following storage has been registered using `ConfigureServices(...)` in `program.cs`. @@ -67,9 +67,6 @@ Additionally, application health-checks have been enabled with the configuration ```json "App": { "HealthCheck": { - "EvaluationInterval": 10, - "FailureNotificationInterval": 60, - "MaximumHistoryEntriesPerEndpoint": 50 } } ``` @@ -88,7 +85,7 @@ Added two additional kubernetes templates, `storage-storageclass.yaml` and `stor Also, updated `deployment.yaml` adding the volumes and volume mounts. -```json +```yaml spec: template: spec: diff --git a/Api.TimeZone/.github/workflows/build-and-deploy.yml b/Api.TimeZone/.github/workflows/build-and-deploy.yml index 85c1a63e..edbde0d8 100644 --- a/Api.TimeZone/.github/workflows/build-and-deploy.yml +++ b/Api.TimeZone/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.TimeZone IMAGE_NAME: api.timezone @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.TimeZone/.kubernetes/deployment.yaml b/Api.TimeZone/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.TimeZone/.kubernetes/deployment.yaml +++ b/Api.TimeZone/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.TimeZone/README.md b/Api.TimeZone/README.md index 7734094e..70e6d283 100644 --- a/Api.TimeZone/README.md +++ b/Api.TimeZone/README.md @@ -38,9 +38,9 @@ The following endpoint is available for testing. | Endpoint | Description | | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| `http://localhost:8080/api/examples/timezone` (GET,POST) | Returns a `200 OK` response with various `DateTimeOffset` properties illustrating the date-time timezone conversions. | +| `http://localhost:8080/api/examples/timezone` (GET,POST) | Returns a `200 OK` response with various `DateTimeOffset` properties illustrating the date-time timezone conversions. | -> 📖 Learn more about **[Nano TimeZone](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#timezone)**. +> 📖 Learn more about **[Nano TimeZone](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#timezone)**. ## Configuration ```json diff --git a/Api.Versioning/.github/workflows/build-and-deploy.yml b/Api.Versioning/.github/workflows/build-and-deploy.yml index a565aa0a..3a01b8e4 100644 --- a/Api.Versioning/.github/workflows/build-and-deploy.yml +++ b/Api.Versioning/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Versioning IMAGE_NAME: api.versioning @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.Versioning/.kubernetes/deployment.yaml b/Api.Versioning/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.Versioning/.kubernetes/deployment.yaml +++ b/Api.Versioning/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.Versioning/Api.Versioning/Api.Versioning.csproj b/Api.Versioning/Api.Versioning/Api.Versioning.csproj index 205fc2c7..ef6130b1 100644 --- a/Api.Versioning/Api.Versioning/Api.Versioning.csproj +++ b/Api.Versioning/Api.Versioning/Api.Versioning.csproj @@ -31,7 +31,7 @@ - + \ No newline at end of file diff --git a/Api.Versioning/Api.Versioning/Controllers/ExamplesController.cs b/Api.Versioning/Api.Versioning/Controllers/ExamplesController.cs index 075a8173..4308de4f 100644 --- a/Api.Versioning/Api.Versioning/Controllers/ExamplesController.cs +++ b/Api.Versioning/Api.Versioning/Controllers/ExamplesController.cs @@ -4,6 +4,7 @@ using System.Net; using System.Threading; using System.Threading.Tasks; +using Asp.Versioning; namespace Api.Versioning.Controllers; diff --git a/Api.Versioning/README.md b/Api.Versioning/README.md index e91f0bd4..82f8fd69 100644 --- a/Api.Versioning/README.md +++ b/Api.Versioning/README.md @@ -32,4 +32,4 @@ The following endpoint is available for testing. | `http://localhost:8080/api/v1.0/examples/versioning` | Returns a `200 OK` response with the message `v1.0`. | | `http://localhost:8080/api/v2.0/examples/versioning` | Returns a `200 OK` response with the message `v2.0`. | -> 📖 Learn more about **[Nano Versioning](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#versioning)**. +> 📖 Learn more about **[Nano Versioning](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#versioning)**. diff --git a/Api.VirusScan/.github/workflows/build-and-deploy.yml b/Api.VirusScan/.github/workflows/build-and-deploy.yml index b1c3e79f..0e0eb79a 100644 --- a/Api.VirusScan/.github/workflows/build-and-deploy.yml +++ b/Api.VirusScan/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.VirusScan IMAGE_NAME: api.virusscan @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api.VirusScan/.kubernetes/deployment.yaml b/Api.VirusScan/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Api.VirusScan/.kubernetes/deployment.yaml +++ b/Api.VirusScan/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Api.VirusScan/README.md b/Api.VirusScan/README.md index 68a8722e..520b6ee9 100644 --- a/Api.VirusScan/README.md +++ b/Api.VirusScan/README.md @@ -26,7 +26,7 @@ This example demonstrates using virus scan for all uploaded files in a Nano appl An virus scan health check is configured. Open **[http://localhost:8080/healthz](http://localhost:8080/healthz)** to view the health-check status in the JSON response. -> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#health-checks)** +> 📖 Learn more about **[Nano Health Checks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#health-checks)** Invoke the endpoint. To test virus detection, you can use the EICAR test files available here: [https://www.eicar.org/download-anti-malware-testfile](https://www.eicar.org/download-anti-malware-testfile). @@ -37,7 +37,7 @@ The following endpoint is available for testing. | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `http://localhost:8080/api/examples/virus-scan` | Returns a `200 OK` response if there is no virus in the file, and otherwise a `500 ERROR` with the found virus name in the error message. | -> 📖 Learn more about **[Nano Virus Scan](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#virus-scan)**. +> 📖 Learn more about **[Nano Virus Scan](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#virus-scan)**. ## Configuration ```json diff --git a/Api._Blank/.github/workflows/build-and-deploy.yml b/Api._Blank/.github/workflows/build-and-deploy.yml index f90f9dda..11902a3d 100644 --- a/Api._Blank/.github/workflows/build-and-deploy.yml +++ b/Api._Blank/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Api.Blank IMAGE_NAME: api.blank @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -119,29 +127,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | - Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Api._Blank/.kubernetes/deployment.yaml b/Api._Blank/.kubernetes/deployment.yaml index fdc5dfdf..b15e7e29 100644 --- a/Api._Blank/.kubernetes/deployment.yaml +++ b/Api._Blank/.kubernetes/deployment.yaml @@ -18,8 +18,10 @@ spec: spec: automountServiceAccountToken: false securityContext: + runAsNonRoot: true runAsUser: 1000 - runAsGroup: 2000 + runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +29,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -48,12 +49,11 @@ spec: memory: %KUBERNETES_MEMORY_LIMIT% cpu: %KUBERNETES_CPU_LIMIT% securityContext: + runAsUser: 1000 + runAsGroup: 2000 privileged: false allowPrivilegeEscalation: false readOnlyRootFilesystem: true - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 2000 capabilities: drop: - ALL @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Console.CustomConfigSection/.github/workflows/build-and-deploy.yml b/Console.CustomConfigSection/.github/workflows/build-and-deploy.yml index 0dd3407a..930c99ac 100644 --- a/Console.CustomConfigSection/.github/workflows/build-and-deploy.yml +++ b/Console.CustomConfigSection/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.CustomConfigSection IMAGE_NAME: console.customconfigsection @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi @@ -33,23 +36,28 @@ env: DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -78,7 +86,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -94,9 +102,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -106,14 +114,14 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.CustomConfigSection/.kubernetes/cronjob.yaml b/Console.CustomConfigSection/.kubernetes/cronjob.yaml index 64a6dce3..32007e73 100644 --- a/Console.CustomConfigSection/.kubernetes/cronjob.yaml +++ b/Console.CustomConfigSection/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux diff --git a/Console.CustomConfigSection/README.md b/Console.CustomConfigSection/README.md index 1e3691ff..f1f29df6 100644 --- a/Console.CustomConfigSection/README.md +++ b/Console.CustomConfigSection/README.md @@ -23,7 +23,7 @@ This application demonstrates a worker that uses a custom configuration section. Run the application to see services and hosted services being resolved, and observe the console output generated by the `ExampleWorker` and the injected `CustomOptions` implementation. -> 📖 Learn more about **[Nano Custom Configuration Sections](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api#custom-configuration-section)**. +> 📖 Learn more about **[Nano Custom Configuration Sections](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Api/README.md#custom-configuration-section)**. ## Configuration A custom configuration section has been added to `appsettings.json`: diff --git a/Console.CustomService/.github/workflows/build-and-deploy.yml b/Console.CustomService/.github/workflows/build-and-deploy.yml index 60592326..784ed671 100644 --- a/Console.CustomService/.github/workflows/build-and-deploy.yml +++ b/Console.CustomService/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.CustomService IMAGE_NAME: console.customservice @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi @@ -33,23 +36,28 @@ env: DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -78,7 +86,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -94,9 +102,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -106,14 +114,14 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.CustomService/.kubernetes/cronjob.yaml b/Console.CustomService/.kubernetes/cronjob.yaml index 64a6dce3..32007e73 100644 --- a/Console.CustomService/.kubernetes/cronjob.yaml +++ b/Console.CustomService/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux diff --git a/Console.CustomService/README.md b/Console.CustomService/README.md index c1d463da..4bc057a7 100644 --- a/Console.CustomService/README.md +++ b/Console.CustomService/README.md @@ -22,7 +22,7 @@ This application demonstrates a worker that use a custom service. Run the application to see services and hosted services being resolved, and observe the console output generated by the `ExampleWorker` and the injected `IExampleService` implementation. -> 📖 Learn more about **[Nano Custom Services](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App#custom-services)**. +> 📖 Learn more about **[Nano Custom Services](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App/README.md#custom-services)**. ## Registration A custom service, `IExampleService` has been added and implemented. In `program.cs` the service is registered using `ConfigureService(...)` method as shown below diff --git a/Console.Data.InMemory/.github/workflows/build-and-deploy.yml b/Console.Data.InMemory/.github/workflows/build-and-deploy.yml index 9c77d12a..9733b697 100644 --- a/Console.Data.InMemory/.github/workflows/build-and-deploy.yml +++ b/Console.Data.InMemory/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Data.InMemory IMAGE_NAME: console.data.inmemory @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi @@ -33,23 +36,28 @@ env: DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -78,7 +86,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -94,9 +102,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -106,14 +114,14 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.Data.InMemory/.kubernetes/cronjob.yaml b/Console.Data.InMemory/.kubernetes/cronjob.yaml index 1590c0e5..71ff564c 100644 --- a/Console.Data.InMemory/.kubernetes/cronjob.yaml +++ b/Console.Data.InMemory/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux diff --git a/Console.Data.InMemory/README.md b/Console.Data.InMemory/README.md index 2be04ea4..d6cb69d4 100644 --- a/Console.Data.InMemory/README.md +++ b/Console.Data.InMemory/README.md @@ -19,15 +19,15 @@ Nano is referenced directly from source (not via NuGet packages) and is expected This application builds on **[Console.Blank](https://github.com/Nano-Core/Nano.Lessons/tree/master/Console._Blank)**. This example demonstrates how various parts of Nano data work together. All data configuration and registration have been completed, and classes have been implemented -for the data parts, including [Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-models), [Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-mappings), -and the [Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-context). +for the data parts, including **[Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-models)**, **[Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-mappings)**, +and the **[Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-context)**. Notice, that the `InMemoryProvider` doesn't require any implementation of `BaseDbContextFactory` and there is no need to add migrations. -The worker creates and retreives a `Example` entity using the Nano [Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#repositories). Run the +The worker creates and retreives a `Example` entity using the Nano **[Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#repositories)**. Run the application and observe the console output generated by the `ExampleWorker`. -> 📖 Learn more about **[Nano.Data.InMemory](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.InMemory)**. +> 📖 Learn more about **[Nano.Data.InMemory](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.InMemory/README#nanodatainmemory)**. ## Registration The following data provider has been registered using `ConfigureServices(...)` in `program.cs`. diff --git a/Console.Data.MySql/.github/workflows/build-and-deploy.yml b/Console.Data.MySql/.github/workflows/build-and-deploy.yml index 9fc579e7..7e45a90a 100644 --- a/Console.Data.MySql/.github/workflows/build-and-deploy.yml +++ b/Console.Data.MySql/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Data.MySql IMAGE_NAME: console.data.mysql @@ -8,56 +13,55 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi KUBERNETES_CPU_REQUEST: 50m KUBERNETES_CPU_LIMIT: 150m KUBERNETES_CRONJOB_SCHEDULE: "0 * * * *" - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-mysql-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_USER }};Pwd=${{ env.DATA_PASSWORD }};SslMode=Preferred; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_ADMIN_USER }};Pwd=${{ env.DATA_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -86,7 +90,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -102,57 +106,78 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName -o tsv; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort -o tsv; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING" `; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - - sudo apt-get update - sudo apt-get install -y mysql-client - - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:DATA_USER');" $env:DATA_MIGRATION_CONNECTIONSTRING; + + $userExists = mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');"; if ($userExists -eq 0) { - mysql --connect-expired-password -e " ` - CREATE USER '$env:DATA_USER'@'%' IDENTIFIED BY '$env:DATA_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:DATA_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:DATA_MIGRATION_CONNECTIONSTRING - }; + mysql ` + --host $env:SQL_HOST ` + --port $env:SQL_PORT ` + --user $env:SQL_ADMIN_USER ` + --ssl-mode=REQUIRED ` + -e "CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE ON $env:SQL_NAME.* TO '$env:SQL_USER'@'%'; FLUSH PRIVILEGES;"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + } + + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/sql-auth-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/sql-auth-secret.tmp.yaml; + kubectl apply -f .kubernetes/sql-auth-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.Data.MySql/.kubernetes/auth-sql-secret.yaml b/Console.Data.MySql/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Console.Data.MySql/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Console.Data.MySql/.kubernetes/cronjob.yaml b/Console.Data.MySql/.kubernetes/cronjob.yaml index 64e7f060..9ae388c3 100644 --- a/Console.Data.MySql/.kubernetes/cronjob.yaml +++ b/Console.Data.MySql/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -29,7 +30,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: diff --git a/Console.Data.MySql/Console.Data.MySql.sln b/Console.Data.MySql/Console.Data.MySql.sln index df93c078..7a66ecdc 100644 --- a/Console.Data.MySql/Console.Data.MySql.sln +++ b/Console.Data.MySql/Console.Data.MySql.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\cronjob.yaml = .kubernetes\cronjob.yaml EndProjectSection diff --git a/Console.Data.MySql/Console.Data.MySql/Console.Data.MySql.csproj b/Console.Data.MySql/Console.Data.MySql/Console.Data.MySql.csproj index 98fd80f3..ecb2bb5e 100644 --- a/Console.Data.MySql/Console.Data.MySql/Console.Data.MySql.csproj +++ b/Console.Data.MySql/Console.Data.MySql/Console.Data.MySql.csproj @@ -42,4 +42,8 @@ + + + + \ No newline at end of file diff --git a/Console.Data.MySql/Console.Data.MySql/Migrations/20260311110547_Initial.Designer.cs b/Console.Data.MySql/Console.Data.MySql/Migrations/20260425173327_Initial.Designer.cs similarity index 84% rename from Console.Data.MySql/Console.Data.MySql/Migrations/20260311110547_Initial.Designer.cs rename to Console.Data.MySql/Console.Data.MySql/Migrations/20260425173327_Initial.Designer.cs index 0308db61..e7296d5e 100644 --- a/Console.Data.MySql/Console.Data.MySql/Migrations/20260311110547_Initial.Designer.cs +++ b/Console.Data.MySql/Console.Data.MySql/Migrations/20260425173327_Initial.Designer.cs @@ -12,7 +12,7 @@ namespace Console.Data.MySql.Migrations { [DbContext(typeof(MySqlDbContext))] - [Migration("20260311110547_Initial")] + [Migration("20260425173327_Initial")] partial class Initial { /// @@ -20,7 +20,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("ProductVersion", "10.0.7") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -216,14 +216,24 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("datetime(6)"); b.Property("CreatedBy") + .IsRequired() .HasMaxLength(256) .HasColumnType("varchar(256)"); + b.Property("EntityKey") + .HasColumnType("char(36)"); + b.Property("EntitySetName") .HasMaxLength(256) .HasColumnType("varchar(256)"); + b.Property("EntityState") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + b.Property("EntityTypeName") + .IsRequired() .HasMaxLength(256) .HasColumnType("varchar(256)"); @@ -234,23 +244,18 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(256) .HasColumnType("varchar(256)"); - b.Property("State") - .HasColumnType("int"); - - b.Property("StateName") - .HasMaxLength(256) - .HasColumnType("varchar(256)"); - b.HasKey("Id"); b.HasIndex("CreatedBy"); + b.HasIndex("EntityKey"); + + b.HasIndex("EntityState"); + b.HasIndex("EntityTypeName"); b.HasIndex("RequestId"); - b.HasIndex("State"); - b.ToTable("__EFAudit", (string)null); }); @@ -326,6 +331,54 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("__EFIdentityApiKey", (string)null); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApiKeyId") + .HasColumnType("char(36)"); + + b.Property("ClaimType") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId", "ClaimType") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType"); + + b.ToTable("__EFIdentityApiKeyClaim", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApiKeyId") + .HasColumnType("char(36)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("ApiKeyId", "RoleId") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyRole_ApiKeyId_RoleId"); + + b.ToTable("__EFIdentityApiKeyRole", (string)null); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.Property("Id") @@ -456,10 +509,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("ExpireAt"); - b.HasIndex("IdentityUserId") - .IsUnique() - .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId"); - b.HasIndex("IdentityUserId", "AppId") .IsUnique() .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId_AppId"); @@ -540,6 +589,36 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("IdentityUser"); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + + b.Navigation("Role"); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") @@ -554,8 +633,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") - .WithOne() - .HasForeignKey("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", "IdentityUserId") + .WithMany() + .HasForeignKey("IdentityUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/Console.Data.MySql/Console.Data.MySql/Migrations/20260311110547_Initial.cs b/Console.Data.MySql/Console.Data.MySql/Migrations/20260425173327_Initial.cs similarity index 86% rename from Console.Data.MySql/Console.Data.MySql/Migrations/20260311110547_Initial.cs rename to Console.Data.MySql/Console.Data.MySql/Migrations/20260425173327_Initial.cs index 4de0291f..e6bbd48f 100644 --- a/Console.Data.MySql/Console.Data.MySql/Migrations/20260311110547_Initial.cs +++ b/Console.Data.MySql/Console.Data.MySql/Migrations/20260425173327_Initial.cs @@ -20,15 +20,14 @@ protected override void Up(MigrationBuilder migrationBuilder) columns: table => new { Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), - CreatedBy = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + CreatedBy = table.Column(type: "varchar(256)", maxLength: 256, nullable: false) .Annotation("MySql:CharSet", "utf8mb4"), + EntityKey = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), EntitySetName = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) .Annotation("MySql:CharSet", "utf8mb4"), - EntityTypeName = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) - .Annotation("MySql:CharSet", "utf8mb4"), - State = table.Column(type: "int", nullable: false), - StateName = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + EntityTypeName = table.Column(type: "varchar(256)", maxLength: 256, nullable: false) .Annotation("MySql:CharSet", "utf8mb4"), + EntityState = table.Column(type: "int", nullable: false, defaultValue: 0), RequestId = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) .Annotation("MySql:CharSet", "utf8mb4"), IsDeleted = table.Column(type: "bigint", nullable: false), @@ -349,11 +348,70 @@ protected override void Up(MigrationBuilder migrationBuilder) }) .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.CreateTable( + name: "__EFIdentityApiKeyClaim", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + ApiKeyId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + ClaimType = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ClaimValue = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityApiKeyClaim", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityApiKeyClaim___EFIdentityApiKey_ApiKeyId", + column: x => x.ApiKeyId, + principalTable: "__EFIdentityApiKey", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "__EFIdentityApiKeyRole", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + ApiKeyId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + RoleId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci") + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityApiKeyRole", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityApiKeyRole___EFIdentityApiKey_ApiKeyId", + column: x => x.ApiKeyId, + principalTable: "__EFIdentityApiKey", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK___EFIdentityApiKeyRole___EFIdentityRole_RoleId", + column: x => x.RoleId, + principalTable: "__EFIdentityRole", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.CreateIndex( name: "IX___EFAudit_CreatedBy", table: "__EFAudit", column: "CreatedBy"); + migrationBuilder.CreateIndex( + name: "IX___EFAudit_EntityKey", + table: "__EFAudit", + column: "EntityKey"); + + migrationBuilder.CreateIndex( + name: "IX___EFAudit_EntityState", + table: "__EFAudit", + column: "EntityState"); + migrationBuilder.CreateIndex( name: "IX___EFAudit_EntityTypeName", table: "__EFAudit", @@ -364,11 +422,6 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "__EFAudit", column: "RequestId"); - migrationBuilder.CreateIndex( - name: "IX___EFAudit_State", - table: "__EFAudit", - column: "State"); - migrationBuilder.CreateIndex( name: "IX___EFAuditProperties_ParentId", table: "__EFAuditProperties", @@ -389,6 +442,23 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "__EFIdentityApiKey", column: "RevokedAt"); + migrationBuilder.CreateIndex( + name: "UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType", + table: "__EFIdentityApiKeyClaim", + columns: new[] { "ApiKeyId", "ClaimType" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityApiKeyRole_RoleId", + table: "__EFIdentityApiKeyRole", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "UX___EFIdentityApiKeyRole_ApiKeyId_RoleId", + table: "__EFIdentityApiKeyRole", + columns: new[] { "ApiKeyId", "RoleId" }, + unique: true); + migrationBuilder.CreateIndex( name: "RoleNameIndex", table: "__EFIdentityRole", @@ -449,12 +519,6 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "__EFIdentityUserRefreshToken", column: "ExpireAt"); - migrationBuilder.CreateIndex( - name: "UX___EFIdentityUserRefreshToken_IdentityUserId", - table: "__EFIdentityUserRefreshToken", - column: "IdentityUserId", - unique: true); - migrationBuilder.CreateIndex( name: "UX___EFIdentityUserRefreshToken_IdentityUserId_AppId", table: "__EFIdentityUserRefreshToken", @@ -492,7 +556,10 @@ protected override void Down(MigrationBuilder migrationBuilder) name: "__EFDataProtectionKeys"); migrationBuilder.DropTable( - name: "__EFIdentityApiKey"); + name: "__EFIdentityApiKeyClaim"); + + migrationBuilder.DropTable( + name: "__EFIdentityApiKeyRole"); migrationBuilder.DropTable( name: "__EFIdentityRoleClaim"); @@ -521,6 +588,9 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "__EFAudit"); + migrationBuilder.DropTable( + name: "__EFIdentityApiKey"); + migrationBuilder.DropTable( name: "__EFIdentityRole"); diff --git a/Console.Data.MySql/Console.Data.MySql/Migrations/MySqlDbContextModelSnapshot.cs b/Console.Data.MySql/Console.Data.MySql/Migrations/MySqlDbContextModelSnapshot.cs index a4a80151..3dc5ef24 100644 --- a/Console.Data.MySql/Console.Data.MySql/Migrations/MySqlDbContextModelSnapshot.cs +++ b/Console.Data.MySql/Console.Data.MySql/Migrations/MySqlDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("ProductVersion", "10.0.7") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -213,14 +213,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetime(6)"); b.Property("CreatedBy") + .IsRequired() .HasMaxLength(256) .HasColumnType("varchar(256)"); + b.Property("EntityKey") + .HasColumnType("char(36)"); + b.Property("EntitySetName") .HasMaxLength(256) .HasColumnType("varchar(256)"); + b.Property("EntityState") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + b.Property("EntityTypeName") + .IsRequired() .HasMaxLength(256) .HasColumnType("varchar(256)"); @@ -231,23 +241,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(256) .HasColumnType("varchar(256)"); - b.Property("State") - .HasColumnType("int"); - - b.Property("StateName") - .HasMaxLength(256) - .HasColumnType("varchar(256)"); - b.HasKey("Id"); b.HasIndex("CreatedBy"); + b.HasIndex("EntityKey"); + + b.HasIndex("EntityState"); + b.HasIndex("EntityTypeName"); b.HasIndex("RequestId"); - b.HasIndex("State"); - b.ToTable("__EFAudit", (string)null); }); @@ -323,6 +328,54 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("__EFIdentityApiKey", (string)null); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApiKeyId") + .HasColumnType("char(36)"); + + b.Property("ClaimType") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId", "ClaimType") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType"); + + b.ToTable("__EFIdentityApiKeyClaim", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApiKeyId") + .HasColumnType("char(36)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("ApiKeyId", "RoleId") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyRole_ApiKeyId_RoleId"); + + b.ToTable("__EFIdentityApiKeyRole", (string)null); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.Property("Id") @@ -453,10 +506,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ExpireAt"); - b.HasIndex("IdentityUserId") - .IsUnique() - .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId"); - b.HasIndex("IdentityUserId", "AppId") .IsUnique() .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId_AppId"); @@ -537,6 +586,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("IdentityUser"); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + + b.Navigation("Role"); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") @@ -551,8 +630,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") - .WithOne() - .HasForeignKey("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", "IdentityUserId") + .WithMany() + .HasForeignKey("IdentityUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/Console.Data.MySql/Console.Data.MySql/appsettings.Development.json b/Console.Data.MySql/Console.Data.MySql/appsettings.Development.json index e586906a..34379ce9 100644 --- a/Console.Data.MySql/Console.Data.MySql/appsettings.Development.json +++ b/Console.Data.MySql/Console.Data.MySql/appsettings.Development.json @@ -1,6 +1,6 @@ { "Data": { - "UseMigrateDatabase": true, + "StartupAction": "Migrate", "ConnectionString": "Server=host.docker.internal;Database=nanoDb;Uid=sa;Pwd=myPassword_123" } } \ No newline at end of file diff --git a/Console.Data.MySql/Console.Data.MySql/appsettings.json b/Console.Data.MySql/Console.Data.MySql/appsettings.json index f52b7fe7..87c218f8 100644 --- a/Console.Data.MySql/Console.Data.MySql/appsettings.json +++ b/Console.Data.MySql/Console.Data.MySql/appsettings.json @@ -7,17 +7,14 @@ "BulkBatchSize": 500, "BulkBatchDelay": 1000, "QueryRetryCount": 0, + "StartupAction": "None", "UseLazyLoading": false, - "UseCreateDatabase": false, - "UseMigrateDatabase": false, - "UseSoftDeletetion": false, "UseSensitiveDataLogging": false, - "UseAudit": false, "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, "Repository": { - "UseAutoSave": false, + "UseAutoSave": true, "QueryIncludeDepth": 4 }, "Identity": null, diff --git a/Console.Data.MySql/README.md b/Console.Data.MySql/README.md index 80016f00..7d5948c4 100644 --- a/Console.Data.MySql/README.md +++ b/Console.Data.MySql/README.md @@ -22,13 +22,13 @@ Nano is referenced directly from source (not via NuGet packages) and is expected This application builds on **[Console.Blank](https://github.com/Nano-Core/Nano.Lessons/tree/master/Console._Blank)**. This example demonstrates how various parts of Nano data work together. All data configuration and registration have been completed, and classes have been implemented -for the data parts, including [Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-models), [Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-mappings), -and the [Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-context). +for the data parts, including **[Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-models)**, **[Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-mappings)**, +and the **[Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-context)**. -The worker creates and retreives a `Example` entity using the Nano [Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#repositories). Run the +The worker creates and retreives a `Example` entity using the Nano **[Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#repositories)**. Run the application and observe the console output generated by the `ExampleWorker`. -> 📖 Learn more about **[Nano.Data.MySql](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.MySql)**. +> 📖 Learn more about **[Nano.Data.MySql](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.MySql/README.md#nanodatamysql)**. ## Registration The following data provider has been registered using `ConfigureServices(...)` in `program.cs`. @@ -58,17 +58,14 @@ Configured the application with the necessary data setup. "BulkBatchSize": 500, "BulkBatchDelay": 1000, "QueryRetryCount": 0, + "StartupAction": "None", "UseLazyLoading": false, - "UseCreateDatabase": false, - "UseMigrateDatabase": false, - "UseSoftDeletetion": false, "UseSensitiveDataLogging": false, - "UseAudit": false, "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, "Repository": { - "UseAutoSave": false, + "UseAutoSave": true, "QueryIncludeDepth": 4 }, "Identity": null, @@ -81,7 +78,7 @@ Configured the application with the necessary data setup. ```json "Data": { - "UseMigrateDatabase": true, + "StartupAction": "Migrate", "ConnectionString": "Server=host.docker.internal;Database=nanoDb;Uid=sa;Pwd=myPassword_123" } ``` @@ -123,7 +120,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring ``` @@ -132,52 +129,46 @@ Add the following environment variables to the `buid-and-deply.yml`. ```yaml env: - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_HOST || secrets.STAGING_MYSQL_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-mysql-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_NANO_DB_PASSWORD || secrets.STAGING_MYSQL_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_USER || secrets.STAGING_MYSQL_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_MYSQL_ADMIN_PASSWORD || secrets.STAGING_MYSQL_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_USER }};Pwd=${{ env.DATA_PASSWORD }};SslMode=Preferred; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }};Port=${{ vars.DATA_MYSQL_PORT }};Database=${{ env.DATA_NAME }};Uid=${{ env.DATA_ADMIN_USER }};Pwd=${{ env.DATA_ADMIN_PASSWORD }};SslMode=Preferred; + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} ``` Additionally, this step has been added to ensure database migrations are applied, and the application database user has been created before the application is deployed. ```yaml -- name: Database Migration +- name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].fullyQualifiedDomainName; + $env:SQL_PORT = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].databasePort; + $env:SQL_ADMIN_USER = az mysql flexible-server list -g $env:AZURE_GROUP_DATABASE --query [0].administratorLogin; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_ADMIN_USER;Pwd=$env:SQL_ADMIN_PASSWORD;SslMode=Preferred"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING" `; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING "; if ($LastExitCode -ne 0) { throw "error"; }; - - sudo apt-get update - sudo apt-get install -y mysql-client + + apt-get update; + apt-get install -y mysql-client; - $userExists = mysql --connect-expired-password --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:DATA_USER');" $env:DATA_MIGRATION_CONNECTIONSTRING; + $userExists = mysql --batch -e "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$env:SQL_USER');" $env:SQL_MIGRATION_CONNECTIONSTRING; if ($userExists -eq 0) { mysql --connect-expired-password -e " ` - CREATE USER '$env:DATA_USER'@'%' IDENTIFIED BY '$env:DATA_PASSWORD'; ` - GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:DATA_USER'@'%'; ` - FLUSH PRIVILEGES;" $env:DATA_MIGRATION_CONNECTIONSTRING + CREATE USER '$env:SQL_USER'@'%' IDENTIFIED BY '$env:SQL_PASSWORD'; ` + GRANT SELECT, INSERT, UPDATE, DELETE ON $database.* TO '$env:SQL_USER'@'%'; ` + FLUSH PRIVILEGES;" $env:SQL_MIGRATION_CONNECTIONSTRING; } ``` -Last, the application connectionstring must be added in a secret in Kuberntes. The `Kubernetes Deploy` step has been updated with the following. +Last, an additional template has been added to the deployment for storing the application connectionstring in a Kuberntes secret. -```yaml -sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; -if ($LastExitCode -ne 0) -{ - throw "error"; -}; -``` diff --git a/Console.Data.PostgreSQL/.github/workflows/build-and-deploy.yml b/Console.Data.PostgreSQL/.github/workflows/build-and-deploy.yml index deb4c064..8e9abbbf 100644 --- a/Console.Data.PostgreSQL/.github/workflows/build-and-deploy.yml +++ b/Console.Data.PostgreSQL/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Data.PostgreSQL IMAGE_NAME: console.data.postgresql @@ -8,56 +13,55 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi KUBERNETES_CPU_REQUEST: 50m KUBERNETES_CPU_LIMIT: 150m KUBERNETES_CRONJOB_SCHEDULE: "0 * * * *" - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_HOST || secrets.STAGING_POSTGRE_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-postgre-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_NANO_DB_PASSWORD || secrets.STAGING_POSTGRE_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_ADMIN_USER || secrets.STAGING_POSTGRE_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_ADMIN_PASSWORD || secrets.STAGING_POSTGRE_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Host=${{ env.DATA_HOST }};Port=${{ vars.DATA_POSTGRE_PORT }};Database=${{ env.DATA_NAME }};Username=${{ env.DATA_USER }};Password=${{ env.DATA_PASSWORD }};SSL Mode=Prefer;Trust Server Certificate=true - DATA_MIGRATION_CONNECTIONSTRING: Host=${{ env.DATA_HOST }};Port=${{ vars.DATA_POSTGRE_PORT }};Database=${{ env.DATA_NAME }};Username=${{ env.DATA_ADMIN_USER }};Password=${{ env.DATA_ADMIN_PASSWORD }};SSL Mode=Prefer;Trust Server Certificate=true DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} + SQL_NAME: nanoDb + SQL_USER: api-data-mysql-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -86,7 +90,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -102,75 +106,86 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration + - name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; + $env:SQL_PORT = "5432"; + $env:SQL_ADMIN_USER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].username" -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;SSL Mode=Prefer;Trust Server Certificate=true"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING" `; if ($LastExitCode -ne 0) { throw "error"; }; - - sudo apt-get update - sudo apt-get install -y postgresql-client + + apt-get update + apt-get install -y postgresql-client - $userExists = psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=postgres" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:DATA_USER';" + $userExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" if ($userExists -ne "1") { - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=postgres" ` - -c "CREATE ROLE $env:DATA_USER WITH LOGIN PASSWORD '$env:DATA_PASSWORD';" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "CREATE ROLE $env:SQL_USER WITH LOGIN PASSWORD '$env:SQL_PASSWORD';" } - $userDbExists = psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:DATA_USER';" + $userDbExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" if ($userDbExists -ne "1") { - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT CONNECT ON DATABASE $env:DATA_NAME TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT CONNECT ON DATABASE $env:SQL_NAME TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT USAGE ON SCHEMA public TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT USAGE ON SCHEMA public TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:SQL_USER;" } + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; + - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:MYSQL_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Uid=$env:SQL_USER;Pwd=$env:SQL_PASSWORD;SslMode=Preferred"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.Data.PostgreSQL/.kubernetes/auth-sql-secret.yaml b/Console.Data.PostgreSQL/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Console.Data.PostgreSQL/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Console.Data.PostgreSQL/.kubernetes/cronjob.yaml b/Console.Data.PostgreSQL/.kubernetes/cronjob.yaml index 64e7f060..9ae388c3 100644 --- a/Console.Data.PostgreSQL/.kubernetes/cronjob.yaml +++ b/Console.Data.PostgreSQL/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -29,7 +30,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: diff --git a/Console.Data.PostgreSQL/Console.Data.PostgreSQL.sln b/Console.Data.PostgreSQL/Console.Data.PostgreSQL.sln index c286d388..78fe9f28 100644 --- a/Console.Data.PostgreSQL/Console.Data.PostgreSQL.sln +++ b/Console.Data.PostgreSQL/Console.Data.PostgreSQL.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\cronjob.yaml = .kubernetes\cronjob.yaml EndProjectSection diff --git a/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Console.Data.PostgreSQL.csproj b/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Console.Data.PostgreSQL.csproj index d449cf84..a2e4d164 100644 --- a/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Console.Data.PostgreSQL.csproj +++ b/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Console.Data.PostgreSQL.csproj @@ -42,4 +42,8 @@ + + + + \ No newline at end of file diff --git a/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/20260311120716_Initial.Designer.cs b/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/20260425181509_Initial.Designer.cs similarity index 85% rename from Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/20260311120716_Initial.Designer.cs rename to Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/20260425181509_Initial.Designer.cs index f219b9b6..cf3450e8 100644 --- a/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/20260311120716_Initial.Designer.cs +++ b/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/20260425181509_Initial.Designer.cs @@ -12,7 +12,7 @@ namespace Console.Data.PostgreSQL.Migrations { [DbContext(typeof(PostgreSqlDbContext))] - [Migration("20260311120716_Initial")] + [Migration("20260425181509_Initial")] partial class Initial { /// @@ -20,7 +20,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("ProductVersion", "10.0.7") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); @@ -215,14 +215,24 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone"); b.Property("CreatedBy") + .IsRequired() .HasMaxLength(256) .HasColumnType("character varying(256)"); + b.Property("EntityKey") + .HasColumnType("uuid"); + b.Property("EntitySetName") .HasMaxLength(256) .HasColumnType("character varying(256)"); + b.Property("EntityState") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + b.Property("EntityTypeName") + .IsRequired() .HasMaxLength(256) .HasColumnType("character varying(256)"); @@ -233,23 +243,18 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(256) .HasColumnType("character varying(256)"); - b.Property("State") - .HasColumnType("integer"); - - b.Property("StateName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - b.HasKey("Id"); b.HasIndex("CreatedBy"); + b.HasIndex("EntityKey"); + + b.HasIndex("EntityState"); + b.HasIndex("EntityTypeName"); b.HasIndex("RequestId"); - b.HasIndex("State"); - b.ToTable("__EFAudit", (string)null); }); @@ -325,6 +330,54 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("__EFIdentityApiKey", (string)null); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApiKeyId") + .HasColumnType("uuid"); + + b.Property("ClaimType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId", "ClaimType") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType"); + + b.ToTable("__EFIdentityApiKeyClaim", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApiKeyId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("ApiKeyId", "RoleId") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyRole_ApiKeyId_RoleId"); + + b.ToTable("__EFIdentityApiKeyRole", (string)null); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.Property("Id") @@ -455,10 +508,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("ExpireAt"); - b.HasIndex("IdentityUserId") - .IsUnique() - .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId"); - b.HasIndex("IdentityUserId", "AppId") .IsUnique() .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId_AppId"); @@ -539,6 +588,36 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("IdentityUser"); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + + b.Navigation("Role"); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") @@ -553,8 +632,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") - .WithOne() - .HasForeignKey("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", "IdentityUserId") + .WithMany() + .HasForeignKey("IdentityUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/20260311120716_Initial.cs b/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/20260425181509_Initial.cs similarity index 85% rename from Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/20260311120716_Initial.cs rename to Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/20260425181509_Initial.cs index 6411abcb..5d011b02 100644 --- a/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/20260311120716_Initial.cs +++ b/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/20260425181509_Initial.cs @@ -20,11 +20,11 @@ protected override void Up(MigrationBuilder migrationBuilder) columns: table => new { Id = table.Column(type: "uuid", nullable: false), - CreatedBy = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + CreatedBy = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + EntityKey = table.Column(type: "uuid", nullable: false), EntitySetName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - EntityTypeName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - State = table.Column(type: "integer", nullable: false), - StateName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EntityTypeName = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + EntityState = table.Column(type: "integer", nullable: false, defaultValue: 0), RequestId = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), IsDeleted = table.Column(type: "bigint", nullable: false), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) @@ -295,11 +295,66 @@ protected override void Up(MigrationBuilder migrationBuilder) onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "__EFIdentityApiKeyClaim", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ApiKeyId = table.Column(type: "uuid", nullable: false), + ClaimType = table.Column(type: "text", nullable: false), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityApiKeyClaim", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityApiKeyClaim___EFIdentityApiKey_ApiKeyId", + column: x => x.ApiKeyId, + principalTable: "__EFIdentityApiKey", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "__EFIdentityApiKeyRole", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ApiKeyId = table.Column(type: "uuid", nullable: false), + RoleId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityApiKeyRole", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityApiKeyRole___EFIdentityApiKey_ApiKeyId", + column: x => x.ApiKeyId, + principalTable: "__EFIdentityApiKey", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK___EFIdentityApiKeyRole___EFIdentityRole_RoleId", + column: x => x.RoleId, + principalTable: "__EFIdentityRole", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateIndex( name: "IX___EFAudit_CreatedBy", table: "__EFAudit", column: "CreatedBy"); + migrationBuilder.CreateIndex( + name: "IX___EFAudit_EntityKey", + table: "__EFAudit", + column: "EntityKey"); + + migrationBuilder.CreateIndex( + name: "IX___EFAudit_EntityState", + table: "__EFAudit", + column: "EntityState"); + migrationBuilder.CreateIndex( name: "IX___EFAudit_EntityTypeName", table: "__EFAudit", @@ -310,11 +365,6 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "__EFAudit", column: "RequestId"); - migrationBuilder.CreateIndex( - name: "IX___EFAudit_State", - table: "__EFAudit", - column: "State"); - migrationBuilder.CreateIndex( name: "IX___EFAuditProperties_ParentId", table: "__EFAuditProperties", @@ -335,6 +385,23 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "__EFIdentityApiKey", column: "RevokedAt"); + migrationBuilder.CreateIndex( + name: "UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType", + table: "__EFIdentityApiKeyClaim", + columns: new[] { "ApiKeyId", "ClaimType" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityApiKeyRole_RoleId", + table: "__EFIdentityApiKeyRole", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "UX___EFIdentityApiKeyRole_ApiKeyId_RoleId", + table: "__EFIdentityApiKeyRole", + columns: new[] { "ApiKeyId", "RoleId" }, + unique: true); + migrationBuilder.CreateIndex( name: "RoleNameIndex", table: "__EFIdentityRole", @@ -395,12 +462,6 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "__EFIdentityUserRefreshToken", column: "ExpireAt"); - migrationBuilder.CreateIndex( - name: "UX___EFIdentityUserRefreshToken_IdentityUserId", - table: "__EFIdentityUserRefreshToken", - column: "IdentityUserId", - unique: true); - migrationBuilder.CreateIndex( name: "UX___EFIdentityUserRefreshToken_IdentityUserId_AppId", table: "__EFIdentityUserRefreshToken", @@ -438,7 +499,10 @@ protected override void Down(MigrationBuilder migrationBuilder) name: "__EFDataProtectionKeys"); migrationBuilder.DropTable( - name: "__EFIdentityApiKey"); + name: "__EFIdentityApiKeyClaim"); + + migrationBuilder.DropTable( + name: "__EFIdentityApiKeyRole"); migrationBuilder.DropTable( name: "__EFIdentityRoleClaim"); @@ -467,6 +531,9 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "__EFAudit"); + migrationBuilder.DropTable( + name: "__EFIdentityApiKey"); + migrationBuilder.DropTable( name: "__EFIdentityRole"); diff --git a/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/PostgreSqlDbContextModelSnapshot.cs b/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/PostgreSqlDbContextModelSnapshot.cs index 9e4e9603..623515cd 100644 --- a/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/Console.Data.PostgreSQL/Console.Data.PostgreSQL/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("ProductVersion", "10.0.7") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); @@ -212,14 +212,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone"); b.Property("CreatedBy") + .IsRequired() .HasMaxLength(256) .HasColumnType("character varying(256)"); + b.Property("EntityKey") + .HasColumnType("uuid"); + b.Property("EntitySetName") .HasMaxLength(256) .HasColumnType("character varying(256)"); + b.Property("EntityState") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + b.Property("EntityTypeName") + .IsRequired() .HasMaxLength(256) .HasColumnType("character varying(256)"); @@ -230,23 +240,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(256) .HasColumnType("character varying(256)"); - b.Property("State") - .HasColumnType("integer"); - - b.Property("StateName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - b.HasKey("Id"); b.HasIndex("CreatedBy"); + b.HasIndex("EntityKey"); + + b.HasIndex("EntityState"); + b.HasIndex("EntityTypeName"); b.HasIndex("RequestId"); - b.HasIndex("State"); - b.ToTable("__EFAudit", (string)null); }); @@ -322,6 +327,54 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("__EFIdentityApiKey", (string)null); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApiKeyId") + .HasColumnType("uuid"); + + b.Property("ClaimType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId", "ClaimType") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType"); + + b.ToTable("__EFIdentityApiKeyClaim", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApiKeyId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("ApiKeyId", "RoleId") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyRole_ApiKeyId_RoleId"); + + b.ToTable("__EFIdentityApiKeyRole", (string)null); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.Property("Id") @@ -452,10 +505,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ExpireAt"); - b.HasIndex("IdentityUserId") - .IsUnique() - .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId"); - b.HasIndex("IdentityUserId", "AppId") .IsUnique() .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId_AppId"); @@ -536,6 +585,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("IdentityUser"); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + + b.Navigation("Role"); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") @@ -550,8 +629,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") - .WithOne() - .HasForeignKey("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", "IdentityUserId") + .WithMany() + .HasForeignKey("IdentityUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/Console.Data.PostgreSQL/Console.Data.PostgreSQL/appsettings.Development.json b/Console.Data.PostgreSQL/Console.Data.PostgreSQL/appsettings.Development.json index abb3ca92..ddba6f26 100644 --- a/Console.Data.PostgreSQL/Console.Data.PostgreSQL/appsettings.Development.json +++ b/Console.Data.PostgreSQL/Console.Data.PostgreSQL/appsettings.Development.json @@ -1,6 +1,6 @@ { "Data": { - "UseMigrateDatabase": true, + "StartupAction": "Migrate", "ConnectionString": "Host=host.docker.internal;Port=5432;Database=nanoDb;Username=sa;Password=myPassword_123" } } \ No newline at end of file diff --git a/Console.Data.PostgreSQL/Console.Data.PostgreSQL/appsettings.json b/Console.Data.PostgreSQL/Console.Data.PostgreSQL/appsettings.json index f52b7fe7..87c218f8 100644 --- a/Console.Data.PostgreSQL/Console.Data.PostgreSQL/appsettings.json +++ b/Console.Data.PostgreSQL/Console.Data.PostgreSQL/appsettings.json @@ -7,17 +7,14 @@ "BulkBatchSize": 500, "BulkBatchDelay": 1000, "QueryRetryCount": 0, + "StartupAction": "None", "UseLazyLoading": false, - "UseCreateDatabase": false, - "UseMigrateDatabase": false, - "UseSoftDeletetion": false, "UseSensitiveDataLogging": false, - "UseAudit": false, "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, "Repository": { - "UseAutoSave": false, + "UseAutoSave": true, "QueryIncludeDepth": 4 }, "Identity": null, diff --git a/Console.Data.PostgreSQL/README.md b/Console.Data.PostgreSQL/README.md index 39dee06a..d27443e3 100644 --- a/Console.Data.PostgreSQL/README.md +++ b/Console.Data.PostgreSQL/README.md @@ -22,13 +22,13 @@ Nano is referenced directly from source (not via NuGet packages) and is expected This application builds on **[Console.Blank](https://github.com/Nano-Core/Nano.Lessons/tree/master/Console._Blank)**. This example demonstrates how various parts of Nano data work together. All data configuration and registration have been completed, and classes have been implemented -for the data parts, including [Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-models), [Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-mappings), -and the [Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-context). +for the data parts, including **[Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-models)**, **[Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-mappings)**, +and the **[Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-context)**. -The worker creates and retreives a `Example` entity using the Nano [Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#repositories). Run the +The worker creates and retreives a `Example` entity using the Nano **[Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#repositories)**. Run the application and observe the console output generated by the `ExampleWorker`. -> 📖 Learn more about **[Nano.Data.PostgreSQL](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.PostgreSQL)**. +> 📖 Learn more about **[Nano.Data.PostgreSQL](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.PostgreSQL/README.md#nanodatamysql)**. ## Registration The following data provider has been registered using `ConfigureServices(...)` in `program.cs`. @@ -58,17 +58,14 @@ Configured the application with the necessary data setup. "BulkBatchSize": 500, "BulkBatchDelay": 1000, "QueryRetryCount": 0, + "StartupAction": "None", "UseLazyLoading": false, - "UseCreateDatabase": false, - "UseMigrateDatabase": false, - "UseSoftDeletetion": false, "UseSensitiveDataLogging": false, - "UseAudit": false, "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, "Repository": { - "UseAutoSave": false, + "UseAutoSave": true, "QueryIncludeDepth": 4 }, "Identity": null, @@ -81,7 +78,7 @@ Configured the application with the necessary data setup. ```json "Data": { - "UseMigrateDatabase": true, + "StartupAction": "Migrate", "ConnectionString": "Host=host.docker.internal;Port=5432;Database=nanoDb;Username=sa;Password=myPassword_123" } ``` @@ -119,7 +116,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring ``` @@ -128,69 +125,62 @@ Add the following environment variables to the `buid-and-deply.yml`. ```yaml env: - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_HOST || secrets.STAGING_POSTGRE_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-postgre-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_NANO_DB_PASSWORD || secrets.STAGING_POSTGRE_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_ADMIN_USER || secrets.STAGING_POSTGRE_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_POSTGRE_ADMIN_PASSWORD || secrets.STAGING_POSTGRE_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Host=${{ env.DATA_HOST }};Port=${{ vars.DATA_POSTGRE_PORT }};Database=${{ env.DATA_NAME }};Username=${{ env.DATA_USER }};Password=${{ env.DATA_PASSWORD }};SSL Mode=Prefer;Trust Server Certificate=true - DATA_MIGRATION_CONNECTIONSTRING: Host=${{ env.DATA_HOST }};Port=${{ vars.DATA_POSTGRE_PORT }};Database=${{ env.DATA_NAME }};Username=${{ env.DATA_ADMIN_USER }};Password=${{ env.DATA_ADMIN_PASSWORD }};SSL Mode=Prefer;Trust Server Certificate=true + SQL_NAME: nanoDb + SQL_USER: api-data-postgres-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} ``` Additionally, this step has been added to ensure database migrations are applied, and the application database user has been created before the application is deployed. ```yaml -- name: Database Migration +- name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; + $env:SQL_PORT = "5432"; + $env:SQL_ADMIN_USER = az postgres flexible-server list -g $env:AZURE_GROUP_DATABASE --query "[0].username" -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Host=$env:SQL_HOST;Port=$env:SQL_PORT;Database=$env:SQL_NAME;Username=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;SSL Mode=Prefer;Trust Server Certificate=true"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING" `; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING" `; if ($LastExitCode -ne 0) { throw "error"; }; - sudo apt-get update - sudo apt-get install -y postgresql-client + apt-get update + apt-get install -y postgresql-client - $userExists = psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=postgres" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:DATA_USER';" + $userExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" if ($userExists -ne "1") { - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=postgres" ` - -c "CREATE ROLE $env:DATA_USER WITH LOGIN PASSWORD '$env:DATA_PASSWORD';" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "CREATE ROLE $env:SQL_USER WITH LOGIN PASSWORD '$env:SQL_PASSWORD';" } - $userDbExists = psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:DATA_USER';" + $userDbExists = psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -tAc "SELECT 1 FROM pg_roles WHERE rolname='$env:SQL_USER';" if ($userDbExists -ne "1") { - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT CONNECT ON DATABASE $env:DATA_NAME TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT CONNECT ON DATABASE $env:SQL_NAME TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT USAGE ON SCHEMA public TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT USAGE ON SCHEMA public TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $env:SQL_USER;" - psql "host=$env:DATA_HOST port=$env:DATA_POSTGRE_PORT user=$env:DATA_ADMIN_USER password=$env:DATA_ADMIN_PASSWORD dbname=$env:DATA_NAME" ` - -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:DATA_USER;" + psql "$env:SQL_MIGRATION_CONNECTIONSTRING" ` + -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $env:SQL_USER;" } ``` -Last, the application connectionstring must be added in a secret in Kuberntes. The `Kubernetes Deploy` step has been updated with the following. - -```yaml -sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; -if ($LastExitCode -ne 0) -{ - throw "error"; -}; -``` +Last, an additional template has been added to the deployment for storing the application connectionstring in a Kuberntes secret. diff --git a/Console.Data.SqLite/.docker/docker-compose.yml b/Console.Data.SqLite/.docker/docker-compose.yml index db8bdb53..2d2a2508 100644 --- a/Console.Data.SqLite/.docker/docker-compose.yml +++ b/Console.Data.SqLite/.docker/docker-compose.yml @@ -6,7 +6,7 @@ services: context: ../Console.Data.SqLite dockerfile: "Dockerfile.Local" volumes: - - ./bin/data:/data + - ./bin/data:/mnt/data networks: - network diff --git a/Console.Data.SqLite/.github/workflows/build-and-deploy.yml b/Console.Data.SqLite/.github/workflows/build-and-deploy.yml index 95a69c0d..86a7f66c 100644 --- a/Console.Data.SqLite/.github/workflows/build-and-deploy.yml +++ b/Console.Data.SqLite/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Data.SqLite IMAGE_NAME: console.data.sqlite @@ -8,51 +13,54 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi KUBERNETES_CPU_REQUEST: 50m KUBERNETES_CPU_LIMIT: 150m KUBERNETES_CRONJOB_SCHEDULE: "0 * * * *" - DATA_NAME: nanoDb - DATA_SIZE: 10Gi - DATA_CONNECTIONSTRING: "Data Source=/data/{{ env.nanoDb }}.sqlite" + SQL_NAME: nanoDb + SQL_SIZE: 10Gi + SQL_CONNECTIONSTRING: "Data Source=/mnt/data/{{ env.SQL_NAME }}.sqlite" DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -111,7 +119,7 @@ jobs: dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING"; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING"; if ($LastExitCode -ne 0) { @@ -122,28 +130,28 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/data-storageclass.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/data-storageclass.tmp.yaml; - sudo kubectl apply -f .kubernetes/data-storageclass.tmp.yaml; + kubectl apply -f .kubernetes/data-storageclass.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/data-pvc.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/data-pvc.tmp.yaml; - sudo kubectl apply -f .kubernetes/data-pvc.tmp.yaml; + kubectl apply -f .kubernetes/data-pvc.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.Data.SqLite/.kubernetes/cronjob.yaml b/Console.Data.SqLite/.kubernetes/cronjob.yaml index 243d9b9c..aa4f8991 100644 --- a/Console.Data.SqLite/.kubernetes/cronjob.yaml +++ b/Console.Data.SqLite/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -47,7 +48,7 @@ spec: - ALL volumeMounts: - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% + mountPath: /mnt/data volumes: - name: %SERVICE_NAME%-volume persistentVolumeClaim: diff --git a/Console.Data.SqLite/Console.Data.SqLite/Dockerfile.Local b/Console.Data.SqLite/Console.Data.SqLite/Dockerfile.Local index c3dd41cd..98887f04 100644 --- a/Console.Data.SqLite/Console.Data.SqLite/Dockerfile.Local +++ b/Console.Data.SqLite/Console.Data.SqLite/Dockerfile.Local @@ -2,8 +2,4 @@ ARG DOTNET_ASPNET_VERSION="10.0" FROM mcr.microsoft.com/dotnet/aspnet:$DOTNET_ASPNET_VERSION AS base -RUN apt-get update \ - && apt-get install -y libsqlite3-mod-spatialite \ - && apt-get install -y libspatialite-dev - ENTRYPOINT ["dotnet", "Console.Data.SqLite.dll"] \ No newline at end of file diff --git a/Console.Data.SqLite/Console.Data.SqLite/Migrations/20260311131050_Initial.Designer.cs b/Console.Data.SqLite/Console.Data.SqLite/Migrations/20260427110613_Initial.Designer.cs similarity index 84% rename from Console.Data.SqLite/Console.Data.SqLite/Migrations/20260311131050_Initial.Designer.cs rename to Console.Data.SqLite/Console.Data.SqLite/Migrations/20260427110613_Initial.Designer.cs index e9c26d2f..f33a1ac1 100644 --- a/Console.Data.SqLite/Console.Data.SqLite/Migrations/20260311131050_Initial.Designer.cs +++ b/Console.Data.SqLite/Console.Data.SqLite/Migrations/20260427110613_Initial.Designer.cs @@ -11,14 +11,14 @@ namespace Console.Data.SqLite.Migrations { [DbContext(typeof(SqLiteDbContext))] - [Migration("20260311131050_Initial")] + [Migration("20260427110613_Initial")] partial class Initial { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); modelBuilder.Entity("Console.Data.SqLite.Data.Models.Example", b => { @@ -203,14 +203,24 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("CreatedBy") + .IsRequired() .HasMaxLength(256) .HasColumnType("TEXT"); + b.Property("EntityKey") + .HasColumnType("TEXT"); + b.Property("EntitySetName") .HasMaxLength(256) .HasColumnType("TEXT"); + b.Property("EntityState") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + b.Property("EntityTypeName") + .IsRequired() .HasMaxLength(256) .HasColumnType("TEXT"); @@ -221,23 +231,18 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(256) .HasColumnType("TEXT"); - b.Property("State") - .HasColumnType("INTEGER"); - - b.Property("StateName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - b.HasKey("Id"); b.HasIndex("CreatedBy"); + b.HasIndex("EntityKey"); + + b.HasIndex("EntityState"); + b.HasIndex("EntityTypeName"); b.HasIndex("RequestId"); - b.HasIndex("State"); - b.ToTable("__EFAudit", (string)null); }); @@ -313,6 +318,54 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("__EFIdentityApiKey", (string)null); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApiKeyId") + .HasColumnType("TEXT"); + + b.Property("ClaimType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId", "ClaimType") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType"); + + b.ToTable("__EFIdentityApiKeyClaim", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApiKeyId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("ApiKeyId", "RoleId") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyRole_ApiKeyId_RoleId"); + + b.ToTable("__EFIdentityApiKeyRole", (string)null); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.Property("Id") @@ -443,10 +496,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("ExpireAt"); - b.HasIndex("IdentityUserId") - .IsUnique() - .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId"); - b.HasIndex("IdentityUserId", "AppId") .IsUnique() .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId_AppId"); @@ -527,6 +576,36 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("IdentityUser"); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + + b.Navigation("Role"); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") @@ -541,8 +620,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") - .WithOne() - .HasForeignKey("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", "IdentityUserId") + .WithMany() + .HasForeignKey("IdentityUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/Console.Data.SqLite/Console.Data.SqLite/Migrations/20260311131050_Initial.cs b/Console.Data.SqLite/Console.Data.SqLite/Migrations/20260427110613_Initial.cs similarity index 85% rename from Console.Data.SqLite/Console.Data.SqLite/Migrations/20260311131050_Initial.cs rename to Console.Data.SqLite/Console.Data.SqLite/Migrations/20260427110613_Initial.cs index 3cc2b508..49419129 100644 --- a/Console.Data.SqLite/Console.Data.SqLite/Migrations/20260311131050_Initial.cs +++ b/Console.Data.SqLite/Console.Data.SqLite/Migrations/20260427110613_Initial.cs @@ -16,11 +16,11 @@ protected override void Up(MigrationBuilder migrationBuilder) columns: table => new { Id = table.Column(type: "TEXT", nullable: false), - CreatedBy = table.Column(type: "TEXT", maxLength: 256, nullable: true), + CreatedBy = table.Column(type: "TEXT", maxLength: 256, nullable: false), + EntityKey = table.Column(type: "TEXT", nullable: false), EntitySetName = table.Column(type: "TEXT", maxLength: 256, nullable: true), - EntityTypeName = table.Column(type: "TEXT", maxLength: 256, nullable: true), - State = table.Column(type: "INTEGER", nullable: false), - StateName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + EntityTypeName = table.Column(type: "TEXT", maxLength: 256, nullable: false), + EntityState = table.Column(type: "INTEGER", nullable: false, defaultValue: 0), RequestId = table.Column(type: "TEXT", maxLength: 256, nullable: true), IsDeleted = table.Column(type: "INTEGER", nullable: false), CreatedAt = table.Column(type: "TEXT", nullable: false) @@ -291,11 +291,66 @@ protected override void Up(MigrationBuilder migrationBuilder) onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "__EFIdentityApiKeyClaim", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + ApiKeyId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: false), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityApiKeyClaim", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityApiKeyClaim___EFIdentityApiKey_ApiKeyId", + column: x => x.ApiKeyId, + principalTable: "__EFIdentityApiKey", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "__EFIdentityApiKeyRole", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + ApiKeyId = table.Column(type: "TEXT", nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityApiKeyRole", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityApiKeyRole___EFIdentityApiKey_ApiKeyId", + column: x => x.ApiKeyId, + principalTable: "__EFIdentityApiKey", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK___EFIdentityApiKeyRole___EFIdentityRole_RoleId", + column: x => x.RoleId, + principalTable: "__EFIdentityRole", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateIndex( name: "IX___EFAudit_CreatedBy", table: "__EFAudit", column: "CreatedBy"); + migrationBuilder.CreateIndex( + name: "IX___EFAudit_EntityKey", + table: "__EFAudit", + column: "EntityKey"); + + migrationBuilder.CreateIndex( + name: "IX___EFAudit_EntityState", + table: "__EFAudit", + column: "EntityState"); + migrationBuilder.CreateIndex( name: "IX___EFAudit_EntityTypeName", table: "__EFAudit", @@ -306,11 +361,6 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "__EFAudit", column: "RequestId"); - migrationBuilder.CreateIndex( - name: "IX___EFAudit_State", - table: "__EFAudit", - column: "State"); - migrationBuilder.CreateIndex( name: "IX___EFAuditProperties_ParentId", table: "__EFAuditProperties", @@ -331,6 +381,23 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "__EFIdentityApiKey", column: "RevokedAt"); + migrationBuilder.CreateIndex( + name: "UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType", + table: "__EFIdentityApiKeyClaim", + columns: new[] { "ApiKeyId", "ClaimType" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityApiKeyRole_RoleId", + table: "__EFIdentityApiKeyRole", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "UX___EFIdentityApiKeyRole_ApiKeyId_RoleId", + table: "__EFIdentityApiKeyRole", + columns: new[] { "ApiKeyId", "RoleId" }, + unique: true); + migrationBuilder.CreateIndex( name: "RoleNameIndex", table: "__EFIdentityRole", @@ -391,12 +458,6 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "__EFIdentityUserRefreshToken", column: "ExpireAt"); - migrationBuilder.CreateIndex( - name: "UX___EFIdentityUserRefreshToken_IdentityUserId", - table: "__EFIdentityUserRefreshToken", - column: "IdentityUserId", - unique: true); - migrationBuilder.CreateIndex( name: "UX___EFIdentityUserRefreshToken_IdentityUserId_AppId", table: "__EFIdentityUserRefreshToken", @@ -434,7 +495,10 @@ protected override void Down(MigrationBuilder migrationBuilder) name: "__EFDataProtectionKeys"); migrationBuilder.DropTable( - name: "__EFIdentityApiKey"); + name: "__EFIdentityApiKeyClaim"); + + migrationBuilder.DropTable( + name: "__EFIdentityApiKeyRole"); migrationBuilder.DropTable( name: "__EFIdentityRoleClaim"); @@ -463,6 +527,9 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "__EFAudit"); + migrationBuilder.DropTable( + name: "__EFIdentityApiKey"); + migrationBuilder.DropTable( name: "__EFIdentityRole"); diff --git a/Console.Data.SqLite/Console.Data.SqLite/Migrations/SqLiteDbContextModelSnapshot.cs b/Console.Data.SqLite/Console.Data.SqLite/Migrations/SqLiteDbContextModelSnapshot.cs index 3726e496..ed2c03c5 100644 --- a/Console.Data.SqLite/Console.Data.SqLite/Migrations/SqLiteDbContextModelSnapshot.cs +++ b/Console.Data.SqLite/Console.Data.SqLite/Migrations/SqLiteDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class SqLiteDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); modelBuilder.Entity("Console.Data.SqLite.Data.Models.Example", b => { @@ -200,14 +200,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("CreatedBy") + .IsRequired() .HasMaxLength(256) .HasColumnType("TEXT"); + b.Property("EntityKey") + .HasColumnType("TEXT"); + b.Property("EntitySetName") .HasMaxLength(256) .HasColumnType("TEXT"); + b.Property("EntityState") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + b.Property("EntityTypeName") + .IsRequired() .HasMaxLength(256) .HasColumnType("TEXT"); @@ -218,23 +228,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(256) .HasColumnType("TEXT"); - b.Property("State") - .HasColumnType("INTEGER"); - - b.Property("StateName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - b.HasKey("Id"); b.HasIndex("CreatedBy"); + b.HasIndex("EntityKey"); + + b.HasIndex("EntityState"); + b.HasIndex("EntityTypeName"); b.HasIndex("RequestId"); - b.HasIndex("State"); - b.ToTable("__EFAudit", (string)null); }); @@ -310,6 +315,54 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("__EFIdentityApiKey", (string)null); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApiKeyId") + .HasColumnType("TEXT"); + + b.Property("ClaimType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId", "ClaimType") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType"); + + b.ToTable("__EFIdentityApiKeyClaim", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApiKeyId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("ApiKeyId", "RoleId") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyRole_ApiKeyId_RoleId"); + + b.ToTable("__EFIdentityApiKeyRole", (string)null); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.Property("Id") @@ -440,10 +493,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ExpireAt"); - b.HasIndex("IdentityUserId") - .IsUnique() - .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId"); - b.HasIndex("IdentityUserId", "AppId") .IsUnique() .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId_AppId"); @@ -524,6 +573,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("IdentityUser"); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + + b.Navigation("Role"); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") @@ -538,8 +617,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") - .WithOne() - .HasForeignKey("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", "IdentityUserId") + .WithMany() + .HasForeignKey("IdentityUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/Console.Data.SqLite/Console.Data.SqLite/Program.cs b/Console.Data.SqLite/Console.Data.SqLite/Program.cs index cac43631..7cdf911d 100644 --- a/Console.Data.SqLite/Console.Data.SqLite/Program.cs +++ b/Console.Data.SqLite/Console.Data.SqLite/Program.cs @@ -2,6 +2,8 @@ using Nano.App.Console; using Nano.Data.Extensions; using Nano.Data.SqLite; +using System; +using System.IO; NanoConsoleApplication .ConfigureApp(args) diff --git a/Console.Data.SqLite/Console.Data.SqLite/appsettings.Development.json b/Console.Data.SqLite/Console.Data.SqLite/appsettings.Development.json index 434b55cf..8927ff99 100644 --- a/Console.Data.SqLite/Console.Data.SqLite/appsettings.Development.json +++ b/Console.Data.SqLite/Console.Data.SqLite/appsettings.Development.json @@ -1,5 +1,5 @@ { "Data": { - "UseMigrateDatabase": true + "StartupAction": "Migrate" } } \ No newline at end of file diff --git a/Console.Data.SqLite/Console.Data.SqLite/appsettings.json b/Console.Data.SqLite/Console.Data.SqLite/appsettings.json index 2f4bba6c..8e22deeb 100644 --- a/Console.Data.SqLite/Console.Data.SqLite/appsettings.json +++ b/Console.Data.SqLite/Console.Data.SqLite/appsettings.json @@ -7,21 +7,18 @@ "BulkBatchSize": 500, "BulkBatchDelay": 1000, "QueryRetryCount": 0, + "StartupAction": "Migrate", "UseLazyLoading": false, - "UseCreateDatabase": false, - "UseMigrateDatabase": false, - "UseSoftDeletetion": false, "UseSensitiveDataLogging": false, - "UseAudit": false, "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, - "ConnectionString": "Data Source=/data/nanoDb.sqlite", + "ConnectionString": "Data Source=/mnt/data/nanoDb.sqlite", "Repository": { - "UseAutoSave": false, + "UseAutoSave": true, "QueryIncludeDepth": 4 }, - "ConnectionPool": null, "Identity": null, + "ConnectionPool": null, "HealthCheck": null } } \ No newline at end of file diff --git a/Console.Data.SqLite/Dockerfile b/Console.Data.SqLite/Dockerfile index f6e105a6..bc4e676b 100644 --- a/Console.Data.SqLite/Dockerfile +++ b/Console.Data.SqLite/Dockerfile @@ -9,10 +9,6 @@ FROM mcr.microsoft.com/dotnet/aspnet:$DOTNET_ASPNET_VERSION AS base LABEL org.opencontainers.image.source=CONTAINER_REGISTRY_SOURCE_LABEL -RUN apt-get update \ - && apt-get install -y libsqlite3-mod-spatialite \ - && apt-get install -y libspatialite-dev - WORKDIR /app FROM mcr.microsoft.com/dotnet/sdk:$DOTNET_SDK_VERSION AS build diff --git a/Console.Data.SqLite/README.md b/Console.Data.SqLite/README.md index 029ddf3e..7661aa11 100644 --- a/Console.Data.SqLite/README.md +++ b/Console.Data.SqLite/README.md @@ -22,13 +22,13 @@ Nano is referenced directly from source (not via NuGet packages) and is expected This application builds on **[Console.Blank](https://github.com/Nano-Core/Nano.Lessons/tree/master/Console._Blank)**. This example demonstrates how various parts of Nano data work together. All data configuration and registration have been completed, and classes have been implemented -for the data parts, including [Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-models), [Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-mappings), -and the [Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-context). +for the data parts, including **[Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-models)**, **[Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-mappings)**, +and the **[Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-context)**. -The worker creates and retreives a `Example` entity using the Nano [Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#repositories). Run the +The worker creates and retreives a `Example` entity using the Nano **[Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#repositories)**. Run the application and observe the console output generated by the `ExampleWorker`. -> 📖 Learn more about **[Nano.Data.SqLite](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.SqLite)**. +> 📖 Learn more about **[Nano.Data.SqLite](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.SqLite/README.md#nanodatamysql)**. ## Registration The following data provider has been registered using `ConfigureServices(...)` in `program.cs`. @@ -58,12 +58,9 @@ Configured the application with the necessary data setup. "BulkBatchSize": 500, "BulkBatchDelay": 1000, "QueryRetryCount": 0, + "StartupAction": "None", "UseLazyLoading": false, - "UseCreateDatabase": false, - "UseMigrateDatabase": false, - "UseSoftDeletetion": false, "UseSensitiveDataLogging": false, - "UseAudit": false, "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": "Data Source=/data/nanoDb.sqlite", @@ -81,7 +78,7 @@ Configured the application with the necessary data setup. ```json "Data": { - "UseMigrateDatabase": true + "StartupAction": "Migrate", } ``` @@ -92,15 +89,7 @@ Added SqLite as a service dependency in `docker-compose.yml`. services: console.data.sqlite: volumes: - - ./bin/data:/data -``` - -Also the `Dockerfile` must have SqLite installed with spatial support. Add the following to both the `Dockerfile` and the `Dockerfile.Local`. - -```dockerfile -RUN apt-get update \ - && apt-get install -y libsqlite3-mod-spatialite \ - && apt-get install -y libspatialite-dev + - ./bin/data:/mnt/data ``` ## Kubernetes @@ -117,7 +106,7 @@ spec: containers: volumeMounts: - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% + mountPath: /mnt/data volumes: - name: %SERVICE_NAME%-volume persistentVolumeClaim: @@ -129,9 +118,9 @@ Add the following environment variables to the `buid-and-deply.yml`. ```yaml env: - DATA_NAME: nanoDb - DATA_SIZE: 10Gi - DATA_CONNECTIONSTRING: "Data Source=/data/{{ env.nanoDb }}.sqlite" + SQL_NAME: nanoDb + SQL_SIZE: 10Gi + SQL_CONNECTIONSTRING: "Data Source=/mnt/data/{{ env.nanoDb }}.sqlite" ``` Additionally, this step has been added to ensure database migrations are applied. @@ -143,7 +132,7 @@ Additionally, this step has been added to ensure database migrations are applied dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING" `; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING" `; if ($LastExitCode -ne 0) { diff --git a/Console.Data.SqlServer/.github/workflows/build-and-deploy.yml b/Console.Data.SqlServer/.github/workflows/build-and-deploy.yml index dd464657..054564d4 100644 --- a/Console.Data.SqlServer/.github/workflows/build-and-deploy.yml +++ b/Console.Data.SqlServer/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Data.SqlServer IMAGE_NAME: console.data.sqlserver @@ -8,56 +13,55 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi KUBERNETES_CPU_REQUEST: 50m KUBERNETES_CPU_LIMIT: 150m KUBERNETES_CRONJOB_SCHEDULE: "0 * * * *" - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_HOST || secrets.STAGING_SQLSERVER_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-sqlserver-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_NANO_DB_PASSWORD || secrets.STAGING_SQLSERVER_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_ADMIN_USER || secrets.STAGING_SQLSERVER_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_ADMIN_PASSWORD || secrets.STAGING_SQLSERVER_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }},${{ vars.DATA_SQLSERVER_PORT }};Database=${{ env.DATA_NAME }};User Id=${{ env.DATA_USER }};Password=${{ env.DATA_PASSWORD }}; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }},${{ vars.DATA_SQLSERVER_PORT }};Database=${{ env.DATA_NAME }};User Id=${{ env.DATA_ADMIN_USER }};Password=${{ env.DATA_ADMIN_PASSWORD }}; DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} + SQL_NAME: nanoDb + SQL_USER: api-data-sqlserver-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -86,7 +90,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -102,87 +106,98 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; }; - - name: Database Migration - shell: pwsh - run: | - dotnet ef database update ` - --no-build ` - --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING" `; - - if ($LastExitCode -ne 0) - { - throw "error"; - }; - - sudo apt-get update - sudo apt-get install -y mssql-tools unixodbc-dev - - $loginExists = sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d master ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:DATA_USER';" - - if ($loginExists -eq 0) - { - sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d master ` - -Q "CREATE LOGIN [$env:DATA_USER] WITH PASSWORD = '$env:DATA_PASSWORD';" - }; - - $userExists = sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d $env:DATA_NAME ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:DATA_USER';" - - if ($userExists -eq 0) - { - sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d $env:DATA_NAME ` - -Q "CREATE USER [$env:DATA_USER] FOR LOGIN [$env:DATA_USER]; - ALTER ROLE db_datareader ADD MEMBER [$env:DATA_USER]; - ALTER ROLE db_datawriter ADD MEMBER [$env:DATA_USER];" - }; + - name: Database Migration & User + shell: pwsh + run: | + $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; + $env:SQL_PORT = "1433" + $env:SQL_ADMIN_USER = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].administratorLogin" -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + + dotnet ef database update ` + --no-build ` + --startup-project $env:APP_NAME ` + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING"; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + apt-get update + apt-get install -y mssql-tools unixodbc-dev + + $loginExists = sqlcmd ` + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d master ` + -h -1 ` + -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:SQL_USER';" + + if ($loginExists -eq 0) + { + sqlcmd ` + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d master ` + -Q "CREATE LOGIN [$env:SQL_USER] WITH PASSWORD = '$env:SQL_PASSWORD';" + }; + + $userExists = sqlcmd ` + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d $env:SQL_NAME ` + -h -1 ` + -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:SQL_USER';" + + if ($userExists -eq 0) + { + sqlcmd ` + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d $env:SQL_NAME ` + -Q "CREATE USER [$env:SQL_USER] FOR LOGIN [$env:SQL_USER]; + ALTER ROLE db_datareader ADD MEMBER [$env:SQL_USER]; + ALTER ROLE db_datawriter ADD MEMBER [$env:SQL_USER];" + }; + + echo "SQL_HOST=$env:SQL_HOST" >> $env:GITHUB_ENV; + echo "SQL_PORT=$env:SQL_PORT" >> $env:GITHUB_ENV; - name: Kubernetes Deploy shell: pwsh run: | - sudo kubectl create secret generic $env:SERVICE_NAME-data-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; + $env:SQL_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_USER;Password=$env:SQL_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + + Get-Content .kubernetes/auth-sql-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-sql-secret.tmp.yaml; + kubectl apply -f .kubernetes/auth-sql-secret.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.Data.SqlServer/.kubernetes/auth-sql-secret.yaml b/Console.Data.SqlServer/.kubernetes/auth-sql-secret.yaml new file mode 100644 index 00000000..fffa83c4 --- /dev/null +++ b/Console.Data.SqlServer/.kubernetes/auth-sql-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: $env:SERVICE_NAME-sql-auth-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + data-connectionstring: %SQL_CONNECTIONSTRING% + diff --git a/Console.Data.SqlServer/.kubernetes/cronjob.yaml b/Console.Data.SqlServer/.kubernetes/cronjob.yaml index 64e7f060..9ae388c3 100644 --- a/Console.Data.SqlServer/.kubernetes/cronjob.yaml +++ b/Console.Data.SqlServer/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -29,7 +30,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-data-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring envFrom: - configMapRef: diff --git a/Console.Data.SqlServer/Console.Data.SqlServer.sln b/Console.Data.SqlServer/Console.Data.SqlServer.sln index b9fbd851..32adc5d0 100644 --- a/Console.Data.SqlServer/Console.Data.SqlServer.sln +++ b/Console.Data.SqlServer/Console.Data.SqlServer.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".tests", ".tests", "{7E0D2A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes", "{0C4D782F-72EF-42B4-BC1B-AF4044CC863A}" ProjectSection(SolutionItems) = preProject + .kubernetes\auth-sql-secret.yaml = .kubernetes\auth-sql-secret.yaml .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\cronjob.yaml = .kubernetes\cronjob.yaml EndProjectSection diff --git a/Console.Data.SqlServer/Console.Data.SqlServer/Migrations/20260311103638_Initial.Designer.cs b/Console.Data.SqlServer/Console.Data.SqlServer/Migrations/20260425181618_Initial.Designer.cs similarity index 84% rename from Console.Data.SqlServer/Console.Data.SqlServer/Migrations/20260311103638_Initial.Designer.cs rename to Console.Data.SqlServer/Console.Data.SqlServer/Migrations/20260425181618_Initial.Designer.cs index 49ffd9e2..1d670d2a 100644 --- a/Console.Data.SqlServer/Console.Data.SqlServer/Migrations/20260311103638_Initial.Designer.cs +++ b/Console.Data.SqlServer/Console.Data.SqlServer/Migrations/20260425181618_Initial.Designer.cs @@ -12,7 +12,7 @@ namespace Console.Data.SqlServer.Migrations { [DbContext(typeof(SqlServerDbContext))] - [Migration("20260311103638_Initial")] + [Migration("20260425181618_Initial")] partial class Initial { /// @@ -20,7 +20,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("ProductVersion", "10.0.7") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -215,14 +215,24 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("datetimeoffset"); b.Property("CreatedBy") + .IsRequired() .HasMaxLength(256) .HasColumnType("nvarchar(256)"); + b.Property("EntityKey") + .HasColumnType("uniqueidentifier"); + b.Property("EntitySetName") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); + b.Property("EntityState") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + b.Property("EntityTypeName") + .IsRequired() .HasMaxLength(256) .HasColumnType("nvarchar(256)"); @@ -233,23 +243,18 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(256) .HasColumnType("nvarchar(256)"); - b.Property("State") - .HasColumnType("int"); - - b.Property("StateName") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); - b.HasKey("Id"); b.HasIndex("CreatedBy"); + b.HasIndex("EntityKey"); + + b.HasIndex("EntityState"); + b.HasIndex("EntityTypeName"); b.HasIndex("RequestId"); - b.HasIndex("State"); - b.ToTable("__EFAudit", (string)null); }); @@ -325,6 +330,54 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("__EFIdentityApiKey", (string)null); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApiKeyId") + .HasColumnType("uniqueidentifier"); + + b.Property("ClaimType") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId", "ClaimType") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType"); + + b.ToTable("__EFIdentityApiKeyClaim", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApiKeyId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("ApiKeyId", "RoleId") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyRole_ApiKeyId_RoleId"); + + b.ToTable("__EFIdentityApiKeyRole", (string)null); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.Property("Id") @@ -458,10 +511,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("ExpireAt"); - b.HasIndex("IdentityUserId") - .IsUnique() - .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId"); - b.HasIndex("IdentityUserId", "AppId") .IsUnique() .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId_AppId"); @@ -542,6 +591,36 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("IdentityUser"); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + + b.Navigation("Role"); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") @@ -556,8 +635,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") - .WithOne() - .HasForeignKey("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", "IdentityUserId") + .WithMany() + .HasForeignKey("IdentityUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/Console.Data.SqlServer/Console.Data.SqlServer/Migrations/20260311103638_Initial.cs b/Console.Data.SqlServer/Console.Data.SqlServer/Migrations/20260425181618_Initial.cs similarity index 85% rename from Console.Data.SqlServer/Console.Data.SqlServer/Migrations/20260311103638_Initial.cs rename to Console.Data.SqlServer/Console.Data.SqlServer/Migrations/20260425181618_Initial.cs index c2847f6d..63eed93a 100644 --- a/Console.Data.SqlServer/Console.Data.SqlServer/Migrations/20260311103638_Initial.cs +++ b/Console.Data.SqlServer/Console.Data.SqlServer/Migrations/20260425181618_Initial.cs @@ -16,11 +16,11 @@ protected override void Up(MigrationBuilder migrationBuilder) columns: table => new { Id = table.Column(type: "uniqueidentifier", nullable: false), - CreatedBy = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + CreatedBy = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + EntityKey = table.Column(type: "uniqueidentifier", nullable: false), EntitySetName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), - EntityTypeName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), - State = table.Column(type: "int", nullable: false), - StateName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EntityTypeName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + EntityState = table.Column(type: "int", nullable: false, defaultValue: 0), RequestId = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), IsDeleted = table.Column(type: "bigint", nullable: false), CreatedAt = table.Column(type: "datetimeoffset", nullable: false) @@ -291,11 +291,66 @@ protected override void Up(MigrationBuilder migrationBuilder) onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "__EFIdentityApiKeyClaim", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ApiKeyId = table.Column(type: "uniqueidentifier", nullable: false), + ClaimType = table.Column(type: "nvarchar(450)", nullable: false), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityApiKeyClaim", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityApiKeyClaim___EFIdentityApiKey_ApiKeyId", + column: x => x.ApiKeyId, + principalTable: "__EFIdentityApiKey", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "__EFIdentityApiKeyRole", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ApiKeyId = table.Column(type: "uniqueidentifier", nullable: false), + RoleId = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK___EFIdentityApiKeyRole", x => x.Id); + table.ForeignKey( + name: "FK___EFIdentityApiKeyRole___EFIdentityApiKey_ApiKeyId", + column: x => x.ApiKeyId, + principalTable: "__EFIdentityApiKey", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK___EFIdentityApiKeyRole___EFIdentityRole_RoleId", + column: x => x.RoleId, + principalTable: "__EFIdentityRole", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateIndex( name: "IX___EFAudit_CreatedBy", table: "__EFAudit", column: "CreatedBy"); + migrationBuilder.CreateIndex( + name: "IX___EFAudit_EntityKey", + table: "__EFAudit", + column: "EntityKey"); + + migrationBuilder.CreateIndex( + name: "IX___EFAudit_EntityState", + table: "__EFAudit", + column: "EntityState"); + migrationBuilder.CreateIndex( name: "IX___EFAudit_EntityTypeName", table: "__EFAudit", @@ -306,11 +361,6 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "__EFAudit", column: "RequestId"); - migrationBuilder.CreateIndex( - name: "IX___EFAudit_State", - table: "__EFAudit", - column: "State"); - migrationBuilder.CreateIndex( name: "IX___EFAuditProperties_ParentId", table: "__EFAuditProperties", @@ -331,6 +381,23 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "__EFIdentityApiKey", column: "RevokedAt"); + migrationBuilder.CreateIndex( + name: "UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType", + table: "__EFIdentityApiKeyClaim", + columns: new[] { "ApiKeyId", "ClaimType" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX___EFIdentityApiKeyRole_RoleId", + table: "__EFIdentityApiKeyRole", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "UX___EFIdentityApiKeyRole_ApiKeyId_RoleId", + table: "__EFIdentityApiKeyRole", + columns: new[] { "ApiKeyId", "RoleId" }, + unique: true); + migrationBuilder.CreateIndex( name: "RoleNameIndex", table: "__EFIdentityRole", @@ -395,12 +462,6 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "__EFIdentityUserRefreshToken", column: "ExpireAt"); - migrationBuilder.CreateIndex( - name: "UX___EFIdentityUserRefreshToken_IdentityUserId", - table: "__EFIdentityUserRefreshToken", - column: "IdentityUserId", - unique: true); - migrationBuilder.CreateIndex( name: "UX___EFIdentityUserRefreshToken_IdentityUserId_AppId", table: "__EFIdentityUserRefreshToken", @@ -438,7 +499,10 @@ protected override void Down(MigrationBuilder migrationBuilder) name: "__EFDataProtectionKeys"); migrationBuilder.DropTable( - name: "__EFIdentityApiKey"); + name: "__EFIdentityApiKeyClaim"); + + migrationBuilder.DropTable( + name: "__EFIdentityApiKeyRole"); migrationBuilder.DropTable( name: "__EFIdentityRoleClaim"); @@ -467,6 +531,9 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "__EFAudit"); + migrationBuilder.DropTable( + name: "__EFIdentityApiKey"); + migrationBuilder.DropTable( name: "__EFIdentityRole"); diff --git a/Console.Data.SqlServer/Console.Data.SqlServer/Migrations/SqlServerDbContextModelSnapshot.cs b/Console.Data.SqlServer/Console.Data.SqlServer/Migrations/SqlServerDbContextModelSnapshot.cs index f6aecaed..6b10f6b3 100644 --- a/Console.Data.SqlServer/Console.Data.SqlServer/Migrations/SqlServerDbContextModelSnapshot.cs +++ b/Console.Data.SqlServer/Console.Data.SqlServer/Migrations/SqlServerDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("ProductVersion", "10.0.7") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -212,14 +212,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetimeoffset"); b.Property("CreatedBy") + .IsRequired() .HasMaxLength(256) .HasColumnType("nvarchar(256)"); + b.Property("EntityKey") + .HasColumnType("uniqueidentifier"); + b.Property("EntitySetName") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); + b.Property("EntityState") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + b.Property("EntityTypeName") + .IsRequired() .HasMaxLength(256) .HasColumnType("nvarchar(256)"); @@ -230,23 +240,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(256) .HasColumnType("nvarchar(256)"); - b.Property("State") - .HasColumnType("int"); - - b.Property("StateName") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); - b.HasKey("Id"); b.HasIndex("CreatedBy"); + b.HasIndex("EntityKey"); + + b.HasIndex("EntityState"); + b.HasIndex("EntityTypeName"); b.HasIndex("RequestId"); - b.HasIndex("State"); - b.ToTable("__EFAudit", (string)null); }); @@ -322,6 +327,54 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("__EFIdentityApiKey", (string)null); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApiKeyId") + .HasColumnType("uniqueidentifier"); + + b.Property("ClaimType") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId", "ClaimType") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyClaim_ApiKeyId_ClaimType"); + + b.ToTable("__EFIdentityApiKeyClaim", (string)null); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApiKeyId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.HasIndex("ApiKeyId", "RoleId") + .IsUnique() + .HasDatabaseName("UX___EFIdentityApiKeyRole_ApiKeyId_RoleId"); + + b.ToTable("__EFIdentityApiKeyRole", (string)null); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.Property("Id") @@ -455,10 +508,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ExpireAt"); - b.HasIndex("IdentityUserId") - .IsUnique() - .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId"); - b.HasIndex("IdentityUserId", "AppId") .IsUnique() .HasDatabaseName("UX___EFIdentityUserRefreshToken_IdentityUserId_AppId"); @@ -539,6 +588,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("IdentityUser"); }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyClaim", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + }); + + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityApiKeyRole", b => + { + b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityApiKey", "ApiKey") + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApiKey"); + + b.Navigation("Role"); + }); + modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserChangeData", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") @@ -553,8 +632,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", b => { b.HasOne("Nano.Data.Abstractions.Models.Identity.IdentityUserEx", "IdentityUser") - .WithOne() - .HasForeignKey("Nano.Data.Abstractions.Models.Identity.IdentityUserRefreshToken", "IdentityUserId") + .WithMany() + .HasForeignKey("IdentityUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/Console.Data.SqlServer/Console.Data.SqlServer/appsettings.Development.json b/Console.Data.SqlServer/Console.Data.SqlServer/appsettings.Development.json index c4f4436e..960866dc 100644 --- a/Console.Data.SqlServer/Console.Data.SqlServer/appsettings.Development.json +++ b/Console.Data.SqlServer/Console.Data.SqlServer/appsettings.Development.json @@ -1,6 +1,6 @@ { "Data": { - "UseMigrateDatabase": true, + "StartupAction": "Migrate", "ConnectionString": "Server=host.docker.internal,1433;Database=nanoDb;User Id=sa;Password=myPassword_123;Encrypt=False;" } } \ No newline at end of file diff --git a/Console.Data.SqlServer/Console.Data.SqlServer/appsettings.json b/Console.Data.SqlServer/Console.Data.SqlServer/appsettings.json index f52b7fe7..87c218f8 100644 --- a/Console.Data.SqlServer/Console.Data.SqlServer/appsettings.json +++ b/Console.Data.SqlServer/Console.Data.SqlServer/appsettings.json @@ -7,17 +7,14 @@ "BulkBatchSize": 500, "BulkBatchDelay": 1000, "QueryRetryCount": 0, + "StartupAction": "None", "UseLazyLoading": false, - "UseCreateDatabase": false, - "UseMigrateDatabase": false, - "UseSoftDeletetion": false, "UseSensitiveDataLogging": false, - "UseAudit": false, "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, "Repository": { - "UseAutoSave": false, + "UseAutoSave": true, "QueryIncludeDepth": 4 }, "Identity": null, diff --git a/Console.Data.SqlServer/README.md b/Console.Data.SqlServer/README.md index 335cb5bd..cdae2e12 100644 --- a/Console.Data.SqlServer/README.md +++ b/Console.Data.SqlServer/README.md @@ -21,13 +21,13 @@ Nano is referenced directly from source (not via NuGet packages) and is expected This application builds on **[Console.Blank](https://github.com/Nano-Core/Nano.Lessons/tree/master/Console._Blank)**. This example demonstrates how various parts of Nano data work together. All data configuration and registration have been completed, and classes have been implemented -for the data parts, including [Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-models), [Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-mappings), -and the [Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#data-context). +for the data parts, including **[Data Models](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-models)**, **[Data Mappings](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-mappings)**, +and the **[Data Context](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#data-context)**. -The worker creates and retreives a `Example` entity using the Nano [Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data#repositories). Run the +The worker creates and retreives a `Example` entity using the Nano **[Data Repository](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data/README.md#repositories)**. Run the application and observe the console output generated by the `ExampleWorker`. -> 📖 Learn more about **[Nano.Data.SqlServer](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.SqlServer)**. +> 📖 Learn more about **[Nano.Data.SqlServer](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Data.SqlServer/README.md#nanodatamysql)**. ## Registration The following data provider has been registered using `ConfigureServices(...)` in `program.cs`. @@ -57,17 +57,14 @@ Configured the application with the necessary data setup. "BulkBatchSize": 500, "BulkBatchDelay": 1000, "QueryRetryCount": 0, + "StartupAction": "None", "UseLazyLoading": false, - "UseCreateDatabase": false, - "UseMigrateDatabase": false, - "UseSoftDeletetion": false, "UseSensitiveDataLogging": false, - "UseAudit": false, "QuerySplittingBehavior": "SingleQuery", "DefaultCollation": null, "ConnectionString": null, "Repository": { - "UseAutoSave": false, + "UseAutoSave": true, "QueryIncludeDepth": 4 }, "Identity": null, @@ -80,7 +77,7 @@ Configured the application with the necessary data setup. ```json "Data": { - "UseMigrateDatabase": true, + "StartupAction": "Migrate", "ConnectionString": "Server=host.docker.internal,1433;Database=nanoDb;User Id=sa;Password=myPassword_123;Encrypt=False;" } ``` @@ -120,7 +117,7 @@ spec: - name: Data__ConnectionString valueFrom: secretKeyRef: - name: %SERVICE_NAME%-secret + name: %SERVICE_NAME%-sql-auth-secret key: data-connectionstring ``` @@ -129,80 +126,73 @@ Add the following environment variables to the `buid-and-deply.yml`. ```yaml env: - DATA_HOST: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_HOST || secrets.STAGING_SQLSERVER_HOST }} - DATA_NAME: nanoDb - DATA_USER: api-data-sqlserver-user - DATA_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_NANO_DB_PASSWORD || secrets.STAGING_SQLSERVER_NANO_DB_PASSWORD }} - DATA_ADMIN_USER: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_ADMIN_USER || secrets.STAGING_SQLSERVER_ADMIN_USER }} - DATA_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQLSERVER_ADMIN_PASSWORD || secrets.STAGING_SQLSERVER_ADMIN_PASSWORD }} - DATA_CONNECTIONSTRING: Server=${{ env.DATA_HOST }},${{ vars.DATA_SQLSERVER_PORT }};Database=${{ env.DATA_NAME }};User Id=${{ env.DATA_USER }};Password=${{ env.DATA_PASSWORD }}; - DATA_MIGRATION_CONNECTIONSTRING: Server=${{ env.DATA_HOST }},${{ vars.DATA_SQLSERVER_PORT }};Database=${{ env.DATA_NAME }};User Id=${{ env.DATA_ADMIN_USER }};Password=${{ env.DATA_ADMIN_PASSWORD }}; + SQL_NAME: nanoDb + SQL_USER: api-data-sqlserver-user + SQL_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_NANO_DB_PASSWORD || secrets.STAGING_SQL_NANO_DB_PASSWORD }} + SQL_ADMIN_PASSWORD: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_SQL_ADMIN_PASSWORD || secrets.STAGING_SQL_ADMIN_PASSWORD }} ``` Additionally, this step has been added to ensure database migrations are applied, and the application database user has been created before the application is deployed. ```yaml -- name: Database Migration +- name: Database Migration & User shell: pwsh run: | + $env:SQL_HOST = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].fullyQualifiedDomainName" -o tsv; + $env:SQL_PORT = "1433" + $env:SQL_ADMIN_USER = az sql server list -g $env:AZURE_GROUP_DATABASE --query "[0].administratorLogin" -o tsv; + $env:SQL_MIGRATION_CONNECTIONSTRING = "Server=$env:SQL_HOST,$env:SQL_PORT;Database=$env:SQL_NAME;User Id=$env:SQL_ADMIN_USER;Password=$env:SQL_ADMIN_PASSWORD;Encrypt=True;TrustServerCertificate=True;"; + dotnet ef database update ` --no-build ` --startup-project $env:APP_NAME ` - --connection "$env:DATA_MIGRATION_CONNECTIONSTRING" `; + --connection "$env:SQL_MIGRATION_CONNECTIONSTRING"; if ($LastExitCode -ne 0) { throw "error"; }; - - sudo apt-get update - sudo apt-get install -y mssql-tools unixodbc-dev + + apt-get update + apt-get install -y mssql-tools unixodbc-dev $loginExists = sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d master ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:DATA_USER';" + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d master ` + -h -1 ` + -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.server_principals WHERE name = '$env:SQL_USER';" if ($loginExists -eq 0) { sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d master ` - -Q "CREATE LOGIN [$env:DATA_USER] WITH PASSWORD = '$env:DATA_PASSWORD';" - } + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d master ` + -Q "CREATE LOGIN [$env:SQL_USER] WITH PASSWORD = '$env:SQL_PASSWORD';" + }; $userExists = sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d $env:DATA_NAME ` - -h -1 ` - -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:DATA_USER';" - - if ($userExists -eq 0) - { - sqlcmd ` - -S "$env:DATA_HOST,$env:DATA_SQLSERVER_PORT" ` - -U $env:DATA_ADMIN_USER ` - -P $env:DATA_ADMIN_PASSWORD ` - -d $env:DATA_NAME ` - -Q "CREATE USER [$env:DATA_USER] FOR LOGIN [$env:DATA_USER]; - ALTER ROLE db_datareader ADD MEMBER [$env:DATA_USER]; - ALTER ROLE db_datawriter ADD MEMBER [$env:DATA_USER];" - } + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d $env:SQL_NAME ` + -h -1 ` + -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM sys.database_principals WHERE name = '$env:SQL_USER';" + + if ($userExists -eq 0) + { + sqlcmd ` + -S "$env:SQL_HOST,$env:SQL_PORT" ` + -U $env:SQL_ADMIN_USER ` + -P $env:SQL_ADMIN_PASSWORD ` + -d $env:SQL_NAME ` + -Q "CREATE USER [$env:SQL_USER] FOR LOGIN [$env:SQL_USER]; + ALTER ROLE db_datareader ADD MEMBER [$env:SQL_USER]; + ALTER ROLE db_datawriter ADD MEMBER [$env:SQL_USER];" + }; ``` -Last, the application connectionstring must be added in a secret in Kuberntes. The `Kubernetes Deploy` step has been updated with the following. - -```yaml -sudo kubectl create secret generic $env:SERVICE_NAME-secret ` --from-literal=data-connectionstring=$env:DATA_CONNECTIONSTRING --save-config --dry-run=client -o yaml | sudo kubectl apply -f -; -if ($LastExitCode -ne 0) -{ - throw "error"; -}; -``` +Last, an additional template has been added to the deployment for storing the application connectionstring in a Kuberntes secret. diff --git a/Console.Eventing.RabbitMq/.github/workflows/build-and-deploy.yml b/Console.Eventing.RabbitMq/.github/workflows/build-and-deploy.yml index 8db75f9e..0233cbe7 100644 --- a/Console.Eventing.RabbitMq/.github/workflows/build-and-deploy.yml +++ b/Console.Eventing.RabbitMq/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Eventing.RabbitMq IMAGE_NAME: console.eventing.rabbitmq @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi @@ -33,23 +36,28 @@ env: DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -78,7 +86,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -94,9 +102,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -106,14 +114,14 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.Eventing.RabbitMq/.kubernetes/cronjob.yaml b/Console.Eventing.RabbitMq/.kubernetes/cronjob.yaml index 5113e567..44ab91fe 100644 --- a/Console.Eventing.RabbitMq/.kubernetes/cronjob.yaml +++ b/Console.Eventing.RabbitMq/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -26,11 +27,16 @@ spec: image: %CONTAINER_REGISTRY_HOST%/%IMAGE_NAME%:%VERSION% imagePullPolicy: Always env: + - name: Eventing__Credentials__Id + valueFrom: + secretKeyRef: + name: rabbitmq-auth + key: username - name: Eventing__Credentials__Secret valueFrom: secretKeyRef: - name: rabbitmq - key: rabbitmq-password + name: rabbitmq-auth + key: password envFrom: - configMapRef: name: %SERVICE_NAME%-config diff --git a/Console.Eventing.RabbitMq/Console.Eventing.RabbitMq/Eventing/EventingHandler.cs b/Console.Eventing.RabbitMq/Console.Eventing.RabbitMq/Eventing/EventingHandler.cs index ab33b2af..4fc890f1 100644 --- a/Console.Eventing.RabbitMq/Console.Eventing.RabbitMq/Eventing/EventingHandler.cs +++ b/Console.Eventing.RabbitMq/Console.Eventing.RabbitMq/Eventing/EventingHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; using Console.Eventing.RabbitMq.Eventing.Models; using Nano.Eventing.Abstractions; @@ -14,8 +15,9 @@ public class EventingHandler : BaseEventHandler /// /// The . /// Whether the event is retrying. + /// The . /// Nothing. - public override async Task CallbackAsync(EventModel @event, bool isRetrying) + public override async Task CallbackAsync(EventModel @event, bool isRetrying, CancellationToken cancellationToken = default) { await Task.CompletedTask; diff --git a/Console.Eventing.RabbitMq/Console.Eventing.RabbitMq/appsettings.Development.json b/Console.Eventing.RabbitMq/Console.Eventing.RabbitMq/appsettings.Development.json index 8593c62d..b1f4b0f1 100644 --- a/Console.Eventing.RabbitMq/Console.Eventing.RabbitMq/appsettings.Development.json +++ b/Console.Eventing.RabbitMq/Console.Eventing.RabbitMq/appsettings.Development.json @@ -1,2 +1,8 @@ { + "Eventing": { + "Credentials": { + "Id": "rabbitmq_user", + "Secret": "password" + } + } } \ No newline at end of file diff --git a/Console.Eventing.RabbitMq/Console.Eventing.RabbitMq/appsettings.json b/Console.Eventing.RabbitMq/Console.Eventing.RabbitMq/appsettings.json index b829b3fe..647f1497 100644 --- a/Console.Eventing.RabbitMq/Console.Eventing.RabbitMq/appsettings.json +++ b/Console.Eventing.RabbitMq/Console.Eventing.RabbitMq/appsettings.json @@ -11,8 +11,8 @@ "Heartbeat": 60, "PrefetchCount": 50, "Credentials": { - "Id": "rabbitmq_user", - "Secret": "password" + "Id": null, + "Secret": null } } } \ No newline at end of file diff --git a/Console.Eventing.RabbitMq/README.md b/Console.Eventing.RabbitMq/README.md index bba70412..d0a49baa 100644 --- a/Console.Eventing.RabbitMq/README.md +++ b/Console.Eventing.RabbitMq/README.md @@ -31,7 +31,7 @@ This message is written by the `EventHandler` when the event is successfully rec You can access the RabbitMQ management interface here: **[http://localhost:15672](http://localhost:15672)**. From there, you can monitor the messages being published and consumed in real time. -> 📖 Learn more about **[Nano.Eventing.RabbitMq](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Eventing.RabbitMq)**. +> 📖 Learn more about **[Nano.Eventing.RabbitMq](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Eventing.RabbitMq/README.md#nanoeventingrabbitmq)**. ## Registration The following eventing provider has been registered using `ConfigureServices(...)` in `Program.cs`. @@ -47,10 +47,9 @@ The following eventing provider has been registered using `ConfigureServices(... ``` ## Configuration -Configured the application with the necessary eventing setup. +Configured the application `appsettings.json` with the necessary eventing setup. ```json -"App": { "Eventing": { "Host": "rabbitmq", "VHost": "/", @@ -66,6 +65,17 @@ Configured the application with the necessary eventing setup. } ``` +...and the `appsettings.Development.json` eventing configuration. + +```json +"Eventing": { + "Credentials": { + "Id": "rabbitmq_user", + "Secret": "password" + } +} +``` + ## Docker Compose Added RabbitMQ as a service dependency in `docker-compose.yml`. @@ -100,11 +110,17 @@ spec: spec: containers: env: + - name: Eventing__Credentials__Id + valueFrom: + secretKeyRef: + name: rabbitmq-auth + key: username + envFrom: - name: Eventing__Credentials__Secret valueFrom: secretKeyRef: - name: rabbitmq - key: rabbitmq-password + name: rabbitmq-auth + key: password ``` > ⚠️ The `rabbitmq` secret is created alongside the **[Nano Azure Kubernetes Eventing](https://github.com/Nano-Core/Nano.Azure.Kubernetes/tree/master/Nano.Azure.Kubernetes.RabbitMQ)** diff --git a/Console.ExceptionHandling/.github/workflows/build-and-deploy.yml b/Console.ExceptionHandling/.github/workflows/build-and-deploy.yml index bade116a..0ad7df32 100644 --- a/Console.ExceptionHandling/.github/workflows/build-and-deploy.yml +++ b/Console.ExceptionHandling/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.ExceptionHandling IMAGE_NAME: console.exceptionhandling @@ -8,22 +13,19 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi @@ -33,23 +35,28 @@ env: DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -78,7 +85,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -94,9 +101,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -106,14 +113,14 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.ExceptionHandling/.kubernetes/cronjob.yaml b/Console.ExceptionHandling/.kubernetes/cronjob.yaml index 64a6dce3..32007e73 100644 --- a/Console.ExceptionHandling/.kubernetes/cronjob.yaml +++ b/Console.ExceptionHandling/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux diff --git a/Console.ExceptionHandling/README.md b/Console.ExceptionHandling/README.md index 4735fb33..bff2a21a 100644 --- a/Console.ExceptionHandling/README.md +++ b/Console.ExceptionHandling/README.md @@ -20,4 +20,4 @@ This application demonstrates exception handling for a console application. Run the application and observe the `Exception` being thrown in the worker `OnStartAsync()`. -> 📖 Learn more about **[Nano Exception Handling](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Console#exception-handling)**. +> 📖 Learn more about **[Nano Exception Handling](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Console/README.md#exception-handling)**. diff --git a/Console.Localization/.github/workflows/build-and-deploy.yml b/Console.Localization/.github/workflows/build-and-deploy.yml index 3a824b45..aacf89be 100644 --- a/Console.Localization/.github/workflows/build-and-deploy.yml +++ b/Console.Localization/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Localization IMAGE_NAME: console.localization @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi @@ -33,23 +36,28 @@ env: DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -78,7 +86,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -94,9 +102,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -106,14 +114,14 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.Localization/.kubernetes/cronjob.yaml b/Console.Localization/.kubernetes/cronjob.yaml index 64a6dce3..32007e73 100644 --- a/Console.Localization/.kubernetes/cronjob.yaml +++ b/Console.Localization/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux diff --git a/Console.Localization/README.md b/Console.Localization/README.md index 094e233d..676793bd 100644 --- a/Console.Localization/README.md +++ b/Console.Localization/README.md @@ -20,7 +20,7 @@ This application demonstrates configuring `Localization` in a Nano console appli Run the application and observe how the configured localization is used when printing out `DateTimeOffset.Now`. -> 📖 Learn more about **[Nano Console Localization](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Console#localization)**. +> 📖 Learn more about **[Nano Console Localization](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Console/README.md#localization)**. ## Configuration ```json diff --git a/Console.Logging.Log4Net/.github/workflows/build-and-deploy.yml b/Console.Logging.Log4Net/.github/workflows/build-and-deploy.yml index 27242720..4dc97976 100644 --- a/Console.Logging.Log4Net/.github/workflows/build-and-deploy.yml +++ b/Console.Logging.Log4Net/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Logging.Log4Net IMAGE_NAME: console.logging.log4net @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi @@ -33,23 +36,28 @@ env: DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -78,7 +86,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -94,9 +102,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -106,14 +114,14 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.Logging.Log4Net/.kubernetes/cronjob.yaml b/Console.Logging.Log4Net/.kubernetes/cronjob.yaml index 64a6dce3..32007e73 100644 --- a/Console.Logging.Log4Net/.kubernetes/cronjob.yaml +++ b/Console.Logging.Log4Net/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux diff --git a/Console.Logging.Log4Net/README.md b/Console.Logging.Log4Net/README.md index 06204354..908b42a8 100644 --- a/Console.Logging.Log4Net/README.md +++ b/Console.Logging.Log4Net/README.md @@ -23,7 +23,7 @@ This application demonstrates logging with Log4Net for a console application. Run the application and observe how `ExampleWorker` logs a warning to the console. Also note the `LogLevelOverrides` configuration, where logs under the `Microsoft` namespace are set to `Warning`, which suppresses several informational messages during application startup. -> 📖 Learn more about **[Nano.Logging.Log4Net](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.Log4Net)**. +> 📖 Learn more about **[Nano.Logging.Log4Net](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.Log4Net/README.md#nanologginglog4net)**. ## Registration The following logging has been registered using `ConfigureServices(...)` in `program.cs`. diff --git a/Console.Logging.Microsoft/.github/workflows/build-and-deploy.yml b/Console.Logging.Microsoft/.github/workflows/build-and-deploy.yml index 7f53a173..1c105bb3 100644 --- a/Console.Logging.Microsoft/.github/workflows/build-and-deploy.yml +++ b/Console.Logging.Microsoft/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Logging.Microsoft IMAGE_NAME: console.logging.microsoft @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi @@ -33,23 +36,28 @@ env: DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -78,7 +86,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -94,9 +102,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -106,14 +114,14 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.Logging.Microsoft/.kubernetes/cronjob.yaml b/Console.Logging.Microsoft/.kubernetes/cronjob.yaml index 64a6dce3..32007e73 100644 --- a/Console.Logging.Microsoft/.kubernetes/cronjob.yaml +++ b/Console.Logging.Microsoft/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux diff --git a/Console.Logging.Microsoft/README.md b/Console.Logging.Microsoft/README.md index 4eb5a2a4..c110bd22 100644 --- a/Console.Logging.Microsoft/README.md +++ b/Console.Logging.Microsoft/README.md @@ -23,7 +23,7 @@ This application demonstrates logging with Microsoft for a console application. Run the application and observe how `ExampleWorker` logs a warning to the console. Also note the `LogLevelOverrides` configuration, where logs under the `Microsoft` namespace are set to `Warning`, which suppresses several informational messages during application startup. -> 📖 Learn more about **[Nano.Logging.Microsoft](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.Microsoft)**. +> 📖 Learn more about **[Nano.Logging.Microsoft](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.Microsoft/README.md#nanologgingmicrosoft)**. ## Registration The following logging has been registered using `ConfigureServices(...)` in `program.cs`. diff --git a/Console.Logging.NLog/.github/workflows/build-and-deploy.yml b/Console.Logging.NLog/.github/workflows/build-and-deploy.yml index e171ed89..01436524 100644 --- a/Console.Logging.NLog/.github/workflows/build-and-deploy.yml +++ b/Console.Logging.NLog/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Logging.NLog IMAGE_NAME: console.logging.nlog @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi @@ -33,23 +36,28 @@ env: DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -78,7 +86,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -94,9 +102,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -106,14 +114,14 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.Logging.NLog/.kubernetes/cronjob.yaml b/Console.Logging.NLog/.kubernetes/cronjob.yaml index 64a6dce3..32007e73 100644 --- a/Console.Logging.NLog/.kubernetes/cronjob.yaml +++ b/Console.Logging.NLog/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux diff --git a/Console.Logging.NLog/README.md b/Console.Logging.NLog/README.md index 5bce3ceb..f5724807 100644 --- a/Console.Logging.NLog/README.md +++ b/Console.Logging.NLog/README.md @@ -23,7 +23,7 @@ This application demonstrates logging with NLog for a console application. Run the application and observe how `ExampleWorker` logs a warning to the console. Also note the `LogLevelOverrides` configuration, where logs under the `Microsoft` namespace are set to `Warning`, which suppresses several informational messages during application startup. -> 📖 Learn more about **[Nano.Logging.NLog](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.NLog)**. +> 📖 Learn more about **[Nano.Logging.NLog](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.NLog/README.md#nanologgingnlog)**. ## Registration The following logging has been registered using `ConfigureServices(...)` in `program.cs`. diff --git a/Console.Logging.Serilog/.github/workflows/build-and-deploy.yml b/Console.Logging.Serilog/.github/workflows/build-and-deploy.yml index 7930e968..fb5412b5 100644 --- a/Console.Logging.Serilog/.github/workflows/build-and-deploy.yml +++ b/Console.Logging.Serilog/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Logging.Serilog IMAGE_NAME: console.logging.serilog @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi @@ -33,23 +36,28 @@ env: DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -78,7 +86,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -94,9 +102,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -106,14 +114,14 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.Logging.Serilog/.kubernetes/cronjob.yaml b/Console.Logging.Serilog/.kubernetes/cronjob.yaml index 64a6dce3..32007e73 100644 --- a/Console.Logging.Serilog/.kubernetes/cronjob.yaml +++ b/Console.Logging.Serilog/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux diff --git a/Console.Logging.Serilog/README.md b/Console.Logging.Serilog/README.md index a79aadc9..df5621de 100644 --- a/Console.Logging.Serilog/README.md +++ b/Console.Logging.Serilog/README.md @@ -23,7 +23,7 @@ This application demonstrates logging with Serilog for a console application. Run the application and observe how `ExampleWorker` logs a warning to the console. Also note the `LogLevelOverrides` configuration, where logs under the `Microsoft` namespace are set to `Warning`, which suppresses several informational messages during application startup. -> 📖 Learn more about **[Nano.Logging.Serilog](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.Serilog)**. +> 📖 Learn more about **[Nano.Logging.Serilog](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Logging.Serilog/README.md#nanologgingserilog)**. ## Registration The following logging has been registered using `ConfigureServices(...)` in `program.cs`. diff --git a/Console.StartupTasks/.github/workflows/build-and-deploy.yml b/Console.StartupTasks/.github/workflows/build-and-deploy.yml index df9d5e96..80e559e4 100644 --- a/Console.StartupTasks/.github/workflows/build-and-deploy.yml +++ b/Console.StartupTasks/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.StartupTasks IMAGE_NAME: console.startuptasks @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi @@ -33,23 +36,28 @@ env: DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -78,7 +86,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -94,9 +102,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -106,14 +114,14 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.StartupTasks/.kubernetes/cronjob.yaml b/Console.StartupTasks/.kubernetes/cronjob.yaml index 64a6dce3..32007e73 100644 --- a/Console.StartupTasks/.kubernetes/cronjob.yaml +++ b/Console.StartupTasks/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux diff --git a/Console.StartupTasks/Console.StartupTasks/Startup/ExampleStartupTask.cs b/Console.StartupTasks/Console.StartupTasks/Startup/ExampleStartupTask.cs index bbde7b51..830990c2 100644 --- a/Console.StartupTasks/Console.StartupTasks/Startup/ExampleStartupTask.cs +++ b/Console.StartupTasks/Console.StartupTasks/Startup/ExampleStartupTask.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Nano.App.Startup; +using Nano.App.StartUp; namespace Console.StartupTasks.Startup; @@ -10,7 +10,7 @@ namespace Console.StartupTasks.Startup; /// Example Startup Task. /// /// The . -public class ExampleStartupTask(ILogger logger) : BaseStartupTask(logger) +public class ExampleStartupTask(ILogger logger) : BaseStartupTask(logger) { /// /// Example On Start. diff --git a/Console.StartupTasks/README.md b/Console.StartupTasks/README.md index 47cef7a8..8277099a 100644 --- a/Console.StartupTasks/README.md +++ b/Console.StartupTasks/README.md @@ -21,4 +21,4 @@ ensuring that the console application only runs after initialization is finished Run the application and observe how both example workers wait for the startup tasks to complete before executing their logic. -> 📖 Learn more about **[Nano Startup Tasks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App#startup-tasks)**. +> 📖 Learn more about **[Nano Startup Tasks](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App/README.md#startup-tasks)**. diff --git a/Console.Storage.Azure/.github/workflows/build-and-deploy.yml b/Console.Storage.Azure/.github/workflows/build-and-deploy.yml index 45dd4072..140653e8 100644 --- a/Console.Storage.Azure/.github/workflows/build-and-deploy.yml +++ b/Console.Storage.Azure/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Storage.Azure IMAGE_NAME: console.storage.azure @@ -8,52 +13,55 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} + AZURE_GROUP_STORAGE: ${{ vars.AZURE_RESOURCE_GROUP_STORAGE }} + AZURE_GROUP_BACKUP: ${{ vars.AZURE_RESOURCE_GROUP_BACKUP }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi KUBERNETES_CPU_REQUEST: 50m KUBERNETES_CPU_LIMIT: 150m KUBERNETES_CRONJOB_SCHEDULE: "0 * * * *" - STORAGE_SHARE_NAME: nano-storage-azure - STORAGE_CREDENTIALS_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_STORAGE_CREDENTIALS_ID || secrets.STAGING_STORAGE_CREDENTIALS_ID }} - STORAGE_CREDENTIALS_SECRET: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_STORAGE_CREDENTIALS_SECRET || secrets.STAGING_STORAGE_CREDENTIALS_SECRET }} STORAGE_SIZE: 1000 + STORAGE_SHARE_NAME: nano-storage-azure DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -78,14 +86,41 @@ jobs: - name: Create Fileshare shell: pwsh run: | - $env:EXISTING_FILE_SHARE = sudo az storage share list --account-name $env:STORAGE_CREDENTIALS_ID --account-key $env:STORAGE_CREDENTIALS_SECRET --query "[?contains(name, '$env:STORAGE_SHARE_NAME')].[name]" -o tsv; - if ([string]::IsNullOrEmpty($env:EXISTING_FILE_SHARE)) + $env:STORAGE_ACCOUNT_NAME = az storage account list -g $env:AZURE_GROUP_STORAGE --query [0].name -o tsv; + + $env:FILE_SHARE_EXISTS = az storage share-rm exists ` + -g $env:AZURE_GROUP_STORAGE ` + -n $env:STORAGE_SHARE_NAME ` + --storage-account $env:STORAGE_ACCOUNT_NAME ` + --query exists; + + if ($env:FILE_SHARE_EXISTS -eq "false") { - sudo az storage share create -n $env:STORAGE_SHARE_NAME --account-name $env:STORAGE_CREDENTIALS_ID --account-key $env:STORAGE_CREDENTIALS_SECRET --quota $env:STORAGE_SIZE; + az storage share-rm create ` + -g $env:AZURE_GROUP_STORAGE ` + -n $env:STORAGE_SHARE_NAME ` + --storage-account $env:STORAGE_ACCOUNT_NAME ` + --access-tier TransactionOptimized ` + --quota $env:STORAGE_SIZE; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:BACKUP_VAULT_NAME = az backup vault list -g $env:AZURE_GROUP_BACKUP --query [0].name -o tsv; + + az backup protection enable-for-azurefileshare ` + -g $env:AZURE_GROUP_BACKUP ` + -v $env:BACKUP_VAULT_NAME ` + -p $env:STORAGE_ACCOUNT_NAME-backup-policy ` + --storage-account $env:STORAGE_ACCOUNT_NAME ` + --azure-file-share $env:STORAGE_SHARE_NAME; + if ($LastExitCode -ne 0) { throw "error"; - }; + }; } - name: Publish Image @@ -95,7 +130,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -111,9 +146,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -122,15 +157,29 @@ jobs: - name: Kubernetes Deploy shell: pwsh run: | + Get-Content .kubernetes/storage-pv.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/storage-pv.tmp.yaml; + kubectl apply -f .kubernetes/storage-pv.tmp.yaml; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + Get-Content .kubernetes/storage-pvc.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/storage-pvc.tmp.yaml; + kubectl apply -f .kubernetes/storage-pvc.tmp.yaml; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.Storage.Azure/.kubernetes/cronjob.yaml b/Console.Storage.Azure/.kubernetes/cronjob.yaml index 03581cdf..ea18271f 100644 --- a/Console.Storage.Azure/.kubernetes/cronjob.yaml +++ b/Console.Storage.Azure/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -25,17 +26,6 @@ spec: - name: %SERVICE_NAME% image: %CONTAINER_REGISTRY_HOST%/%IMAGE_NAME%:%VERSION% imagePullPolicy: Always - env: - - name: Storage__Credentials__Id - valueFrom: - secretKeyRef: - name: storage-account-secret - key: azurestorageaccountname - - name: Storage__Credentials__Secret - valueFrom: - secretKeyRef: - name: storage-account-secret - key: azurestorageaccountkey envFrom: - configMapRef: name: %SERVICE_NAME%-config @@ -64,10 +54,8 @@ spec: restartPolicy: OnFailure volumes: - name: %SERVICE_NAME%-volume - azureFile: - secretName: storage-account-secret - shareName: %STORAGE_SHARE_NAME% - readOnly: false + persistentVolumeClaim: + claimName: %SERVICE_NAME%-azurefile-pvc - name: tmp emptyDir: {} imagePullSecrets: diff --git a/Console.Storage.Azure/.kubernetes/storage-pv.yaml b/Console.Storage.Azure/.kubernetes/storage-pv.yaml new file mode 100644 index 00000000..b6281fb8 --- /dev/null +++ b/Console.Storage.Azure/.kubernetes/storage-pv.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: %SERVICE_NAME%-azurefile-pv +spec: + capacity: + storage: %STORAGE_SIZE% + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: azurefile-static + csi: + driver: file.csi.azure.com + volumeHandle: %STORAGE_SHARE_NAME% + volumeAttributes: + shareName: %STORAGE_SHARE_NAME% + storageAccount: %STORAGE_ACCOUNT_NAME% \ No newline at end of file diff --git a/Console.Storage.Azure/.kubernetes/storage-pvc.yaml b/Console.Storage.Azure/.kubernetes/storage-pvc.yaml new file mode 100644 index 00000000..252c7857 --- /dev/null +++ b/Console.Storage.Azure/.kubernetes/storage-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: %SERVICE_NAME%-azurefile-pvc + namespace: %KUBERNETES_NAMESPACE% +spec: + accessModes: + - ReadWriteMany + storageClassName: azurefile-static + resources: + requests: + storage: %STORAGE_SIZE% \ No newline at end of file diff --git a/Console.Storage.Azure/Console.Storage.Azure.sln b/Console.Storage.Azure/Console.Storage.Azure.sln index a75c97a6..63806ffa 100644 --- a/Console.Storage.Azure/Console.Storage.Azure.sln +++ b/Console.Storage.Azure/Console.Storage.Azure.sln @@ -21,6 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".kubernetes", ".kubernetes" ProjectSection(SolutionItems) = preProject .kubernetes\configmap.yaml = .kubernetes\configmap.yaml .kubernetes\cronjob.yaml = .kubernetes\cronjob.yaml + .kubernetes\storage-pv.yaml = .kubernetes\storage-pv.yaml + .kubernetes\storage-pvc.yaml = .kubernetes\storage-pvc.yaml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Console.Storage.Azure", ".tests\Tests.Console.Storage.Azure\Tests.Console.Storage.Azure.csproj", "{EA602798-C127-44E3-8CBA-27E9BA269EB7}" diff --git a/Console.Storage.Azure/Console.Storage.Azure/appsettings.json b/Console.Storage.Azure/Console.Storage.Azure/appsettings.json index 85d28d81..061420b2 100644 --- a/Console.Storage.Azure/Console.Storage.Azure/appsettings.json +++ b/Console.Storage.Azure/Console.Storage.Azure/appsettings.json @@ -3,10 +3,6 @@ "Version": "1.0.0.0" }, "Storage": { - "ShareName": "nano-storage-azure", - "Credentials": { - "Id": null, - "Secret": null - } + "ShareName": "nano-storage-azure" } } \ No newline at end of file diff --git a/Console.Storage.Azure/README.md b/Console.Storage.Azure/README.md index 769c24ae..3731aeef 100644 --- a/Console.Storage.Azure/README.md +++ b/Console.Storage.Azure/README.md @@ -25,7 +25,7 @@ This application demonstrates creating a file and saving it to a mapped file sha When running locally, files are **NOT** written to the Azure File Share. Instead, Docker mounts a local directory to simulate the file share. Files are saved in `.docker/bin/`. -> 📖 Learn more about **[Nano.Storage.Azure](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Storage.Azure)**. +> 📖 Learn more about **[Nano.Storage.Azure](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Storage.Azure/README#nanostorageazure)**. ## Registration The following storage has been registered using `ConfigureServices(...)` in `program.cs`. @@ -44,11 +44,7 @@ Configured the application with the necessary storage setup. ```json "Storage": { - "ShareName": "nano-storage-azure", - "Credentials": { - "Id": "id", - "Secret": "secret" - } + "ShareName": "nano-storage-azure" } ``` @@ -62,37 +58,24 @@ docker ``` ## Kubernetes -Added the volumes, volume mounts and secrets to the `cronjob.yaml`. +Added two new kubernetes templaets, the `storage-pv.yaml` and `storage-pvc.yaml`. Updated the `cronjob.yaml` mounting the volume. -```json +```yaml spec: template: spec: containers: - env: - - name: Storage__Credentials__Id - valueFrom: - secretKeyRef: - name: storage-account-secret - key: azurestorageaccountname - - name: Storage__Credentials__Secret - valueFrom: - secretKeyRef: - name: storage-account-secret - key: azurestorageaccountkey volumeMounts: - - name: tmp - mountPath: /tmp - name: %SERVICE_NAME%-volume mountPath: /mnt/%STORAGE_SHARE_NAME% + - name: tmp + mountPath: /tmp volumes: + - name: %SERVICE_NAME%-volume + persistentVolumeClaim: + claimName: %SERVICE_NAME%-azurefile-pvc - name: tmp emptyDir: {} - - name: %SERVICE_NAME%-volume - azureFile: - secretName: storage-account-secret - shareName: %STORAGE_SHARE_NAME% - readOnly: false ``` ## GitHub Actions @@ -100,10 +83,10 @@ Add the following environment variables to the `buid-and-deply.yml`. ```yaml env: - STORAGE_SHARE_NAME: nano-storage-azure - STORAGE_CREDENTIALS_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_STORAGE_CREDENTIALS_ID || secrets.STAGING_STORAGE_CREDENTIALS_ID }} - STORAGE_CREDENTIALS_SECRET: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_STORAGE_CREDENTIALS_SECRET || secrets.STAGING_STORAGE_CREDENTIALS_SECRET }} + AZURE_GROUP_BACKUP: ${{ vars.AZURE_BACKUP_RESOURCE_GROUP }} + AZURE_GROUP_STORAGE: ${{ vars.AZURE_STORAGE_RESOURCE_GROUP }} STORAGE_SIZE: 1000 + STORAGE_SHARE_NAME: nano-storage-azure ``` Additionally, this step has been added to ensure the file share is created before the application is deployed. @@ -112,13 +95,40 @@ Additionally, this step has been added to ensure the file share is created befor - name: Create Fileshare shell: pwsh run: | - $env:EXISTING_FILE_SHARE = sudo az storage share list --account-name $env:STORAGE_CREDENTIALS_ID --account-key $env:STORAGE_CREDENTIALS_SECRET --query "[?contains(name, '$env:STORAGE_SHARE_NAME')].[name]" -o tsv; - if ([string]::IsNullOrEmpty($env:EXISTING_FILE_SHARE)) + $env:STORAGE_ACCOUNT_NAME = sudo az storage account list -g $env:AZURE_GROUP_STORAGE --query [0].name -o tsv; + + $env:FILE_SHARE_EXISTS = sudo az storage share-rm exists ` + -g $env:AZURE_GROUP_STORAGE ` + -n $env:STORAGE_SHARE_NAME ` + --storage-account $env:STORAGE_ACCOUNT_NAME ` + --query exists; + + if ($env:FILE_SHARE_EXISTS -eq "false") { - sudo az storage share create -n $env:STORAGE_SHARE_NAME --account-name $env:STORAGE_CREDENTIALS_ID --account-key $env:STORAGE_CREDENTIALS_SECRET --quota $env:STORAGE_SIZE; + sudo az storage share-rm create ` + -g $env:AZURE_GROUP_STORAGE ` + -n $env:STORAGE_SHARE_NAME ` + --storage-account $env:STORAGE_ACCOUNT_NAME ` + --access-tier TransactionOptimized ` + --quota $env:STORAGE_SIZE; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + $env:BACKUP_VAULT_NAME = sudo az backup vault list -g $env:AZURE_GROUP_BACKUP --query [0].name -o tsv; + + sudo az backup protection enable-for-azurefileshare ` + -g $env:AZURE_GROUP_BACKUP ` + -v $env:BACKUP_VAULT_NAME ` + -p $env:STORAGE_ACCOUNT_NAME-backup-policy ` + --storage-account $env:STORAGE_ACCOUNT_NAME ` + --azure-file-share $env:STORAGE_SHARE_NAME; + if ($LastExitCode -ne 0) { throw "error"; - }; + }; } ``` \ No newline at end of file diff --git a/Console.Storage.Local/.github/workflows/build-and-deploy.yml b/Console.Storage.Local/.github/workflows/build-and-deploy.yml index 6fcaacb3..236021f4 100644 --- a/Console.Storage.Local/.github/workflows/build-and-deploy.yml +++ b/Console.Storage.Local/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Storage.Local IMAGE_NAME: console.storage.local @@ -8,49 +13,53 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi KUBERNETES_CPU_REQUEST: 50m KUBERNETES_CPU_LIMIT: 150m KUBERNETES_CRONJOB_SCHEDULE: "0 * * * *" + STORAGE_SIZE: 1000 STORAGE_SHARE_NAME: nano-storage-local DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -79,7 +88,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -95,9 +104,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -107,28 +116,28 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/storage-storageclass.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/storage-storageclass.tmp.yaml; - sudo kubectl apply -f .kubernetes/storage-storageclass.tmp.yaml; + kubectl apply -f .kubernetes/storage-storageclass.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/storage-pvc.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/storage-pvc.tmp.yaml; - sudo kubectl apply -f .kubernetes/storage-pvc.tmp.yaml; + kubectl apply -f .kubernetes/storage-pvc.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.Storage.Local/.kubernetes/cronjob.yaml b/Console.Storage.Local/.kubernetes/cronjob.yaml index a2a45d83..0604cbad 100644 --- a/Console.Storage.Local/.kubernetes/cronjob.yaml +++ b/Console.Storage.Local/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux diff --git a/Console.Storage.Local/README.md b/Console.Storage.Local/README.md index 5ced1ea7..f8b0ac76 100644 --- a/Console.Storage.Local/README.md +++ b/Console.Storage.Local/README.md @@ -24,7 +24,7 @@ This application builds on **[Console.Blank](https://github.com/Nano-Core/Nano.L This application demonstrates creating a file and saving it to a locally mapped file-share. Files are saved in `.docker/bin/`. -> 📖 Learn more about **[Nano.Storage.Local](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Storage.Local)**. +> 📖 Learn more about **[Nano.Storage.Local](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.Storage.Local/README#nanostoragelocal)**. ## Registration The following storage has been registered using `ConfigureServices(...)` in `program.cs`. @@ -61,7 +61,7 @@ Added two additional kubernetes templates, `storage-storageclass.yaml` and `stor Also, updated `cronjob.yaml` adding the volumes and volume mounts. -```json +```yaml spec: template: spec: diff --git a/Console.Workers/.github/workflows/build-and-deploy.yml b/Console.Workers/.github/workflows/build-and-deploy.yml index bca9f1a9..16aadd83 100644 --- a/Console.Workers/.github/workflows/build-and-deploy.yml +++ b/Console.Workers/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Workers IMAGE_NAME: console.workers @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi @@ -33,23 +36,28 @@ env: DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -78,7 +86,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -94,9 +102,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -106,14 +114,14 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console.Workers/.kubernetes/cronjob.yaml b/Console.Workers/.kubernetes/cronjob.yaml index e6c9edaa..503cd99a 100644 --- a/Console.Workers/.kubernetes/cronjob.yaml +++ b/Console.Workers/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux diff --git a/Console.Workers/README.md b/Console.Workers/README.md index e207e06c..6f891e3c 100644 --- a/Console.Workers/README.md +++ b/Console.Workers/README.md @@ -20,4 +20,4 @@ This application demonstrates multiple workers for a console application. Run the application and observe how both implemented workers execute, and see the console output generated by the `ExampleWorker` and `AnotherExampleWorker`. -> 📖 Learn more about **[Nano Console Workers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Console#console-workers)**. +> 📖 Learn more about **[Nano Console Workers](https://github.com/Nano-Core/Nano.Library/tree/master/Nano.App.Console/README.md#console-workers)**. diff --git a/Console._Blank/.github/workflows/build-and-deploy.yml b/Console._Blank/.github/workflows/build-and-deploy.yml index 30741d0c..4a3c8b1d 100644 --- a/Console._Blank/.github/workflows/build-and-deploy.yml +++ b/Console._Blank/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Console.Blank IMAGE_NAME: console.blank @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/Nano-Core/Nano.Template.Console - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_HISTORY_COUNT: 4 KUBERNETES_MEMORY_REQUEST: 256Mi KUBERNETES_MEMORY_LIMIT: 768Mi @@ -33,23 +36,28 @@ env: DOTNET_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -78,7 +86,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -94,9 +102,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -106,14 +114,14 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/cronjob.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/cronjob.tmp.yaml; - sudo kubectl apply -f .kubernetes/cronjob.tmp.yaml; + kubectl apply -f .kubernetes/cronjob.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Console._Blank/.kubernetes/cronjob.yaml b/Console._Blank/.kubernetes/cronjob.yaml index 1590c0e5..71ff564c 100644 --- a/Console._Blank/.kubernetes/cronjob.yaml +++ b/Console._Blank/.kubernetes/cronjob.yaml @@ -18,6 +18,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux diff --git a/Nano.Lessons.sln.cmd b/Nano.Lessons.sln.cmd new file mode 100644 index 00000000..0c8adc5e --- /dev/null +++ b/Nano.Lessons.sln.cmd @@ -0,0 +1,2 @@ +@echo off +start "" devenv.exe "%~dp0" \ No newline at end of file diff --git a/Web._Blank/.github/workflows/build-and-deploy.yml b/Web._Blank/.github/workflows/build-and-deploy.yml index a9e645aa..ec5d0318 100644 --- a/Web._Blank/.github/workflows/build-and-deploy.yml +++ b/Web._Blank/.github/workflows/build-and-deploy.yml @@ -1,6 +1,11 @@ name: Build And Deploy on: - push + pull_request: + branches: + - master + push: + branches: + - master env: APP_NAME: Web.Blank IMAGE_NAME: web.blank @@ -8,22 +13,20 @@ env: VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' DOTNET_SDK_VERSION: 10.0 DOTNET_ASPNET_VERSION: 10.0 - AZURE_GROUP: ${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }} + AZURE_GROUP_KUBERNETES: ${{ vars.AZURE_RESOURCE_GROUP_KUBERNETES }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AZURE_SUBSCRIPTION_ID || secrets.STAGING_AZURE_SUBSCRIPTION_ID }} - NUGET_HOST: ${{ secrets.NUGET_HOST }} - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_APIKEY }} - NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} - CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} - CONTAINER_REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} - CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + NUGET_HOST: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + CONTAINER_REGISTRY_HOST: ghcr.io + CONTAINER_REGISTRY_USERNAME: ${{ github.actor }} + CONTAINER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} CONTAINER_REGISTRY_SOURCE_LABEL: https://github.com/${{ github.repository }} - KUBERNETES_CLUSTER: ${{ github.ref == 'refs/heads/master' && vars.PRODUCTION_KUBERNETES_CLUSTER || vars.STAGING_KUBERNETES_CLUSTER }} KUBERNETES_NODEPOOL_COMPUTE: cpu - KUBERNETES_NAMESPACE: default + KUBERNETES_NAMESPACE: apps KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 3 || 2 }} KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 8 || 5 }} KUBERNETES_REPLICA_HISTORY_COUNT: 0 @@ -36,23 +39,28 @@ env: ASPNETCORE_ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} jobs: build-and-deploy: - runs-on: ubuntu-latest + runs-on: + - self-hosted + - linux + - ${{ github.ref == 'refs/heads/master' && 'Production' || 'Staging' }} permissions: - contents: read + contents: write packages: write id-token: write concurrency: group: ${{ github.repository }} cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Azure Login shell: pwsh run: | - sudo az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; - sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; - sudo az aks get-credentials -g $env:AZURE_GROUP -n $env:KUBERNETES_CLUSTER --overwrite -o none; + az login --service-principal -u $env:AZURE_CLIENT_ID -p $env:AZURE_CLIENT_SECRET --tenant $env:AZURE_TENANT_ID -o none; + az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + + $env:KUBERNETES_CLUSTER = az aks list -g $env:AZURE_GROUP_KUBERNETES --query [0].name -o tsv; + az aks get-credentials -g $env:AZURE_GROUP_KUBERNETES -n $env:KUBERNETES_CLUSTER --overwrite -o none; - name: Build shell: pwsh @@ -81,7 +89,7 @@ jobs: $imageLatestTag = $registryHost + "/" + $env:IMAGE_NAME + ":latest"; $imageVersionTag = $registryHost + "/" + $env:IMAGE_NAME + ":" + $env:VERSION - sudo docker build ` + docker build ` -t $imageLatestTag ` -t $imageVersionTag ` --build-arg DOTNET_SDK_VERSION=$env:DOTNET_SDK_VERSION ` @@ -97,9 +105,9 @@ jobs: throw "error"; }; - sudo docker login -u="$env:CONTAINER_REGISTRY_USERNAME" -p="$env:CONTAINER_REGISTRY_PASSWORD" $env:CONTAINER_REGISTRY_HOST; - sudo docker push $imageLatestTag; - sudo docker push $imageVersionTag; + echo $env:CONTAINER_REGISTRY_PASSWORD | docker login $env:CONTAINER_REGISTRY_HOST -u $env:CONTAINER_REGISTRY_USERNAME --password-stdin; + docker push $imageLatestTag; + docker push $imageVersionTag; if ($LastExitCode -ne 0) { throw "error"; @@ -110,7 +118,7 @@ jobs: run: | $nugetProjectModels=$env:APP_NAME + ".Models/" + $env:APP_NAME + ".Models.csproj"; dotnet pack $nugetProjectModels -c Release --output nupkgs /p:PackageVersion=$env:VERSION --include-symbols --no-build; - dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_APIKEY; + dotnet nuget push nupkgs/$env:APP_NAME".Models."$env:VERSION.nupkg -s $env:NUGET_HOST -k $env:NUGET_PASSWORD; if ($LastExitCode -ne 0) { throw "error"; @@ -120,28 +128,28 @@ jobs: shell: pwsh run: | Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; - sudo kubectl apply -f .kubernetes/service.tmp.yaml; + kubectl apply -f .kubernetes/service.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; - sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + kubectl apply -f .kubernetes/configmap.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; - sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + kubectl apply -f .kubernetes/deployment.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; }; Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; - sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + kubectl apply -f .kubernetes/autoscaler.tmp.yaml; if ($LastExitCode -ne 0) { throw "error"; diff --git a/Web._Blank/.kubernetes/deployment.yaml b/Web._Blank/.kubernetes/deployment.yaml index fdc5dfdf..02882c56 100644 --- a/Web._Blank/.kubernetes/deployment.yaml +++ b/Web._Blank/.kubernetes/deployment.yaml @@ -20,6 +20,7 @@ spec: securityContext: runAsUser: 1000 runAsGroup: 2000 + fsGroup: 2000 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname @@ -27,7 +28,6 @@ spec: labelSelector: matchLabels: app: %SERVICE_NAME% - automountServiceAccountToken: false nodeSelector: nodepool.compute: %KUBERNETES_NODEPOOL_COMPUTE% kubernetes.io/os: linux @@ -73,11 +73,6 @@ spec: periodSeconds: 5 initialDelaySeconds: 20 timeoutSeconds: 2 - volumeMounts: - - name: tmp - mountPath: /tmp - - name: %SERVICE_NAME%-volume - mountPath: /mnt/%STORAGE_SHARE_NAME% imagePullSecrets: - name: ghcr-pull-secret diff --git a/Web._Blank/Web.Blank/_Imports.razor b/Web._Blank/Web.Blank/_Imports.razor new file mode 100644 index 00000000..665e8283 --- /dev/null +++ b/Web._Blank/Web.Blank/_Imports.razor @@ -0,0 +1,2 @@ +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Routing \ No newline at end of file