From 1d23de2ef99d47e9b3f9456392352e209b8f4d73 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Mon, 5 Feb 2024 15:06:45 +0100 Subject: [PATCH] Bugfix make podman work on fedora, apple silicone and windows OS (#74) * fix podman compose script with podman machine running with applehv due to bugs with qemu and m3 mac * update all volume definitions so they match * remove bind.selinux as it does not seem to have any affect * Add updated readme for podman and add a hack for resolving the bind issues on mac * update readme * adde one line to readme.md * test new notation for information and warnings * test more with info sections * try to fix indend * Restructure the readme * update readme * some small changes to the README.md * use host.docker.internal as it seems to work on both windows and mac * change nginx internal domain to host.containers.internal * Differentiate on host and container domains for podman * bind problems also seems to be present on fedora. Renaming hack method in Makefile * update readme with known issue about bind mounts * fix bug in makefile * Make local frontend port configurable * Podman defaults localtest to 8000 Detect running local dev servers of app-frontend-react instead of static value * Update readme with guide to add hyper-v firewall rule if needed * big Z instead of small z * Update known issue in readme --------- Co-authored-by: tjololo <1145298+tjololo@users.noreply.github.com> --- .env.template | 3 +- Makefile | 18 +- README.md | 172 +++++++++++------- docker-compose.yml | 1 + loadbalancer/templates/nginx.conf.conf | 2 +- podman-compose.yml | 33 +++- scripts/OpenAppPortInHyperVFirewall.ps1 | 7 + src/Configuration/LocalPlatformSettings.cs | 4 + src/Controllers/FrontendVersionController.cs | 25 +-- src/Models/LocalFrontendInfo.cs | 7 + .../Implementation/LocalFrontendService.cs | 47 +++++ .../Interface/ILocalFrontendService.cs | 8 + src/Startup.cs | 4 + src/appsettings.Docker.json | 1 + src/appsettings.Podman.json | 3 +- src/appsettings.json | 1 + 16 files changed, 244 insertions(+), 92 deletions(-) create mode 100644 scripts/OpenAppPortInHyperVFirewall.ps1 create mode 100644 src/Models/LocalFrontendInfo.cs create mode 100644 src/Services/LocalFrontend/Implementation/LocalFrontendService.cs create mode 100644 src/Services/LocalFrontend/Interface/ILocalFrontendService.cs diff --git a/.env.template b/.env.template index f1f7a00b..511c4bbd 100644 --- a/.env.template +++ b/.env.template @@ -4,7 +4,8 @@ ## What port should the loadbalancer use? ## sometimes PCs have another service running on port 80, so you might need to -## change this. Default in podman-compose.yml is 8080 as ports below 1024 are privileged ports on most unix systems. +## change this. +## Default in podman-compose.yml is 8000 as ports below 1024 are privileged ports on most unix systems. #ALTINN3LOCAL_PORT=80 diff --git a/Makefile b/Makefile index a8628963..5e3ce2c3 100644 --- a/Makefile +++ b/Makefile @@ -12,4 +12,20 @@ podman-start-localtest: .PHONY: podman-stop-localtest podman-stop-localtest: - podman compose --file podman-compose.yml down \ No newline at end of file + podman compose --file podman-compose.yml down + +.PHONY: podman-compose-start-localtest +podman-compose-start-localtest: + podman-compose --file podman-compose.yml up -d --build + +.PHONY: podman-compose-stop-localtest +podman-compose-stop-localtest: + podman-compose --file podman-compose.yml down + + +.PHONY: podman-selinux-bind-hack +podman-selinux-bind-hack: + @echo "Running best effort commands to make bind mounts work on Apple Silicon and Linux with podman. Dirty hack until actual issue is located and fixed." + podman container run -v ./testdata/:/testdata/:Z --rm -it --entrypoint cat nginx:alpine-perl /testdata/authorization/claims/1337.json > /dev/null + podman container run -v ./loadbalancer/templates/:/testdata/:Z --rm -it --entrypoint cat nginx:alpine-perl /testdata/nginx.conf.conf > /dev/null + podman container run -v ./loadbalancer/www/:/testdata/:Z --rm -it --entrypoint cat nginx:alpine-perl /testdata/502App.html > /dev/null \ No newline at end of file diff --git a/README.md b/README.md index d470bb26..7e122261 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,14 @@ These are some of the required steps, tips, and tricks when it comes to running - [Prerequisites](#prerequisites) - [Setup](#setup) + - [Clone the repository](#clone-the-repository) + - [Option A: Start the containers using podman](#option-a-start-the-containers-using-podman) + - [Option B: Start the containers using Docker](#option-b-start-the-containers-using-docker) + - [Start your app](#start-your-app) +- [Changing configuration](#changing-configuration) +- [Multiple apps at the same time (running LocalTest locally)](#multiple-apps-at-the-same-time-running-localtest-locally) - [Changing test data](#changing-test-data) +- [Known issues](#known-issues) ### Prerequisites @@ -14,89 +21,94 @@ These are some of the required steps, tips, and tricks when it comes to running - Also install [recommended extensions](https://code.visualstudio.com/docs/editor/extension-gallery#_workspace-recommended-extensions) ( f.ex. [C#](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp)) -4. [Docker Desktop](https://www.docker.com/products/docker-desktop) (Linux users can also use native Docker) - +4. [Podman Desktop](https://podman-desktop.io)/[Podman](https://podman.io) (Linux users can also use native Docker) or [Docker Desktop](https://www.docker.com/products/docker-desktop) (Windows and Mac) This might require you to purchase a license. +On mac with apple silicone (M1, M2, M3): +5. [vfkit](https://github.com/crc-org/vfkit?tab=readme-ov-file#installation) ### Setup -#### Using docker - -1. Clone the `app-localtest` repository to a local folder and move into the folder. - - ```shell - git clone https://github.com/Altinn/app-localtest - cd app-localtest - ``` - -2. Build and run the containers in the background. - - ```shell - docker compose up -d --build - ``` - - :information_source: If you are using linux or mac you can use the Makefile to build and run the containers. - - ```shell - make docker-start-localtest - ``` - - This mode supports running one app at a time. If you need to run multiple apps at once, stop the localtest container with `docker stop localtest` and follow the instructions below to run LocalTest locally outside Docker. +#### Clone the repository -3. Start your app - _This step requires that you have already [created an app](https://docs.altinn.studio/app/getting-started/create-app/), added a [data model](https://docs.altinn.studio/app/development/data/data-model/data-models-tool/), and [cloned the app](https://docs.altinn.studio/app/getting-started/local-dev/) to your local environment._ - - Move into the `App` folder of your application. +```shell +git clone https://github.com/Altinn/app-localtest +cd app-localtest + ``` - Example: If your application is named `my-awesome-app` and is located in the folder `C:\my_applications`, run the following command: +#### Option A: Start the containers using podman - ```shell - cd C:\my_applications\my-awasome-app\App - ``` +This mode supports running one app at a time. If you need to run multiple apps at once, stop the localtest container with `podman stop localtest` and follow the instructions below to run LocalTest locally outside Docker/Podman. - Run the application: +> [!IMPORTANT] +> If you are using an mac with either a M1, M2 or M3 chip you may need to use `applehv` instead of `qemu` as the podman machine driver. +> This can be done by setting the environment variable `CONTAINERS_MACHINE_PROVIDER` to `applehv` before running the command below. +> To add this to your zsh profile run the following command: `echo "export CONTAINERS_MACHINE_PROVIDER=applehv" >> ~/.zprofile` +> If you are using Podman Desktop you also need to add these lines in `~/.config/containers/containers.conf` (check if the `[machine]` section already exists): +> +> ``` +> [machine] +> provider = "applehv" +> ``` - ```shell - dotnet run - ``` +Start the containers with the following command: -#### Using podman - -1. Clone the `app-localtest` repository to a local folder and move into the folder. - - ```shell - git clone https://github.com/Altinn/app-localtest - cd app-localtest - ``` - -2. Build and run the containers in the background. - - ```shell - podman compose --file podman-compose.yml up -d --build - ``` - - :information_source: If you are using linux or mac you can use the Makefile to build and run the containers. +```shell +podman compose --file podman-compose.yml up -d --build +``` - ```shell - make podman-start-localtest - ``` +> [!NOTE] +> If you are using linux or mac you can use the Makefile to build and run the containers. +> +> ```shell +> make podman-start-localtest +> ``` + +> [!IMPORTANT] +> Are you running podman version < 4.7.0 you need to use the following command instead: +> +> ```shell +> podman-compose --file podman-compose.yml up -d --build +> ``` +> +> or the make command: +> +> ```shell +> make podman-compose-start-localtest +> ``` + +Localtest should now be runningn on port 8000 and can be accessed on . + +#### Option B: Start the containers using Docker + +This mode supports running one app at a time. If you need to run multiple apps at once, stop the localtest container with `docker stop localtest` and follow the instructions below to run LocalTest locally outside Docker. + +```shell +docker compose up -d --build +``` + +> [!NOTE] +> If you are using linux or mac you can use the Makefile to build and run the containers. +> +> ```shell +> make docker-start-localtest +> ``` - This mode supports running one app at a time. If you need to run multiple apps at once, stop the localtest container with `podman stop localtest` and follow the instructions below to run LocalTest locally outside Docker. +Localtest should now be runningn on port 80 and can be accessed on . -3. Start your app - _This step requires that you have already [created an app](https://docs.altinn.studio/app/getting-started/create-app/), added a [data model](https://docs.altinn.studio/app/development/data/data-model/data-models-tool/), and [cloned the app](https://docs.altinn.studio/app/getting-started/local-dev/) to your local environment._ +#### Start your app +_This step requires that you have already [created an app](https://docs.altinn.studio/app/getting-started/create-app/), added a [data model](https://docs.altinn.studio/app/development/data/data-model/data-models-tool/), and [cloned the app](https://docs.altinn.studio/app/getting-started/local-dev/) to your local environment._ - Move into the `App` folder of your application. +Move into the `App` folder of your application. - Example: If your application is named `my-awesome-app` and is located in the folder `C:\my_applications`, run the following command: +Example: If your application is named `my-awesome-app` and is located in the folder `C:\my_applications`, run the following command: - ```shell - cd C:\my_applications\my-awasome-app\App - ``` +```shell +cd C:\my_applications\my-awasome-app\App +``` - Run the application: +Run the application: - ```shell - dotnet run - ``` + ```shell + dotnet run +``` The app and local platform services are now running locally. The app can be accessed on . @@ -203,3 +215,29 @@ This would be required if your app requires a role which none of the test users 4. Save and close the file 5. Restart LocalTest + +### Known issues + +#### Bind mounts folders gives permission denied. Nginx returns default page + +On some nix systems you might experience problems with the bind mounts used by the containers. If you get the default nginx page when trying to access local.altinn.cloud this might be the case. + +To verify this you can run the following command: + +```shell +podman container exec -it localtest-loadbalancer cat /etc/nginx/templates/nginx.conf.conf +``` + +if you get a permission denied message this verifies that the bind mount is not working. A best effort fix for this is to run the following command: + +```shell +make podman-selinux-bind-hack +``` + +#### Localtest reports that the app is not running even though it is + +If localtest and you app is running, but localtest reports that the app is not running, it might be that the port is not open in the firewall. + +You can verify if the app is running by opening `http://localhost:5005///swagger/index.html` (remember to replace `` and `` with the correct values). + +If this is the case you can open a Windows Powershell as administrator and run the script `OpenAppPortInHyperVFirewall.ps1` located in the `scripts` folder. diff --git a/docker-compose.yml b/docker-compose.yml index 9f985728..831fa6c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,7 @@ services: - NGINX_HOST=localhost - NGINX_PORT=80 - TEST_DOMAIN=${TEST_DOMAIN:-local.altinn.cloud} + - HOST_DOMAIN=host.docker.internal - INTERNAL_DOMAIN=host.docker.internal - ALTINN3LOCAL_PORT=${ALTINN3LOCAL_PORT:-80} - NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx/ diff --git a/loadbalancer/templates/nginx.conf.conf b/loadbalancer/templates/nginx.conf.conf index e4913d5a..828f7450 100644 --- a/loadbalancer/templates/nginx.conf.conf +++ b/loadbalancer/templates/nginx.conf.conf @@ -36,7 +36,7 @@ http { } upstream app { - server ${INTERNAL_DOMAIN}:5005; + server ${HOST_DOMAIN}:5005; } # Redirect localhost and the old altinn3local.no to the configured test domain server { diff --git a/podman-compose.yml b/podman-compose.yml index 74160d03..d81e4213 100644 --- a/podman-compose.yml +++ b/podman-compose.yml @@ -5,25 +5,34 @@ networks: external: false services: - local.altinn.cloud: + localtest_loadbalancer: container_name: localtest-loadbalancer image: nginx:alpine-perl restart: always networks: - - altinntestlocal_network + altinntestlocal_network: + aliases: + - ${TEST_DOMAIN:-local.altinn.cloud} ports: - - "${ALTINN3LOCAL_PORT:-8080}:80" + - "${ALTINN3LOCAL_PORT:-8000}:80" environment: - NGINX_HOST=localhost - NGINX_PORT=80 - TEST_DOMAIN=${TEST_DOMAIN:-local.altinn.cloud} + - HOST_DOMAIN=host.docker.internal - INTERNAL_DOMAIN=host.containers.internal - - ALTINN3LOCAL_PORT=${ALTINN3LOCAL_PORT:-80} + - ALTINN3LOCAL_PORT=${ALTINN3LOCAL_PORT:-8000} - NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx/ - NGINX_ENVSUBST_TEMPLATE_SUFFIX=.conf volumes: - - ./loadbalancer/templates:/etc/nginx/templates/:ro - - ./loadbalancer/www:/www/:ro + - type: bind + source: ./loadbalancer/templates/ + target: /etc/nginx/templates/ + read_only: true + - type: bind + source: ./loadbalancer/www/ + target: /www/ + read_only: true altinn_platform_pdf: @@ -57,11 +66,17 @@ services: environment: - DOTNET_ENVIRONMENT=Podman - ASPNETCORE_URLS=http://*:5101/ - - GeneralSettings__BaseUrl=http://${TEST_DOMAIN:-local.altinn.cloud}:${ALTINN3LOCAL_PORT:-80} + - GeneralSettings__BaseUrl=http://${TEST_DOMAIN:-local.altinn.cloud}:${ALTINN3LOCAL_PORT:-8000} - GeneralSettings__HostName=${TEST_DOMAIN:-local.altinn.cloud} volumes: - - ./testdata:/testdata - - ${ALTINN3LOCALSTORAGE_PATH:-AltinnPlatformLocal}:/AltinnPlatformLocal + - type: volume + source: AltinnPlatformLocal + target: /AltinnPlatformLocal/ + read_only: false + - type: bind + source: ./testdata/ + target: /testdata/ + read_only: true volumes: AltinnPlatformLocal: diff --git a/scripts/OpenAppPortInHyperVFirewall.ps1 b/scripts/OpenAppPortInHyperVFirewall.ps1 new file mode 100644 index 00000000..5ecd68b8 --- /dev/null +++ b/scripts/OpenAppPortInHyperVFirewall.ps1 @@ -0,0 +1,7 @@ +$AppPort = 5005 +$NetFirewallHyperVName = '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}' + +Get-NetFirewallHyperVVMCreator +Get-NetFirewallHyperVVMSetting -PolicyStore ActiveStore -Name $NetFirewallHyperVName +Get-NetFirewallHyperVRule -VMCreatorId $NetFirewallHyperVName +New-NetFirewallHyperVRule -Name Altinn3App -DisplayName "Altinn 3 Application" -Direction Inbound -VMCreatorId $NetFirewallHyperVName -Protocol TCP -LocalPorts $AppPort \ No newline at end of file diff --git a/src/Configuration/LocalPlatformSettings.cs b/src/Configuration/LocalPlatformSettings.cs index e7d540cc..f5606661 100644 --- a/src/Configuration/LocalPlatformSettings.cs +++ b/src/Configuration/LocalPlatformSettings.cs @@ -41,6 +41,10 @@ public string LocalTestingStaticTestDataPath } } + public string LocalFrontendHostname { get; set; } + + public string LocalFrontendProtocol { get; set; } = "http"; + /// /// Url for the local app when LocalAppMode == http /// diff --git a/src/Controllers/FrontendVersionController.cs b/src/Controllers/FrontendVersionController.cs index 74db638f..382ccf81 100644 --- a/src/Controllers/FrontendVersionController.cs +++ b/src/Controllers/FrontendVersionController.cs @@ -2,13 +2,8 @@ using System.Text.Json; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -using LocalTest.Configuration; using LocalTest.Models; -using LocalTest.Services.LocalApp.Interface; - -using LocalTest.Services.TestData; +using LocalTest.Services.LocalFrontend.Interface; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Mvc.Rendering; @@ -23,7 +18,7 @@ public class FrontendVersionController : Controller public static readonly string FRONTEND_URL_COOKIE_NAME = "frontendVersion"; [HttpGet] - public async Task Index([FromServices] HttpClient client) + public async Task Index([FromServices] HttpClient client, [FromServices] ILocalFrontendService localFrontendService) { var versionFromCookie = HttpContext.Request.Cookies[FRONTEND_URL_COOKIE_NAME]; @@ -36,14 +31,20 @@ public async Task Index([FromServices] HttpClient client) { Text = "Keep as is", Value = "", - }, - new () - { - Text = "localhost:8080 (local dev)", - Value = "http://localhost:8080/" } } }; + var groupLocalVersions = new SelectListGroup() { Name = "Frontend served from local dev server" }; + var localFrontendPorts = await localFrontendService.GetLocalFrontendDevPorts(); + foreach (var localPort in localFrontendPorts) + { + frontendVersion.Versions.Add(new SelectListItem() + { + Text = $"Local dev-server on port {localPort.Port} ({localPort.Branch} branch)", + Value = $"http://localhost:{localPort.Port}/", + Group = groupLocalVersions + }); + } var cdnVersionsString = await client.GetStringAsync("https://altinncdn.no/toolkits/altinn-app-frontend/index.json"); var groupCdnVersions = new SelectListGroup() { Name = "Specific version from cdn" }; var versions = JsonSerializer.Deserialize>(cdnVersionsString)!; diff --git a/src/Models/LocalFrontendInfo.cs b/src/Models/LocalFrontendInfo.cs new file mode 100644 index 00000000..e5a5cbb9 --- /dev/null +++ b/src/Models/LocalFrontendInfo.cs @@ -0,0 +1,7 @@ +namespace LocalTest.Models; + +public struct LocalFrontendInfo +{ + public string Port { get; init; } + public string Branch { get; init; } +} \ No newline at end of file diff --git a/src/Services/LocalFrontend/Implementation/LocalFrontendService.cs b/src/Services/LocalFrontend/Implementation/LocalFrontendService.cs new file mode 100644 index 00000000..2570b081 --- /dev/null +++ b/src/Services/LocalFrontend/Implementation/LocalFrontendService.cs @@ -0,0 +1,47 @@ +#nullable enable +using LocalTest.Configuration; +using LocalTest.Models; +using LocalTest.Services.LocalFrontend.Interface; +using Microsoft.Extensions.Options; + +namespace LocalTest.Services.LocalFrontend; + +public class LocalFrontendService: ILocalFrontendService +{ + private readonly HttpClient _httpClient; + private static readonly Range PortRange = 8080..8090; + private readonly string _localFrontedBaseUrl; + + public LocalFrontendService(IHttpClientFactory httpClientFactory, IOptions localPlatformSettings) + { + _httpClient = httpClientFactory.CreateClient(); + _localFrontedBaseUrl = $"{localPlatformSettings.Value.LocalFrontendProtocol}://{localPlatformSettings.Value.LocalFrontendHostname}"; + } + + public async Task> GetLocalFrontendDevPorts() + { + var ports = new List(); + for (int i = PortRange.Start.Value; i < PortRange.End.Value; i++) + { + try + { + var response = + await _httpClient.GetAsync($"{_localFrontedBaseUrl}:{i.ToString()}/"); + if (response.Headers.TryGetValues("X-Altinn-Frontend-Branch", out var values)) + { + + ports.Add(new LocalFrontendInfo() + { + Port = i.ToString(), + Branch = values?.First() ?? "Unknown" + }); + } + } + catch(HttpRequestException) + { + + } + } + return ports; + } +} \ No newline at end of file diff --git a/src/Services/LocalFrontend/Interface/ILocalFrontendService.cs b/src/Services/LocalFrontend/Interface/ILocalFrontendService.cs new file mode 100644 index 00000000..c1dc6f42 --- /dev/null +++ b/src/Services/LocalFrontend/Interface/ILocalFrontendService.cs @@ -0,0 +1,8 @@ +using LocalTest.Models; + +namespace LocalTest.Services.LocalFrontend.Interface; + +public interface ILocalFrontendService +{ + public Task> GetLocalFrontendDevPorts(); +} \ No newline at end of file diff --git a/src/Startup.cs b/src/Startup.cs index ef45057f..97d43f6a 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -36,6 +36,8 @@ using LocalTest.Services.Events.Implementation; using LocalTest.Services.LocalApp.Implementation; using LocalTest.Services.LocalApp.Interface; +using LocalTest.Services.LocalFrontend; +using LocalTest.Services.LocalFrontend.Interface; using LocalTest.Services.Profile.Implementation; using LocalTest.Services.Profile.Interface; using LocalTest.Services.Register.Implementation; @@ -199,6 +201,8 @@ public void ConfigureServices(IServiceCollection services) { services.AddTransient(); } + + services.AddTransient(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/src/appsettings.Docker.json b/src/appsettings.Docker.json index fbd54958..d53a099a 100644 --- a/src/appsettings.Docker.json +++ b/src/appsettings.Docker.json @@ -1,6 +1,7 @@ { "LocalPlatformSettings": { "LocalAppMode": "http", + "LocalFrontendHostname": "host.docker.internal", "LocalAppUrl": "http://host.docker.internal:5005", "LocalTestingStorageBasePath": "/AltinnPlatformLocal/", "LocalTestingStaticTestDataPath": "/testdata/" diff --git a/src/appsettings.Podman.json b/src/appsettings.Podman.json index ef53cb54..d53a099a 100644 --- a/src/appsettings.Podman.json +++ b/src/appsettings.Podman.json @@ -1,7 +1,8 @@ { "LocalPlatformSettings": { "LocalAppMode": "http", - "LocalAppUrl": "http://host.containers.internal:5005", + "LocalFrontendHostname": "host.docker.internal", + "LocalAppUrl": "http://host.docker.internal:5005", "LocalTestingStorageBasePath": "/AltinnPlatformLocal/", "LocalTestingStaticTestDataPath": "/testdata/" } diff --git a/src/appsettings.json b/src/appsettings.json index f642d1b1..eeef8481 100644 --- a/src/appsettings.json +++ b/src/appsettings.json @@ -40,6 +40,7 @@ "AllowedHosts": "*", "LocalPlatformSettings": { "LocalAppMode": "file", + "LocalMachineHostName": "localhost", "LocalAppUrl": "http://localhost:5005", "LocalTestingStorageBasePath": "c:/AltinnPlatformLocal/", "AppRepositoryBasePath": "",