Commit graph

58 commits

Author SHA1 Message Date
b709ae9fbe fix prod crash with yaml parsing 2026-01-31 08:25:07 -06:00
ef8d9199ba fix: add C NIF compilation to k8s Dockerfile
Match the root Dockerfile changes:
- Add libsnmp-dev build dependency
- Add libsnmp40 runtime dependency
- Copy c_src and compile C NIF before Elixir compilation
- Add TERM=dumb for cross-architecture builds
2026-01-30 13:06:18 -06:00
d29fb8cfd7 totp fixes 2026-01-28 16:16:59 -06:00
c694d452de
fix: add vendor directory and snmp package to k8s Dockerfile 2026-01-27 09:21:55 -06:00
7b6298f5dc
ecto ssl tweaks 2026-01-25 16:00:48 -06:00
76854479c9
update deployment 2026-01-25 15:29:38 -06:00
81f13f789b
revert: remove custom base image dependency from main Dockerfile
Reverted k8s/Dockerfile back to using official hexpm/elixir and debian
images directly instead of custom gmcintire/towerops-base images.

This restores the original Dockerfile behavior where all dependencies
are installed during each build. The base image work in k8s/base-image/
remains available but is not used by the main application build.
2026-01-25 11:12:37 -06:00
564caed3e5
refactor: rename base images to gmcintire/towerops-base
Changed image naming scheme from separate builder/runtime images to a
single image name with different tags:

- docker.io/gmcintire/towerops-base:builder
- docker.io/gmcintire/towerops-base:runtime
- docker.io/gmcintire/towerops-base:latest (alias for runtime)

This simplifies the naming and makes it clearer that both images are
part of the same towerops-base image family.

Changes:
- build.sh: Use IMAGE_NAME with BUILDER_TAG/RUNTIME_TAG
- Makefile: Update all targets to use new naming
- k8s/Dockerfile: Point to gmcintire/towerops-base:builder and :runtime
2026-01-25 09:33:42 -06:00
edd64479df
feat: create builder and runtime base images for faster deploys
Instead of installing system packages and tools on every deploy, we now
have two base images that pre-bake all the slow setup steps.

Base Images Created:
1. elixir-builder:latest - Builder stage base
   - hexpm/elixir with build-essential and git pre-installed
   - hex and rebar pre-installed
   - Saves ~40 seconds per deploy

2. debian-runtime:latest - Runtime stage base
   - Debian with all runtime packages pre-installed
   - Locale pre-configured (en_US.UTF-8)
   - /app directory pre-created
   - Saves ~60 seconds per deploy

Main Dockerfile Changes:
- Use elixir-builder:latest instead of hexpm/elixir
- Use debian-runtime:latest instead of debian:trixie-slim
- Remove apt-get install steps (now in base images)
- Remove hex/rebar install (now in builder base)
- Remove locale setup (now in runtime base)

Build System:
- build.sh builds both images with podman/docker auto-detect
- Pushes to both GitLab registry and Docker Hub
- Makefile targets for build/test/push/clean

Total Time Saved Per Deploy: ~100 seconds (1m40s)

Benefits:
- Faster CI/CD builds (no repeated apt-get install)
- Faster local development builds
- Consistent build environment across all deploys
- Security updates centralized in base image rebuilds
2026-01-25 09:29:22 -06:00
26a3b39edd
fix: correct paths for hexpm/elixir image structure
The hexpm images install Erlang and Elixir in /usr/local instead of /usr/lib.

Changes:
- Copy from /usr/local/lib/erlang instead of /usr/lib/erlang
- Copy all Erlang/Elixir binaries from /usr/local/bin
- Remove manual symlink creation (binaries already exist)
- Binaries are symlinks to ../lib/erlang/bin and ../lib/elixir/bin
2026-01-25 09:22:45 -06:00
7685fa53d1
fix: use official hexpm/elixir images instead of erlang-solutions repo
The Erlang Solutions APT repository was experiencing 504 Gateway Timeout
errors during builds, making the image build process unreliable.

