diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..b4e87d709 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,336 @@ +## Source: https://raw.githubusercontent.com/dotnet/runtime/refs/heads/main/.dockerignore + +### VisualStudio ### + +# Tool Runtime Dir +**/.dotnet/ +**/.packages/ +**/.tools/ + +# User-specific files +**/*.suo +**/*.user +**/*.userosscache +**/*.sln.docstates + +# Build results +**/artifacts/ +**/.idea/ +**/[Dd]ebug/ +**/[Dd]ebugPublic/ +**/[Rr]elease/ +**/[Rr]eleases/ +**/bld/ +**/[Bb]in/ +**/[Oo]bj/ +**/msbuild.log +**/msbuild.err +**/msbuild.wrn +**/msbuild.binlog +**/.deps/ +**/.dirstamp +**/.libs/ +**/*.lo +**/*.o + +# Cross building rootfs +**/cross/rootfs/ +**/cross/android-rootfs/ + +# Visual Studio +**/.vs/ + +# Ionide +**/.ionide/ + +# MSTest test Results +**/[Tt]est[Rr]esult*/ +**/[Bb]uild[Ll]og.* + +#NUNIT +**/*.VisualState.xml +**/TestResult.xml + +# Build Results of an ATL Project +**/[Dd]ebugPS/ +**/[Rr]eleasePS/ +**/dlldata.c + +**/*_i.c +**/*_p.c +**/*.ilk +**/*.meta +**/*.obj +**/*.pch +**/*.pdb +!**/_.pdb +**/*.pgc +**/*.pgd +**/*.rsp +**/*.sbr +**/*.tlb +**/*.tli +**/*.tlh +**/*.tmp +**/*.tmp_proj +**/*.log +**/*.vspscc +**/*.vssscc +**/.builds +**/*.pidb +**/*.svclog +**/*.scc + +# Chutzpah Test files +**/_Chutzpah* + +# Visual C++ cache files +**/ipch/ +**/*.aps +**/*.ncb +**/*.opendb +**/*.opensdf +**/*.sdf +**/*.cachefile +**/*.VC.db + +# Visual Studio profiler +**/*.psess +**/*.vsp +**/*.vspx + +# TFS 2012 Local Workspace +**/$tf/ + +# Guidance Automation Toolkit +**/*.gpState + +# ReSharper is a .NET coding add-in +**/_ReSharper*/ +**/*.[Rr]e[Ss]harper +**/*.DotSettings.user + +# JustCode is a .NET coding addin-in +**/.JustCode + +# TeamCity is a build add-in +**/_TeamCity* + +# DotCover is a Code Coverage Tool +**/*.dotCover + +# NCrunch +**/_NCrunch_* +**/.*crunch*.local.xml + +# MightyMoose +**/*.mm.* +**/AutoTest.Net/ + +# Web workbench (sass) +**/.sass-cache/ + +# Installshield output folder +**/[Ee]xpress/ + +# DocProject is a documentation generator add-in +**/DocProject/buildhelp/ +**/DocProject/Help/*.HxT +**/DocProject/Help/*.HxC +**/DocProject/Help/*.hhc +**/DocProject/Help/*.hhk +**/DocProject/Help/*.hhp +**/DocProject/Help/Html2 +**/DocProject/Help/html + +# Publish Web Output +**/*.[Pp]ublish.xml +**/*.azurePubxml +**/*.pubxml +**/*.publishproj + +# NuGet Packages +**/*.nupkg +**/*.nuget.g.props +**/*.nuget.g.targets +**/*.nuget.cache +**/**/packages/* +**/project.lock.json +**/project.assets.json +**/*.nuget.dgspec.json + +# Windows Azure Build Output +**/csx/ +**/*.build.csdef + +# Windows Store app package directory +**/AppPackages/ + +# Others +**/*.Cache +**/ClientBin/ +**/[Ss]tyle[Cc]op.* +**/~$* +**/*.dbmdl +**/*.dbproj.schemaview +**/*.pfx +**/*.publishsettings +**/node_modules/ +**/*.metaproj +**/*.metaproj.tmp +**/bin.localpkg/ + +# RIA/Silverlight projects +**/Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +**/_UpgradeReport_Files/ +**/Backup*/ +**/UpgradeLog*.XML +**/UpgradeLog*.htm + +# SQL Server files +**/*.mdf +**/*.ldf + +# Business Intelligence projects +**/*.rdl.data +**/*.bim.layout +**/*.bim_*.settings + +# Microsoft Fakes +**/FakesAssemblies/ + +# C/C++ extension for Visual Studio Code +**/browse.VC.db +# Local settings folder for Visual Studio Code +**/**/.vscode/** +!**/**/.vscode/c_cpp_properties.json + +### MonoDevelop ### + +**/*.pidb +**/*.userprefs + +### Windows ### + +# Windows image file caches +**/Thumbs.db +**/ehthumbs.db + +# Folder config file +**/Desktop.ini + +# Recycle Bin used on file shares +**/$RECYCLE.BIN/ + +# Windows Installer files +**/*.cab +**/*.msi +**/*.msm +**/*.msp + +# Windows shortcuts +**/*.lnk + +### Linux ### + +**/*~ + +# KDE directory preferences +**/.directory + +### OSX ### + +**/.DS_Store +**/.AppleDouble +**/.LSOverride + +# Icon must end with two \r +**/Icon + +# Thumbnails +**/._* + +# Files that might appear on external disk +**/.Spotlight-V100 +**/.Trashes + +# Directories potentially created on remote AFP share +**/.AppleDB +**/.AppleDesktop +**/Network Trash Folder +**/Temporary Items +**/.apdisk + +# vim temporary files +**/[._]*.s[a-w][a-z] +**/[._]s[a-w][a-z] +**/*.un~ +**/Session.vim +**/.netrwhist +**/*~ + +# Visual Studio Code +**/.vscode/ + +# Private test configuration and binaries. +**/config.ps1 +**/**/IISApplications + +# VS debug support files +**/launchSettings.json + +# Snapcraft files +**/.snapcraft +**/*.snap +**/parts/ +**/prime/ +**/stage/ + +# CLR prebuilt generated files +!**/src/pal/prebuilt/idl/*_i.c + +# Valid 'debug' folder, that contains CLR debugging code +!**/src/**/debug + +# Ignore folders created by the CLR test build +**/TestWrappers_x64_[d|D]ebug +**/TestWrappers_x64_[c|C]hecked +**/TestWrappers_x64_[r|R]elease +**/TestWrappers_x86_[d|D]ebug +**/TestWrappers_x86_[c|C]hecked +**/TestWrappers_x86_[r|R]elease +**/TestWrappers_arm_[d|D]ebug +**/TestWrappers_arm_[c|C]hecked +**/TestWrappers_arm_[r|R]elease +**/TestWrappers_arm64_[d|D]ebug +**/TestWrappers_arm64_[c|C]hecked +**/TestWrappers_arm64_[r|R]elease +**/tests/src/common/test_runtime/project.json + +**/Vagrantfile +**/.vagrant + +# CMake files +**/CMakeFiles/ +**/cmake_install.cmake +**/CMakeCache.txt +**/Makefile + +# Cross compilation +**/cross/rootfs/* +**/cross/android-rootfs/* +# add x86 as it is ignored in 'Build results' +!**/cross/x86 + +#python import files +**/*.pyc + +# JIT32 files +**/src/jit32 + +# performance testing sandbox +**/sandbox diff --git a/.github/workflows/build-and-push-image.yml b/.github/workflows/build-and-push-image.yml index 860dbbc88..40ce791ad 100644 --- a/.github/workflows/build-and-push-image.yml +++ b/.github/workflows/build-and-push-image.yml @@ -59,11 +59,21 @@ jobs: "Dockerfile", "Dockerfile.PersonsApi" ] + stage: [ + "final" + ] include: - image: "Dockerfile" aca_name: "ACA_CONTAINERAPP_NAME" prefix: "" name: "tramsapi-app" + + - image: "Dockerfile" + aca_name: "ACA_CONTAINERAPP_NAME" + prefix: "init-" + name: "tramsapi-app" + stage: initcontainer + - image: "Dockerfile.PersonsApi" aca_name: "ACA_CONTAINERAPP_PERSONS_API_NAME" prefix: "persons-api-" @@ -71,10 +81,12 @@ jobs: with: docker-image-name: '${{ matrix.name }}' docker-build-file-name: './${{ matrix.image }}' + docker-build-target: ${{ matrix.stage }} docker-tag-prefix: ${{ matrix.prefix }} environment: ${{ needs.set-env.outputs.environment }} # Only annotate the release once, because both apps are deployed at the same time - annotate-release: ${{ matrix.name == 'tramsapi-app' }} + annotate-release: ${{ matrix.name == 'tramsapi-app' && matrix.stage == 'final' }} + import-without-deploy: ${{ matrix.stage == 'initcontainer' }} docker-build-args: | COMMIT_SHA="${{ needs.set-env.outputs.checked-out-sha }}" CI=true diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 6773cff2d..a70804bdd 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -16,6 +16,12 @@ jobs: "Dockerfile", "Dockerfile.PersonsApi" ] + stage: [ + "final" + ] + include: + - image: "Dockerfile" + stage: "initcontainer" steps: - name: Checkout code uses: actions/checkout@v4 @@ -31,4 +37,5 @@ jobs: secrets: github_token=${{ secrets.GITHUB_TOKEN }} cache-from: type=gha cache-to: type=gha + target: ${{ matrix.stage }} push: false diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index 1345fac2e..87a1247c3 100644 --- a/.github/workflows/docker-test.yml +++ b/.github/workflows/docker-test.yml @@ -15,6 +15,12 @@ jobs: "Dockerfile", "Dockerfile.PersonsApi" ] + stage: [ + "final" + ] + include: + - image: "Dockerfile" + stage: "initcontainer" steps: - name: Checkout code uses: actions/checkout@v4 @@ -29,18 +35,19 @@ jobs: file: './${{ matrix.image }}' build-args: CI=true secrets: github_token=${{ secrets.GITHUB_TOKEN }} + target: ${{ matrix.stage }} load: true cache-from: type=gha cache-to: type=gha push: false - name: Export docker image as tar - run: docker save -o ${{ matrix.image }}.tar ${{ steps.build.outputs.imageid }} + run: docker save -o ${{ matrix.image }}-${{ matrix.stage }}.tar ${{ steps.build.outputs.imageid }} - name: Scan Docker image for CVEs uses: aquasecurity/trivy-action@0.29.0 with: - input: ${{ matrix.image }}.tar + input: ${{ matrix.image }}-${{ matrix.stage }}.tar format: 'sarif' output: 'trivy-results.sarif' limit-severities-for-sarif: true diff --git a/Dockerfile b/Dockerfile index ce8720e62..011eb0345 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,47 +1,74 @@ # Set the major version of dotnet ARG DOTNET_VERSION=8.0 -# Build the app using the dotnet SDK -FROM "mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-azurelinux3.0" AS build +# ============================================== +# .NET: SDK Builder +# ============================================== +FROM "mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-azurelinux3.0" AS builder WORKDIR /build +RUN ["tdnf", "update", "--security", "-y"] +RUN ["tdnf", "clean", "all"] ARG CI ENV CI=${CI} - -COPY . . COPY ./script/web-docker-entrypoint.sh /app/docker-entrypoint.sh +## START: Restore Packages +ARG PROJECT_NAME="TramsDataApi" +COPY ./${PROJECT_NAME}.sln ./ +COPY ./${PROJECT_NAME}/${PROJECT_NAME}.csproj ./${PROJECT_NAME}/ + +ARG PROJECT_NAME="Dfe.Academies" +COPY ./${PROJECT_NAME}.Api.Infrastructure/${PROJECT_NAME}.Infrastructure.csproj ./${PROJECT_NAME}.Api.Infrastructure/ +COPY ./${PROJECT_NAME}.Application/${PROJECT_NAME}.Application.csproj ./${PROJECT_NAME}.Application/ +COPY ./${PROJECT_NAME}.Domain/${PROJECT_NAME}.Domain.csproj ./${PROJECT_NAME}.Domain/ +COPY ./${PROJECT_NAME}.Utils/${PROJECT_NAME}.Utils.csproj ./${PROJECT_NAME}.Utils/ + # Mount GitHub Token as a Docker secret so that NuGet Feed can be accessed RUN --mount=type=secret,id=github_token dotnet nuget add source --username USERNAME --password $(cat /run/secrets/github_token) --store-password-in-clear-text --name github "https://nuget.pkg.github.com/DFE-Digital/index.json" +RUN ["dotnet", "restore", "TramsDataApi"] +## END: Restore Packages -# Generate the Entity Framework migration scripts -RUN ["dotnet", "new", "tool-manifest"] -RUN ["dotnet", "tool", "install", "dotnet-ef", "--version", "8.0.11"] -RUN ["mkdir", "-p", "/app/SQL"] -RUN ["touch", "/app/SQL/DbMigrationScriptOutput.txt"] -RUN ["touch", "/app/SQL/DbMigrationScriptOutputLegacy.txt"] -RUN ["dotnet", "restore", "TramsDataApi.sln"] -RUN ["dotnet", "ef", "migrations", "script", "--output", "/app/SQL/DbMigrationScriptLegacy.sql", "--project", "TramsDataApi", "--context", "TramsDataApi.DatabaseModels.LegacyTramsDbContext", "--idempotent"] -RUN ["dotnet", "ef", "migrations", "script", "--output", "/app/SQL/DbMigrationScript.sql", "--project", "TramsDataApi", "--context", "TramsDataApi.DatabaseModels.TramsDbContext", "--idempotent"] - -# Build and publish the dotnet solution -RUN dotnet build TramsDataApi.sln --no-restore -c Release -p CI=${CI} -RUN ["dotnet", "publish", "TramsDataApi", "--no-build", "-o", "/app"] - -# Install SQL tools to allow migrations to be run -FROM "mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}-azurelinux3.0" AS base -RUN curl "https://packages.microsoft.com/config/rhel/9/prod.repo" | tee /etc/yum.repos.d/mssql-release.repo -ENV ACCEPT_EULA=Y -RUN ["tdnf", "update", "--security", "-y"] -RUN ["tdnf", "install", "-y", "mssql-tools18"] -RUN ["tdnf", "clean", "all"] +ARG PROJECT_NAME="TramsDataApi" +COPY ./${PROJECT_NAME}/ ./${PROJECT_NAME}/ + +ARG PROJECT_NAME="Dfe.Academies" +COPY ./${PROJECT_NAME}.Api.Infrastructure/ ./${PROJECT_NAME}.Api.Infrastructure/ +COPY ./${PROJECT_NAME}.Application/ ./${PROJECT_NAME}.Application/ +COPY ./${PROJECT_NAME}.Domain/ ./${PROJECT_NAME}.Domain/ +COPY ./${PROJECT_NAME}.Utils/ ./${PROJECT_NAME}.Utils/ + +RUN ["dotnet", "publish", "TramsDataApi", "-c", "Release", "-o", "/app", "/p:CI=${CI}", "--no-restore"] + +# ============================================== +# Entity Framework: Migration Builder +# ============================================== +FROM builder AS efbuilder +WORKDIR /build +ENV PATH=$PATH:/root/.dotnet/tools +RUN ["mkdir", "/sql"] +RUN ["dotnet", "tool", "install", "--global", "dotnet-ef"] +RUN ["dotnet", "ef", "migrations", "bundle", "-r", "linux-x64", "--configuration", "Release", "-p", "TramsDataApi", "--context", "TramsDataApi.DatabaseModels.LegacyTramsDbContext", "--no-build", "-o", "/sql/migratelegacydb"] +RUN ["dotnet", "ef", "migrations", "bundle", "-r", "linux-x64", "--configuration", "Release", "-p", "TramsDataApi", "--context", "TramsDataApi.DatabaseModels.TramsDbContext", "--no-build", "-o", "/sql/migratedb"] + +# ============================================== +# Entity Framework: Migration Runner +# ============================================== +FROM "mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}-azurelinux3.0" AS initcontainer +WORKDIR /sql +COPY --from=efbuilder /sql /sql +COPY --from=builder /app/appsettings* /TramsDataApi/ +RUN chown "$APP_UID" "/sql" -R +RUN chown "$APP_UID" "/TramsDataApi" -R +USER $APP_UID -# Build a runtime environment -FROM base AS final +# ============================================== +# .NET: Runtime +# ============================================== +FROM "mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}-azurelinux3.0" AS final WORKDIR /app LABEL org.opencontainers.image.source="https://github.com/DFE-Digital/academies-api" LABEL org.opencontainers.image.description="Academies API" -COPY --from=build /app /app +COPY --from=builder /app /app RUN ["chmod", "+x", "./docker-entrypoint.sh"] -RUN chown "$APP_UID" "/app/SQL" -R USER $APP_UID diff --git a/script/web-docker-entrypoint.sh b/script/web-docker-entrypoint.sh index 10760793b..57792dacf 100755 --- a/script/web-docker-entrypoint.sh +++ b/script/web-docker-entrypoint.sh @@ -4,30 +4,4 @@ set -e set -o pipefail -ConnectionStrings__DefaultConnection=${ConnectionStrings__DefaultConnection:?} - -declare -A mysqlconn - -for keyvaluepair in $(echo "$ConnectionStrings__DefaultConnection" | sed "s/ //g; s/;/ /g") -do - IFS=" " read -r -a ARR <<< "${keyvaluepair//=/ }" - mysqlconn[${ARR[0]}]=${ARR[1]} -done - -echo "Running TramsDbContext database migrations ..." -until /opt/mssql-tools18/bin/sqlcmd -S "${mysqlconn[Server]}" -U "${mysqlconn[UserId]}" -P "${mysqlconn[Password]}" -d "${mysqlconn[Database]}" -C -i /app/SQL/DbMigrationScript.sql -o /app/SQL/DbMigrationScriptOutput.txt -do - cat /app/SQL/DbMigrationScriptOutput.txt - echo "Retrying database migrations ..." - sleep 5 -done - -echo "Running LegacyTramsDbContext database migrations ..." -until /opt/mssql-tools18/bin/sqlcmd -S "${mysqlconn[Server]}" -U "${mysqlconn[UserId]}" -P "${mysqlconn[Password]}" -d "${mysqlconn[Database]}" -C -i /app/SQL/DbMigrationScriptLegacy.sql -o /app/SQL/DbMigrationScriptOutputLegacy.txt -do - cat /app/SQL/DbMigrationScriptOutputLegacy.txt - echo "Retrying database migrations ..." - sleep 5 -done - exec "$@" diff --git a/terraform/README.md b/terraform/README.md index 907d55b5e..1129ef8f9 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -184,6 +184,7 @@ No resources. | [enable\_dns\_zone](#input\_enable\_dns\_zone) | Conditionally create a DNS zone | `bool` | n/a | yes | | [enable\_event\_hub](#input\_enable\_event\_hub) | Send Azure Container App logs to an Event Hub sink | `bool` | `false` | no | | [enable\_health\_insights\_api](#input\_enable\_health\_insights\_api) | Deploys a Function App that exposes the last 3 HTTP Web Tests via an API endpoint. 'enable\_app\_insights\_integration' and 'enable\_monitoring' must be set to 'true'. | `bool` | `false` | no | +| [enable\_init\_container](#input\_enable\_init\_container) | Deploy an Init Container. Init containers run before the primary app container and are used to perform initialization tasks such as downloading data or preparing the environment | `bool` | `false` | no | | [enable\_logstash\_consumer](#input\_enable\_logstash\_consumer) | Create an Event Hub consumer group for Logstash | `bool` | `false` | no | | [enable\_monitoring](#input\_enable\_monitoring) | Create an App Insights instance and notification group for the Container App | `bool` | n/a | yes | | [environment](#input\_environment) | Environment name. Will be used along with `project_name` as a prefix for all resources. | `string` | n/a | yes | @@ -194,6 +195,8 @@ No resources. | [health\_insights\_api\_cors\_origins](#input\_health\_insights\_api\_cors\_origins) | List of hostnames that are permitted to contact the Health insights API | `list(string)` |
[
"*"
]
| no | | [health\_insights\_api\_ipv4\_allow\_list](#input\_health\_insights\_api\_ipv4\_allow\_list) | List of IPv4 addresses that are permitted to contact the Health insights API | `list(string)` | `[]` | no | | [image\_name](#input\_image\_name) | Image name | `string` | n/a | yes | +| [init\_container\_command](#input\_init\_container\_command) | Container command for the Init Container | `list(any)` | `[]` | no | +| [init\_container\_image](#input\_init\_container\_image) | Image name for the Init Container. Leave blank to use the same Container image from the primary app | `string` | `""` | no | | [key\_vault\_access\_ipv4](#input\_key\_vault\_access\_ipv4) | List of IPv4 Addresses that are permitted to access the Key Vault | `list(string)` | n/a | yes | | [monitor\_email\_receivers](#input\_monitor\_email\_receivers) | A list of email addresses that should be notified by monitoring alerts | `list(string)` | n/a | yes | | [monitor\_endpoint\_healthcheck](#input\_monitor\_endpoint\_healthcheck) | Specify a route that should be monitored for a 200 OK status | `string` | n/a | yes | diff --git a/terraform/container-apps-hosting.tf b/terraform/container-apps-hosting.tf index a51f4a3a1..0d2dbffa5 100644 --- a/terraform/container-apps-hosting.tf +++ b/terraform/container-apps-hosting.tf @@ -59,5 +59,8 @@ module "azure_container_apps_hosting" { existing_network_watcher_name = local.existing_network_watcher_name existing_network_watcher_resource_group_name = local.existing_network_watcher_resource_group_name - custom_container_apps = local.custom_container_apps + custom_container_apps = local.custom_container_apps + enable_init_container = local.enable_init_container + init_container_image = local.init_container_image + init_container_command = local.init_container_command } diff --git a/terraform/locals.tf b/terraform/locals.tf index 41c8779d8..c85ea2a24 100644 --- a/terraform/locals.tf +++ b/terraform/locals.tf @@ -55,4 +55,7 @@ locals { health_insights_api_ipv4_allow_list = var.health_insights_api_ipv4_allow_list enable_cdn_frontdoor_vdp_redirects = var.enable_cdn_frontdoor_vdp_redirects cdn_frontdoor_vdp_destination_hostname = var.cdn_frontdoor_vdp_destination_hostname + enable_init_container = var.enable_init_container + init_container_image = var.init_container_image + init_container_command = var.init_container_command } diff --git a/terraform/variables.tf b/terraform/variables.tf index 304afaf9e..f724e66e8 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -424,3 +424,21 @@ variable "container_port" { type = number default = 8080 } + +variable "enable_init_container" { + description = "Deploy an Init Container. Init containers run before the primary app container and are used to perform initialization tasks such as downloading data or preparing the environment" + type = bool + default = false +} + +variable "init_container_image" { + description = "Image name for the Init Container. Leave blank to use the same Container image from the primary app" + type = string + default = "" +} + +variable "init_container_command" { + description = "Container command for the Init Container" + type = list(any) + default = [] +}