From 23855b4f862626ce8e6a7604768b3c6869b9aa2a Mon Sep 17 00:00:00 2001 From: Allen Eubank Date: Fri, 9 Oct 2026 17:57:35 -0500 Subject: [PATCH] feat: build a GHCR image and add swiss k8s manifests Builds ghcr.io/0xsend/token-supply-api on every push to main (tagged sha- and latest) and adds a kustomize base plus a prod overlay for the swiss cluster: Deployment (non-root, restricted PSA), Service, nginx Ingress for supply.send.it, and an ExternalSecret for the RPC URLs. Flux wiring lives in 0xsend/infra. The prod overlay's image tag is a placeholder until the first main build exists. --- .github/workflows/image.yml | 40 +++++++++++++++++ k8s/base/deployment.yaml | 65 +++++++++++++++++++++++++++ k8s/base/ingress.yaml | 24 ++++++++++ k8s/base/kustomization.yaml | 7 +++ k8s/base/service.yaml | 14 ++++++ k8s/overlays/prod/externalsecret.yaml | 25 +++++++++++ k8s/overlays/prod/kustomization.yaml | 13 ++++++ k8s/overlays/prod/namespace.yaml | 10 +++++ 8 files changed, 198 insertions(+) create mode 100644 .github/workflows/image.yml create mode 100644 k8s/base/deployment.yaml create mode 100644 k8s/base/ingress.yaml create mode 100644 k8s/base/kustomization.yaml create mode 100644 k8s/base/service.yaml create mode 100644 k8s/overlays/prod/externalsecret.yaml create mode 100644 k8s/overlays/prod/kustomization.yaml create mode 100644 k8s/overlays/prod/namespace.yaml diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml new file mode 100644 index 0000000..4fd5462 --- /dev/null +++ b/.github/workflows/image.yml @@ -0,0 +1,40 @@ +name: Build image + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v7 + + - uses: docker/setup-buildx-action@v4 + + - uses: docker/login-action@v4 + if: github.event_name == 'push' + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - id: meta + uses: docker/metadata-action@v6 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=sha,format=long + type=raw,value=latest,enable={{is_default_branch}} + + - uses: docker/build-push-action@v7 + with: + context: . + push: ${{ github.event_name == 'push' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64 diff --git a/k8s/base/deployment.yaml b/k8s/base/deployment.yaml new file mode 100644 index 0000000..0dceedf --- /dev/null +++ b/k8s/base/deployment.yaml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: token-supply-api + labels: + app.kubernetes.io/name: token-supply-api +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: token-supply-api + template: + metadata: + labels: + app.kubernetes.io/name: token-supply-api + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + seccompProfile: + type: RuntimeDefault + imagePullSecrets: + - name: ghcr-credentials + containers: + - name: token-supply-api + # Pinned per environment by the overlay `images:` transformer. + image: ghcr.io/0xsend/token-supply-api:latest + ports: + - containerPort: 8080 + name: http + env: + - name: NODE_ENV + value: production + envFrom: + - secretRef: + name: token-supply-api + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + resources: + requests: + cpu: 20m + memory: 96Mi + limits: + memory: 256Mi + # The server binds only after the first total and circulating supply + # lookups succeed, so an answering /total means the cached values exist. + startupProbe: + httpGet: + path: /total + port: http + periodSeconds: 5 + failureThreshold: 24 + readinessProbe: + httpGet: + path: /total + port: http + periodSeconds: 15 + livenessProbe: + httpGet: + path: /total + port: http + periodSeconds: 30 diff --git a/k8s/base/ingress.yaml b/k8s/base/ingress.yaml new file mode 100644 index 0000000..871b15a --- /dev/null +++ b/k8s/base/ingress.yaml @@ -0,0 +1,24 @@ +# Cloudflare (proxied, TLS at the edge) -> ingress-nginx -> Service, like the +# rest of the swiss fleet. supply.send.it reaches the cluster through the +# proxied *.send.it wildcard once its DigitalOcean CNAME is removed. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: token-supply-api + labels: + app.kubernetes.io/name: token-supply-api + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "false" +spec: + ingressClassName: nginx + rules: + - host: supply.send.it + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: token-supply-api + port: + number: 80 diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml new file mode 100644 index 0000000..1d4f62e --- /dev/null +++ b/k8s/base/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml + - service.yaml + - ingress.yaml diff --git a/k8s/base/service.yaml b/k8s/base/service.yaml new file mode 100644 index 0000000..58de408 --- /dev/null +++ b/k8s/base/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: token-supply-api + labels: + app.kubernetes.io/name: token-supply-api +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: token-supply-api + ports: + - name: http + port: 80 + targetPort: http diff --git a/k8s/overlays/prod/externalsecret.yaml b/k8s/overlays/prod/externalsecret.yaml new file mode 100644 index 0000000..f3a925b --- /dev/null +++ b/k8s/overlays/prod/externalsecret.yaml @@ -0,0 +1,25 @@ +# RPC endpoints carry provider API keys, so they come from 1Password item +# `token-supply-api` in the sendapp-prod vault. +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: token-supply-api + labels: + app.kubernetes.io/name: token-supply-api +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: op-sendapp-prod + target: + name: token-supply-api + creationPolicy: Owner + data: + - secretKey: MAINNET_RPC_URL + remoteRef: + key: token-supply-api + property: mainnet-rpc-url + - secretKey: BASE_RPC_URL + remoteRef: + key: token-supply-api + property: base-rpc-url diff --git a/k8s/overlays/prod/kustomization.yaml b/k8s/overlays/prod/kustomization.yaml new file mode 100644 index 0000000..e0b5fee --- /dev/null +++ b/k8s/overlays/prod/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: token-supply-prod + +resources: + - namespace.yaml + - externalsecret.yaml + - ../../base + +images: + - name: ghcr.io/0xsend/token-supply-api + newTag: sha-REPLACE_WITH_MAIN_SHA diff --git a/k8s/overlays/prod/namespace.yaml b/k8s/overlays/prod/namespace.yaml new file mode 100644 index 0000000..70d7651 --- /dev/null +++ b/k8s/overlays/prod/namespace.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: token-supply-prod + labels: + # Injects the ghcr-credentials image pull secret (fleet convention). + ghcr-enabled: "true" + app.kubernetes.io/name: token-supply-api + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/enforce-version: latest