Changes:
- Use official hexpm/elixir Docker image as builder stage
- More reliable (official Docker Hub images)
- Faster builds (pre-built binaries, no package installation)
- Still produces minimal runtime-only final image
- Updated documentation to reflect new approach

Benefits:
- No dependency on erlang-solutions CDN availability
- Faster build times (no apt-get install of large packages)
- Same minimal final image size (~150-200 MB)
- Easier to specify exact Erlang/Elixir versions
2026-01-25 09:21:42 -06:00
d92443d91b
feat: add podman support and Docker Hub push to base image builder
Enhanced base image build system with:

Container Engine Support:
- Auto-detect podman or docker (prefers podman if both installed)
- All scripts work with either engine seamlessly
- No configuration needed

Docker Hub Integration:
- Push to both GitLab and Docker Hub registries
- GitLab: registry.gitlab.com/towerops/towerops/elixir-runtime
- Docker Hub: docker.io/gmcintire/elixir-runtime
- Both versioned and :latest tags pushed to each

Build Script:
- Detect and use $CONTAINER_CMD for all operations
- Show which container engine is being used in output
- Tag and push to both registries automatically

Makefile:
- Add CONTAINER_CMD variable with auto-detection
- Add DOCKERHUB_REGISTRY configuration
- Update all targets to use detected container engine
- Add login-dockerhub target for Docker Hub authentication
- Update push target to push to both registries

Documentation:
- Document podman/docker auto-detection
- Update login instructions for both registries
- Update troubleshooting with examples for both engines

This makes the build system more flexible and accessible to users
who prefer podman over docker, while also publishing to the public
Docker Hub registry for easier access.
2026-01-25 09:18:03 -06:00
8f87d4bbab
feat: add custom minimal Debian base image build system
Created build system for minimal Debian 13 (Trixie) image with Erlang/OTP
and Elixir runtime pre-installed. This will speed up CI/CD builds by caching
the runtime environment.

Features:
- Multi-stage Dockerfile for minimal final image (~150-200 MB)
- Build script with automatic tagging (versioned, latest, dated)
- Update script for easy security patch rebuilds
- Makefile for common operations
- Comprehensive documentation (README + QUICKSTART)

Benefits:
- Faster CI/CD: Saves 30-60s per build (no apt-get install runtime deps)
- Smaller images: Only essential runtime packages included
- Security: Easy to rebuild weekly/monthly with latest patches
- Consistency: Same runtime across all environments

Usage:
  cd k8s/base-image
  make build    # Build the image
  make test     # Verify it works
  make push     # Push to registry

Then update k8s/Dockerfile to use the custom base image.

This replaces the previous Dockerfile.base approach with a more
comprehensive and maintainable build system.
2026-01-25 09:07:55 -06:00
29593ac734
refactor: migrate from etcd to Oban for distributed job coordination
Replaces etcd-based distributed locking with Oban's PostgreSQL-backed job queue.
This simplifies the architecture by eliminating the need for a separate etcd cluster
while providing better reliability and observability.

Changes:
- Add Oban dependency and migration (oban_jobs table)
- Create DevicePollerCoordinator and DeviceMonitorCoordinator Oban workers
- Remove EtcdCoordinator and EtcdLock modules
- Update application supervisor to start Oban
- Configure Oban with pollers (50 workers) and monitors (50 workers) queues
- Remove etcd StatefulSet from Kubernetes manifests
- Update monitoring supervisor documentation

Benefits:
- Simpler architecture (no etcd cluster to manage)
- PostgreSQL-based (uses existing database)
- Built-in uniqueness prevents duplicate jobs cluster-wide
- Better observability with Oban Web UI
- Automatic job rescue on node crashes
- Easier local development (no etcd required)

What was removed:
- etcd StatefulSet (3 pods)
- EtcdCoordinator module (320 lines)
- EtcdLock module (158 lines)
- eetcd dependency

