From 6552c915a899a412d08d12a163b02acc5b293db7 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev Date: Wed, 24 Jun 2026 02:29:46 +0700 Subject: [PATCH] Load Flutter runtime config from Vault --- .gitea/workflows/build-and-deploy.yml | 16 ------ Dockerfile | 11 ++-- .../mapflow/presentation/mapflow_shell.dart | 9 ++-- lib/main.dart | 8 +-- lib/shared/runtime_config/runtime_config.dart | 2 + .../runtime_config/runtime_config_stub.dart | 3 ++ .../runtime_config/runtime_config_web.dart | 13 +++++ nginx.conf | 2 +- scripts/40-mapflow-runtime-config.sh | 50 +++++++++++++++++++ web/index.prod.html | 1 + 10 files changed, 83 insertions(+), 32 deletions(-) create mode 100644 lib/shared/runtime_config/runtime_config.dart create mode 100644 lib/shared/runtime_config/runtime_config_stub.dart create mode 100644 lib/shared/runtime_config/runtime_config_web.dart create mode 100644 scripts/40-mapflow-runtime-config.sh diff --git a/.gitea/workflows/build-and-deploy.yml b/.gitea/workflows/build-and-deploy.yml index e839b88..b493739 100644 --- a/.gitea/workflows/build-and-deploy.yml +++ b/.gitea/workflows/build-and-deploy.yml @@ -16,33 +16,17 @@ jobs: SERVICE_NAME: flutter IMAGE_SHA: gitea.dsrptlab.com/mapflow/flutter:${{ github.sha }} IMAGE_LATEST: gitea.dsrptlab.com/mapflow/flutter:latest - VAULT_HOST: mars - VAULT_SERVICE: mapflow-vault-zy5kwf - VAULT_PROJECT_PATH: secret/projects/mapflow/prod steps: - uses: actions/checkout@v4 - name: Build and push image run: | set -euo pipefail - mapbox_token_file="$(mktemp)" - trap 'rm -f "$mapbox_token_file"' EXIT - ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes "root@$VAULT_HOST" \ - "set -euo pipefail - vault_service='$VAULT_SERVICE' - vault_project_path='$VAULT_PROJECT_PATH' - c=\$(docker ps --format '{{.Names}}' | grep \"^\$vault_service\" | head -1) - token=\$(docker service inspect \"\$vault_service\" --format '{{json .Spec.TaskTemplate.ContainerSpec.Env}}' | jq -r '.[] | select(startswith(\"VAULT_ROOT_TOKEN=\")) | sub(\"^VAULT_ROOT_TOKEN=\"; \"\")') - docker exec -e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=\"\$token\" \"\$c\" vault kv get -field=MAPBOX_ACCESS_TOKEN \"\$vault_project_path\"" \ - > "$mapbox_token_file" - [ -s "$mapbox_token_file" ] for attempt in 1 2 3; do if docker build \ --provenance=false \ --tag "$IMAGE_SHA" \ --tag "$IMAGE_LATEST" \ - --secret id=mapbox_access_token,src="$mapbox_token_file" \ - --build-arg MAPBOX_STYLE="mapbox/standard" \ --build-arg TELEGRAM_BOT_USERNAME="carfteebot" \ --build-arg BUILD_VERSION="${GITHUB_SHA}" \ . && diff --git a/Dockerfile b/Dockerfile index 2e99e8d..844d63e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,12 @@ -# syntax=docker/dockerfile:1.7 FROM ghcr.io/cirruslabs/flutter:stable AS build WORKDIR /app -ARG MAPBOX_STYLE="mapbox/standard" ARG TELEGRAM_BOT_USERNAME="carfteebot" ARG BUILD_VERSION="local" COPY pubspec.* ./ RUN flutter pub get COPY . . -RUN --mount=type=secret,id=mapbox_access_token,required=true \ - MAPBOX_ACCESS_TOKEN="$(cat /run/secrets/mapbox_access_token)" && \ - rm -rf build/web && \ +RUN rm -rf build/web && \ flutter build web --release --base-href=/ -o build/web \ - --dart-define=MAPBOX_ACCESS_TOKEN="$MAPBOX_ACCESS_TOKEN" \ - --dart-define=MAPBOX_STYLE="$MAPBOX_STYLE" \ --dart-define=TELEGRAM_BOT_USERNAME="$TELEGRAM_BOT_USERNAME" && \ cp web/index.prod.html build/web/index.html && \ rm -f build/web/index.prod.html && \ @@ -22,6 +16,9 @@ RUN --mount=type=secret,id=mapbox_access_token,required=true \ --version="$BUILD_VERSION" FROM nginx:1.27-alpine +RUN apk add --no-cache curl jq COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY scripts/40-mapflow-runtime-config.sh /docker-entrypoint.d/40-mapflow-runtime-config.sh COPY --from=build /app/build/web /usr/share/nginx/html +RUN chmod +x /docker-entrypoint.d/40-mapflow-runtime-config.sh EXPOSE 80 diff --git a/lib/features/mapflow/presentation/mapflow_shell.dart b/lib/features/mapflow/presentation/mapflow_shell.dart index 04098e7..574ca23 100644 --- a/lib/features/mapflow/presentation/mapflow_shell.dart +++ b/lib/features/mapflow/presentation/mapflow_shell.dart @@ -15,16 +15,17 @@ import '../../../app/router/app_router.dart'; import '../../../app/theme/mapflow_theme.dart'; import '../../../shared/auth/telegram_login_button.dart'; import '../../../shared/auth/telegram_session.dart' as telegram_session; +import '../../../shared/runtime_config/runtime_config.dart' as runtime_config; import '../application/place_cubit.dart'; import '../data/auth_repository.dart'; import '../data/places_repository.dart'; import '../domain/place_models.dart'; import 'widgets/place_photo_card.dart'; -const _mapboxStyle = String.fromEnvironment( - 'MAPBOX_STYLE', - defaultValue: 'mapbox/standard', -); +String get _mapboxStyle { + final style = runtime_config.mapboxStyle(); + return style.isEmpty ? 'mapbox/standard' : style; +} class MapflowShell extends StatelessWidget { const MapflowShell({super.key}); diff --git a/lib/main.dart b/lib/main.dart index 6cb85d7..9b2be3d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,13 +4,13 @@ import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' as mbx; import 'app/app.dart'; import 'shared/auth/telegram_session.dart' as telegram_session; - -const _mapboxAccessToken = String.fromEnvironment('MAPBOX_ACCESS_TOKEN'); +import 'shared/runtime_config/runtime_config.dart' as runtime_config; void main() { WidgetsFlutterBinding.ensureInitialized(); - if (_mapboxAccessToken.isNotEmpty) { - mbx.MapboxOptions.setAccessToken(_mapboxAccessToken); + final mapboxAccessToken = runtime_config.mapboxAccessToken(); + if (mapboxAccessToken.isNotEmpty) { + mbx.MapboxOptions.setAccessToken(mapboxAccessToken); } usePathUrlStrategy(); telegram_session.configureTelegramWebApp(); diff --git a/lib/shared/runtime_config/runtime_config.dart b/lib/shared/runtime_config/runtime_config.dart new file mode 100644 index 0000000..d3e3b66 --- /dev/null +++ b/lib/shared/runtime_config/runtime_config.dart @@ -0,0 +1,2 @@ +export 'runtime_config_stub.dart' + if (dart.library.js_interop) 'runtime_config_web.dart'; diff --git a/lib/shared/runtime_config/runtime_config_stub.dart b/lib/shared/runtime_config/runtime_config_stub.dart new file mode 100644 index 0000000..0ce5857 --- /dev/null +++ b/lib/shared/runtime_config/runtime_config_stub.dart @@ -0,0 +1,3 @@ +String mapboxAccessToken() => ''; + +String mapboxStyle() => ''; diff --git a/lib/shared/runtime_config/runtime_config_web.dart b/lib/shared/runtime_config/runtime_config_web.dart new file mode 100644 index 0000000..bca104e --- /dev/null +++ b/lib/shared/runtime_config/runtime_config_web.dart @@ -0,0 +1,13 @@ +import 'dart:js_interop'; + +@JS('__MAPFLOW_CONFIG__') +external _MapflowConfig? get _config; + +extension type _MapflowConfig(JSObject _) implements JSObject { + external JSString? get mapboxAccessToken; + external JSString? get mapboxStyle; +} + +String mapboxAccessToken() => _config?.mapboxAccessToken?.toDart ?? ''; + +String mapboxStyle() => _config?.mapboxStyle?.toDart ?? ''; diff --git a/nginx.conf b/nginx.conf index ff4a688..4c42bd6 100644 --- a/nginx.conf +++ b/nginx.conf @@ -28,7 +28,7 @@ server { try_files /index.html =404; } - location ~* ^/(bootstrap\.js|sw\.js)$ { + location ~* ^/(bootstrap\.js|sw\.js|config\.js)$ { add_header Cache-Control "no-cache" always; add_header Pragma "no-cache" always; add_header Expires "0" always; diff --git a/scripts/40-mapflow-runtime-config.sh b/scripts/40-mapflow-runtime-config.sh new file mode 100644 index 0000000..0394798 --- /dev/null +++ b/scripts/40-mapflow-runtime-config.sh @@ -0,0 +1,50 @@ +#!/bin/sh +set -eu + +config_path="/usr/share/nginx/html/config.js" + +require_env() { + name="$1" + eval "value=\${$name:-}" + if [ -z "$value" ]; then + echo "$name is required when VAULT_ENABLED=true." >&2 + exit 1 + fi +} + +read_vault_path() { + path="$1" + curl -fsS \ + -H "X-Vault-Token: $VAULT_TOKEN" \ + "$VAULT_ADDR/v1/$VAULT_KV_MOUNT/data/$path" | + jq -e '.data.data' +} + +json_string() { + jq -Rn --arg value "$1" '$value' +} + +if [ "${VAULT_ENABLED:-}" = "true" ]; then + require_env VAULT_ADDR + require_env VAULT_TOKEN + require_env VAULT_KV_MOUNT + require_env VAULT_SHARED_PATH + require_env VAULT_PROJECT_PATH + + shared_env="$(read_vault_path "$VAULT_SHARED_PATH")" + project_env="$(read_vault_path "$VAULT_PROJECT_PATH")" + runtime_env="$(jq -n --argjson shared "$shared_env" --argjson project "$project_env" '$shared * $project')" + + MAPBOX_ACCESS_TOKEN="$(printf '%s' "$runtime_env" | jq -er '.MAPBOX_ACCESS_TOKEN')" + MAPBOX_STYLE="$(printf '%s' "$runtime_env" | jq -r '.MAPBOX_STYLE // "mapbox/standard"')" +else + MAPBOX_ACCESS_TOKEN="${MAPBOX_ACCESS_TOKEN:-}" + MAPBOX_STYLE="${MAPBOX_STYLE:-mapbox/standard}" +fi + +cat >"$config_path" < +