Load Flutter runtime config from Vault
Build and deploy Flutter Web / build (push) Successful in 6m25s

This commit is contained in:
Ruslan Bakiev
2026-06-24 02:29:46 +07:00
parent c427801621
commit 6552c915a8
10 changed files with 83 additions and 32 deletions
-16
View File
@@ -16,33 +16,17 @@ jobs:
SERVICE_NAME: flutter SERVICE_NAME: flutter
IMAGE_SHA: gitea.dsrptlab.com/mapflow/flutter:${{ github.sha }} IMAGE_SHA: gitea.dsrptlab.com/mapflow/flutter:${{ github.sha }}
IMAGE_LATEST: gitea.dsrptlab.com/mapflow/flutter:latest IMAGE_LATEST: gitea.dsrptlab.com/mapflow/flutter:latest
VAULT_HOST: mars
VAULT_SERVICE: mapflow-vault-zy5kwf
VAULT_PROJECT_PATH: secret/projects/mapflow/prod
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Build and push image - name: Build and push image
run: | run: |
set -euo pipefail 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 for attempt in 1 2 3; do
if docker build \ if docker build \
--provenance=false \ --provenance=false \
--tag "$IMAGE_SHA" \ --tag "$IMAGE_SHA" \
--tag "$IMAGE_LATEST" \ --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 TELEGRAM_BOT_USERNAME="carfteebot" \
--build-arg BUILD_VERSION="${GITHUB_SHA}" \ --build-arg BUILD_VERSION="${GITHUB_SHA}" \
. && . &&
+4 -7
View File
@@ -1,18 +1,12 @@
# syntax=docker/dockerfile:1.7
FROM ghcr.io/cirruslabs/flutter:stable AS build FROM ghcr.io/cirruslabs/flutter:stable AS build
WORKDIR /app WORKDIR /app
ARG MAPBOX_STYLE="mapbox/standard"
ARG TELEGRAM_BOT_USERNAME="carfteebot" ARG TELEGRAM_BOT_USERNAME="carfteebot"
ARG BUILD_VERSION="local" ARG BUILD_VERSION="local"
COPY pubspec.* ./ COPY pubspec.* ./
RUN flutter pub get RUN flutter pub get
COPY . . COPY . .
RUN --mount=type=secret,id=mapbox_access_token,required=true \ RUN rm -rf build/web && \
MAPBOX_ACCESS_TOKEN="$(cat /run/secrets/mapbox_access_token)" && \
rm -rf build/web && \
flutter build web --release --base-href=/ -o 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" && \ --dart-define=TELEGRAM_BOT_USERNAME="$TELEGRAM_BOT_USERNAME" && \
cp web/index.prod.html build/web/index.html && \ cp web/index.prod.html build/web/index.html && \
rm -f build/web/index.prod.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" --version="$BUILD_VERSION"
FROM nginx:1.27-alpine FROM nginx:1.27-alpine
RUN apk add --no-cache curl jq
COPY nginx.conf /etc/nginx/conf.d/default.conf 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 COPY --from=build /app/build/web /usr/share/nginx/html
RUN chmod +x /docker-entrypoint.d/40-mapflow-runtime-config.sh
EXPOSE 80 EXPOSE 80
@@ -15,16 +15,17 @@ import '../../../app/router/app_router.dart';
import '../../../app/theme/mapflow_theme.dart'; import '../../../app/theme/mapflow_theme.dart';
import '../../../shared/auth/telegram_login_button.dart'; import '../../../shared/auth/telegram_login_button.dart';
import '../../../shared/auth/telegram_session.dart' as telegram_session; 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 '../application/place_cubit.dart';
import '../data/auth_repository.dart'; import '../data/auth_repository.dart';
import '../data/places_repository.dart'; import '../data/places_repository.dart';
import '../domain/place_models.dart'; import '../domain/place_models.dart';
import 'widgets/place_photo_card.dart'; import 'widgets/place_photo_card.dart';
const _mapboxStyle = String.fromEnvironment( String get _mapboxStyle {
'MAPBOX_STYLE', final style = runtime_config.mapboxStyle();
defaultValue: 'mapbox/standard', return style.isEmpty ? 'mapbox/standard' : style;
); }
class MapflowShell extends StatelessWidget { class MapflowShell extends StatelessWidget {
const MapflowShell({super.key}); const MapflowShell({super.key});
+4 -4
View File
@@ -4,13 +4,13 @@ import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' as mbx;
import 'app/app.dart'; import 'app/app.dart';
import 'shared/auth/telegram_session.dart' as telegram_session; import 'shared/auth/telegram_session.dart' as telegram_session;
import 'shared/runtime_config/runtime_config.dart' as runtime_config;
const _mapboxAccessToken = String.fromEnvironment('MAPBOX_ACCESS_TOKEN');
void main() { void main() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
if (_mapboxAccessToken.isNotEmpty) { final mapboxAccessToken = runtime_config.mapboxAccessToken();
mbx.MapboxOptions.setAccessToken(_mapboxAccessToken); if (mapboxAccessToken.isNotEmpty) {
mbx.MapboxOptions.setAccessToken(mapboxAccessToken);
} }
usePathUrlStrategy(); usePathUrlStrategy();
telegram_session.configureTelegramWebApp(); telegram_session.configureTelegramWebApp();
@@ -0,0 +1,2 @@
export 'runtime_config_stub.dart'
if (dart.library.js_interop) 'runtime_config_web.dart';
@@ -0,0 +1,3 @@
String mapboxAccessToken() => '';
String mapboxStyle() => '';
@@ -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 ?? '';
+1 -1
View File
@@ -28,7 +28,7 @@ server {
try_files /index.html =404; 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 Cache-Control "no-cache" always;
add_header Pragma "no-cache" always; add_header Pragma "no-cache" always;
add_header Expires "0" always; add_header Expires "0" always;
+50
View File
@@ -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" <<EOF
window.__MAPFLOW_CONFIG__ = {
mapboxAccessToken: $(json_string "$MAPBOX_ACCESS_TOKEN"),
mapboxStyle: $(json_string "$MAPBOX_STYLE")
};
EOF
+1
View File
@@ -87,6 +87,7 @@
}); });
} }
</script> </script>
<script src="config.js"></script>
<script <script
defer defer
data-sw-bootstrap data-sw-bootstrap