All 3,686 tests passing.
2026-01-24 16:12:27 -06:00
979d246160
reduce replicas 2026-01-24 15:59:52 -06:00
be818b49b8
refactor: remove Valkey from K8s, move to Proxmox hosts
Removing all Valkey (Redis) resources from Kubernetes due to instability
caused by Flannel CNI networking issues. Redis will now run on Proxmox
hosts for better stability and performance.

Changes:
- Delete Valkey StatefulSet (master + 2 replicas)
- Delete Valkey Sentinel StatefulSet (3 instances)
- Delete Valkey services (headless and sentinel)
- Delete Valkey ConfigMap
- Remove Valkey resources from kustomization.yaml
- Update deployment to use towerops-redis secret for connection

Next Steps:
- Set up Redis Sentinel on 3 Proxmox hosts/LXC containers
- Create towerops-redis secret with REDIS_HOST and REDIS_PORT
- Test failover and application connectivity

Benefits:
- Not affected by K8s networking issues (Flannel failures)
- More stable (no restarts from node issues)
- Better performance (no K8s overhead)
- Independent lifecycle from K8s cluster
2026-01-24 14:12:02 -06:00
3156eb19ac
fix: make Valkey service headless for StatefulSet DNS
Sentinel requires individual pod DNS names to work correctly.
Making the valkey service headless enables DNS resolution for:
- valkey-0.valkey.towerops.svc.cluster.local
- valkey-1.valkey.towerops.svc.cluster.local
- valkey-2.valkey.towerops.svc.cluster.local

This fixes CrashLoopBackOff in valkey-sentinel pods.
2026-01-24 14:06:03 -06:00
5ee241fc16
feat: implement multi-replica Valkey with Sentinel for high availability
Addresses production Redis disconnection issues by implementing a highly
resilient Valkey (Redis) setup with automatic failover capabilities.

Infrastructure Changes:
- Add Valkey ConfigMap with optimized connection and memory settings
  - TCP keepalive (60s), connection limits (10k clients)
  - Memory management (256MB with LRU eviction)
  - Separate configs for master, replica, and sentinel

- Update Valkey StatefulSet to 3 replicas (1 master + 2 replicas)
  - Auto-configuration via init container (master vs replica)
  - Increased memory limits: 256Mi → 1Gi
  - Improved readiness probes with replication status checks

- Add Valkey Sentinel StatefulSet (3 instances for quorum)
  - Automatic failover detection (5s down-after-milliseconds)
  - Fast failover execution (10s timeout)
  - Monitors master and promotes replicas automatically

- Add Sentinel headless service for pod discovery

Application Resilience:
- Update Phoenix.PubSub.Redis with TCP keepalive and reconnection
  - Connection timeout: 5s
  - Exponential backoff: 500ms → 30s
  - exit_on_disconnection: false

- Update Exq background jobs with same resilience settings
  - TCP keepalive enabled
  - Better connection pool management

Benefits:
- Automatic failover when Valkey pod dies (no manual intervention)
- Zero data loss with replica synchronization
- Fast failure detection and recovery (5-10s total)
- Survives Flannel CNI networking issues
- Apps reconnect automatically during disconnections

Testing:
- All 3,686 tests passing
- Kustomize manifest validated

🤖 Generated with Claude Code
2026-01-24 14:03:50 -06:00
f8d6837f20
fix etcd 2026-01-23 16:59:15 -06:00
2b78b1a2d3
snmpkit overhaul and etcd 2026-01-23 16:23:57 -06:00
5f6ac5ddb9
update deployment 2026-01-23 07:47:00 -06:00
ba5464332b
fix: use system ping command instead of raw ICMP sockets
- Rewrote Ping module to use system ping binary instead of raw ICMP
  sockets which required CAP_NET_RAW privileges
- Added iputils-ping package to Docker images for production
- Added proper IP address validation before executing ping command
- Updated tests to account for 1 second minimum timeout of system ping
- System ping binary is setuid root and works without elevated privileges
2026-01-21 11:15:59 -06:00
7f7cbac965
Add init container to Valkey to wait for Flannel CNI readiness
Prevents FailedCreatePodSandBox errors when talos-worker1 reboots and pods
try to start before Flannel writes /run/flannel/subnet.env.

