From 7e4bbbaff5593c311595b608ce0df3f6564eff6d Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 28 Jul 2025 12:06:12 -0500 Subject: [PATCH] Fix deployment timestamp always showing 'less than a minute ago' in k8s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the deployment timestamp was set to DateTime.utc_now() when the application started, which meant it would always show as recent in k8s environments where pods are recreated on each deployment. This commit: - Updates Release.init() to check for DEPLOYED_AT environment variable - Falls back to current time if env var is not set (backwards compatible) - Adds proper ISO8601 timestamp parsing with error handling - Updates GitHub Actions deploy workflow to set DEPLOYED_AT during deployment - Creates k8s patch file for future manual deployments The deployment timestamp will now persist across pod restarts in k8s, showing the actual deployment time rather than pod startup time. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .github/workflows/deploy.yml | 27 +++++++++++++- Dockerfile | 5 ++- k8s/statefulset-deployed-at-patch.yaml | 13 +++++++ lib/aprsme/application.ex | 3 ++ lib/aprsme/release.ex | 50 ++++++++++++++++---------- lib/aprsme_web/live/map_live/index.ex | 2 +- 6 files changed, 77 insertions(+), 23 deletions(-) create mode 100644 k8s/statefulset-deployed-at-patch.yaml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index be274be..26b7ae2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -74,7 +74,32 @@ jobs: - name: Deploy to K3s run: | + # Get current timestamp in ISO8601 format + DEPLOYED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + # Create a temporary patch file with the deployment timestamp + cat > /tmp/deployed-at-patch.yaml < /app/deployed_at.txt && \ - chown elixir:root /app/deployed_at.txt +# Ensure proper ownership +RUN chown -R elixir:root /app COPY --from=builder --chown=elixir:root /app/_build/prod/rel/aprsme ./ diff --git a/k8s/statefulset-deployed-at-patch.yaml b/k8s/statefulset-deployed-at-patch.yaml new file mode 100644 index 0000000..5c078a8 --- /dev/null +++ b/k8s/statefulset-deployed-at-patch.yaml @@ -0,0 +1,13 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: aprs + namespace: aprs +spec: + template: + spec: + containers: + - name: aprs + env: + - name: DEPLOYED_AT + value: "TIMESTAMP_PLACEHOLDER" \ No newline at end of file diff --git a/lib/aprsme/application.ex b/lib/aprsme/application.ex index a8bfd46..3d360ae 100644 --- a/lib/aprsme/application.ex +++ b/lib/aprsme/application.ex @@ -9,6 +9,9 @@ defmodule Aprsme.Application do @impl true def start(_type, _args) do + # Initialize deployment timestamp + Aprsme.Release.init() + # Run migrations on startup migrate() diff --git a/lib/aprsme/release.ex b/lib/aprsme/release.ex index 60e8403..294a4bd 100644 --- a/lib/aprsme/release.ex +++ b/lib/aprsme/release.ex @@ -75,10 +75,29 @@ defmodule Aprsme.Release do This is called during application startup. """ def init do - # Read deployment timestamp from file - deployed_at = read_deployment_timestamp() + # Check if deployment timestamp is provided via environment variable + # This allows k8s deployments to set a persistent timestamp + deployed_at = + case System.get_env("DEPLOYED_AT") do + nil -> + # Fallback to current time for local development or non-k8s deployments + DateTime.utc_now() - # Add to application config + timestamp_str -> + # Parse the ISO8601 timestamp from environment variable + case DateTime.from_iso8601(timestamp_str) do + {:ok, datetime, _offset} -> + datetime + + {:error, _} -> + require Logger + + Logger.warning("Invalid DEPLOYED_AT timestamp: #{timestamp_str}, using current time") + DateTime.utc_now() + end + end + + # Store in application config Application.put_env(:aprsme, :deployed_at, deployed_at) deployed_at @@ -88,7 +107,16 @@ defmodule Aprsme.Release do Get the deployment timestamp. """ def deployed_at do - Application.get_env(:aprsme, :deployed_at) || DateTime.utc_now() + case Application.get_env(:aprsme, :deployed_at) do + nil -> + # If not initialized yet, initialize now + timestamp = DateTime.utc_now() + Application.put_env(:aprsme, :deployed_at, timestamp) + timestamp + + timestamp -> + timestamp + end end defp run_migrations_with_timeout(timeout) do @@ -113,18 +141,4 @@ defmodule Aprsme.Release do timeout: timeout ) end - - defp read_deployment_timestamp do - case File.read("/app/deployed_at.txt") do - {:ok, timestamp} -> - case DateTime.from_iso8601(String.trim(timestamp)) do - {:ok, datetime, _} -> datetime - _ -> DateTime.utc_now() - end - - _ -> - # Fallback to current time if file doesn't exist - DateTime.utc_now() - end - end end diff --git a/lib/aprsme_web/live/map_live/index.ex b/lib/aprsme_web/live/map_live/index.ex index 6ad42ea..a21afea 100644 --- a/lib/aprsme_web/live/map_live/index.ex +++ b/lib/aprsme_web/live/map_live/index.ex @@ -671,7 +671,7 @@ defmodule AprsmeWeb.MapLive.Index do end) |> Enum.filter(& &1) - socket = + socket = if Enum.any?(station_packets) do # Build packet data for the RF path stations packet_data_list = DataBuilder.build_packet_data_list(station_packets)