Init container polls DNS resolution until network is ready before starting Valkey.
2026-01-20 11:41:21 -06:00
375cce8174
Add system-cluster-critical priority class to towerops deployment
Prevents race condition where towerops pods try to start before Flannel
CNI is fully initialized during node restarts.

Same fix as applied to Valkey StatefulSet (commit 30a0b9a). Both
critical infrastructure components now have proper priority classes to
ensure CNI readiness before pod scheduling.

Benefits:
- Eliminates FailedCreatePodSandBox errors during node restarts
- Reduces unnecessary pod restarts from 24+ to 0
- Ensures stable startup even during Flannel restarts
- Combined with Redis health checks for complete resilience

Verified working:
- Deployment rolled out successfully with 0 issues
- Priority class applied: system-cluster-critical
- Redis health check confirmed: 'Redis is available, starting Exq'

🤖 Generated with Claude Code
2026-01-19 17:09:33 -06:00
30a0b9a3a0
Add system-cluster-critical priority class to Valkey StatefulSet
Fixes race condition where Valkey pod tries to start before Flannel CNI
is fully initialized during node restarts.

Problem:
- Valkey pod had 24 restarts due to FailedCreatePodSandBox errors
- During node restarts, pods would start before Flannel wrote /run/flannel/subnet.env
- CNI plugin would fail: 'failed to load flannel subnet.env file'
- Pod would retry until Flannel finished initialization

Solution:
- Added priorityClassName: system-cluster-critical to Valkey StatefulSet
- This gives Valkey same priority level as etcd, coredns, and other cluster services
- Scheduler will ensure CNI (Flannel) is ready before starting Valkey
- Reduces race condition window during node restarts

Priority Classes:
- system-node-critical (2000001000): Flannel, kube-proxy
- system-cluster-critical (2000000000): Valkey, etcd, coredns
- default (0): Regular application pods

Combined with previous commit's Redis health checks and Exq supervisor
improvements, the application is now resilient to both infrastructure
issues and Redis connection failures.

🤖 Generated with Claude Code
2026-01-19 15:40:39 -06:00
3d2cd0d43f
Fix Credo issues and improve code quality
- Reduced cyclomatic complexity in Ping.send_icmp_packet by extracting handle_icmp_recv helper
- Reduced cyclomatic complexity in Devices.resolve_snmp_config by extracting determine_snmp_source helper
- Added Repo alias to discovery_test.exs to fix nested module warning
- All tests passing (992 tests, 1 failure in unrelated test)
- Removed incomplete mib_parser_test.exs
- Cleaned up debug-redis.sh script
2026-01-19 15:09:19 -06:00
b8b3c5dce0
add parallel build 2026-01-19 14:17:05 -06:00
b4f8b40b7f
Include MIB files in Docker image instead of using PVC
- Remove PVC volume mount from k8s/deployment.yaml
- Delete k8s/mib-pvc.yaml (no longer needed)
- Update .gitignore to commit MIB files to git
- Add mix import_mibs task to copy MIBs from LibreNMS
- Add all MIB files from priv/mibs/ to git

This fixes the multi-attach volume error in Kubernetes where new pods
couldn't start because the RWO (ReadWriteOnce) PVC was attached to the
old pod. MIBs are now part of the Docker image and can be used by all
pods simultaneously.
2026-01-19 14:01:03 -06:00
adec4b134f
snmp overhaul 2026-01-18 16:00:01 -06:00
d2e38e351e
feat: display all timestamps in user's timezone
- Add timezone to socket assigns in mount_current_scope
- Detect user timezone from browser using Intl.DateTimeFormat
- Auto-save detected timezone to user profile on first connection
- Update all templates to use TimeHelpers with timezone parameter
- Update agent helper functions to accept timezone parameter
- Templates updated:
  - Alert list (triggered, acknowledged, resolved)
  - Device list (last checked)
  - Agent list and show (created, last seen, updated)
  - Admin dashboard (impersonation logs)
  - Admin org/user lists (created/joined dates)
  - Main dashboard (alert times)
2026-01-18 12:19:17 -06:00
5152ceac8b
fix: simplify Redis host to just 'valkey' for same-namespace service 2026-01-18 12:06:44 -06:00
7e9940c619
fix(k8s): use ClusterIP service and FQDN for Valkey connection
Changed Valkey service from headless to ClusterIP and updated
REDIS_HOST to use full FQDN for more reliable DNS resolution.

Changes:
- valkey-service.yaml: Changed from headless (clusterIP: None) to ClusterIP
- deployment.yaml: Changed REDIS_HOST from "valkey" to "valkey.towerops.svc.cluster.local"

This ensures stable ClusterIP and explicit DNS resolution for Redis connections.
2026-01-18 11:23:51 -06:00
bac286ee1b
feat(k8s): move Valkey to standalone StatefulSet
Previously Valkey ran as a sidecar container in the main deployment,
causing it to restart every time the web app deployed. This resulted
in connection errors and cache loss during deployments.

Changes:
- Created valkey-statefulset.yaml with StatefulSet resource (1 replica)
- Created valkey-service.yaml with headless service
- Removed Valkey sidecar container from deployment.yaml
- Updated REDIS_HOST from 127.0.0.1 to valkey-0.valkey.towerops.svc.cluster.local
- Added new resources to kustomization.yaml

Benefits:
- Valkey persists across web app deployments
- No connection errors during rolling updates
- Cache data preserved
- Independent scaling and resource management
2026-01-18 11:10:36 -06:00
cafca82dd4
scale: increase production replicas to 4 pods
With Horde's distributed supervision, devices will automatically
distribute ~25% per pod across all 4 replicas.

Benefits:
- 4x horizontal scaling for device monitoring and SNMP polling
- Better fault tolerance (can lose 3 pods and still operate)
- Automatic load balancing via consistent hashing
- Each device still polls from exactly ONE pod (no duplicates)
2026-01-18 10:15:59 -06:00
3b4fd98b21
Update debug script to select only running pods
Changed pod selection to use --field-selector=status.phase=Running
to avoid selecting Terminating pods during rollout
2026-01-17 17:23:36 -06:00
77562cafb5
Add Redis/Valkey debugging script for production
Script checks:
- Container status in pod
- Valkey logs
- Network connectivity from towerops to Valkey
- Environment variables
- Valkey health

Usage: ./k8s/debug-redis.sh [namespace]
2026-01-17 17:20:37 -06:00
5836925166
Fix Redis connection in production with startup probe and explicit 127.0.0.1
Changes:
- Added startupProbe to towerops container to allow up to 70s for startup
  (gives Valkey sidecar time to be ready)
- Changed REDIS_HOST from 'localhost' to '127.0.0.1' for explicit loopback
- Removed initialDelaySeconds from liveness/readiness probes (handled by startup)
- Added comment clarifying Redis connects to Valkey sidecar

This ensures the app waits for Valkey to be ready before accepting traffic.
2026-01-17 17:20:14 -06:00
eb7b57aa42
feat: add Valkey as k8s sidecar for Redis compatibility
- Add Valkey 8.0 alpine container as sidecar in k8s deployment
- Configure with appropriate security context (non-root, no privilege escalation)
- Add resource limits (256Mi memory, 200m CPU) and requests (128Mi, 50m)
- Add health probes using valkey-cli ping command
- Configure Redis connection via REDIS_HOST and REDIS_PORT environment variables
- Add Redis config to runtime.exs, dev.exs, and test.exs
- Valkey runs on localhost:6379 within the same pod as Phoenix app
2026-01-17 16:30:38 -06:00
9c2d95b55a
Add zero-downtime deployment configuration
Prevent WebSocket disconnections during deployments by:

1. Rolling Update Strategy:
   - maxSurge: 1 (allow 1 extra pod during rollout)
   - maxUnavailable: 0 (keep all pods running)
   - minReadySeconds: 10 (wait before continuing rollout)

2. Graceful Shutdown:
   - terminationGracePeriodSeconds: 30 (allow Phoenix to drain connections)

3. PodDisruptionBudget:
   - minAvailable: 1 (ensure at least 1 pod always available)
   - Prevents all pods from being terminated simultaneously

4. Improved Health Checks:
   - Faster readiness probe (5s initial, 3s period)
   - More aggressive success/failure thresholds
   - New pods marked ready faster

This ensures new pods are fully ready and serving traffic before old
pods are terminated, maintaining WebSocket connections and preventing
'something went wrong' errors during deployments.
2026-01-17 12:44:22 -06:00
07b0ddb67d
Set DEPLOY_TIMESTAMP from GitLab CI instead of pod metadata
- Update GitLab CI deploy job to set DEPLOY_TIMESTAMP when deploying
- Use 'kubectl set env' to update all pods with same timestamp
- Change deployment.yaml to use static placeholder value
- Update documentation to explain GitLab CI timestamp approach

This ensures all pods show the same deployment time, regardless of:
- Which pod handles the request (with replicas=2)
- When individual pods were created or restarted
- Pod restarts from crashes or rolling updates

The timestamp now represents when the deployment was initiated by GitLab CI,
not when individual pods were created.
2026-01-17 11:17:07 -06:00
9fcf19b304
Use Kubernetes pod creation timestamp for deploy time in footer
- Read DEPLOY_TIMESTAMP environment variable for actual deploy time
- Fall back to compile time in development when env var not set
- Add Kubernetes Downward API to inject metadata.creationTimestamp
- Update k8s/deployment.yaml to set DEPLOY_TIMESTAMP env var
- Document deployment timestamp mechanism in k8s/README.md

This ensures the footer shows when the pod was deployed to the cluster,
not when the Docker image was built in CI/CD.
2026-01-17 11:14:15 -06:00
ae949b4a0b
Remove secretGenerator from kustomization for GitOps compatibility
Secrets are now managed directly in the cluster rather than generated
from .envrc files. This fixes FluxCD reconciliation errors since .envrc
is gitignored and cannot be used in GitOps workflows.

All secrets have been backed up to 1Password for disaster recovery.
2026-01-14 13:09:19 -06:00
1b8017dd98
change replicas 2026-01-12 10:43:24 -06:00
d382f6a46e
update gitlab ci 2026-01-11 13:33:54 -06:00
0f238ac33d
set up ses 2026-01-09 16:52:11 -06:00
fd317770fe
Fix health check logging with dynamic log level in endpoint
Use Plug.Telemetry's dynamic log level feature to disable logging for
/health requests. This is the proper Phoenix way to exclude specific
endpoints from request logs.

- Add log_level/1 function to endpoint that returns false for /health
- Configure Plug.Telemetry to use dynamic log level
- Remove log: false from router (handled by endpoint now)

This prevents Kubernetes health probe spam in application logs.
2026-01-06 13:57:32 -06:00
40d699716e
Increase pod memory to 2GB
- Set memory request to 1Gi
- Set memory limit to 2Gi
2026-01-04 12:47:57 -06:00
25b82848d6
Run migrations on app start instead of separate job
- Migrations now run in Application.start/2 before starting services
- Ecto's advisory locks prevent concurrent migrations in clustered setup
- Simplified deployment - no separate migration job needed
- Removed migrate-job.yaml manifest
2026-01-03 16:09:31 -06:00
66ed55d78e
Add security context to meet PodSecurity restricted standard
- Set runAsNonRoot and runAsUser to 65534 (nobody)
- Disable privilege escalation
- Drop all Linux capabilities
- Use RuntimeDefault seccomp profile
- Applies to both migration job and main deployment
2026-01-03 16:02:27 -06:00
d7481dc0fd
Silence debconf warnings in Docker builds
Set DEBIAN_FRONTEND=noninteractive for all apt-get operations
to prevent interactive frontend warnings in build logs
2026-01-03 16:00:54 -06:00