k8s/ci-cd: add forgejo gitops and argocd deployment

- Forgejo git forge + OCI registry
- Argo CD pull-based GitOps
- Private CA TLS (self-signed 10-year cert)
- Machine credentials scoped to repositories
This commit is contained in:
Story Crater Bot
2026-08-18 15:08:00 -07:00
parent 0af06b1239
commit a3f261f548
14 changed files with 738 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
# k8s/talos-ci-cd/argocd-values.yaml
# Argo CD — single-replica homelab install (fits the 4-CPU / 32 GB RAM budget).
# Deployed via helmfile (name=argocd) so it sits alongside Authentik, MinIO, etc.
#
# UI is exposed via the cluster Ingress (k8s/ingress/ingress.yaml,
# argocd.riotpiao.homelab.com) over the WireGuard/LAN-only nginx ingress —
# never as a LoadBalancer, since argocd-server holds cluster-admin
# credentials and that Ingress isn't reachable from the WAN.
#
# OIDC + RBAC declared in configs.cm / configs.rbac below — applied by Helm directly.
# Requires: oidc-secret K8s secret (created by helmfile argocd presync hook from env vars).
# CA trust for Forgejo repo clones injected into argocd-tls-certs-cm by postsync hook.
global:
domain: argocd.riotpiao.homelab.com
configs:
params:
server.insecure: false # keep TLS on argocd-server even behind the ingress
cm:
# Must match the Ingress host above and the redirect URI registered in
# Authentik (provision_oidc.py argocd_url) exactly — ArgoCD builds its
# OIDC redirect_uri as "{url}/auth/callback", so any mismatch here is
# what Authentik's "Invalid redirect URL" error means.
url: "https://argocd.riotpiao.homelab.com"
oidc.config: |
name: Authentik
issuer: https://authentik.riotpiao.homelab.com/application/o/argocd/
clientID: $oidc-secret:client-id
clientSecret: $oidc-secret:client-secret
requestedScopes: [openid, profile, email, groups]
rootCA: |
-----BEGIN CERTIFICATE-----
MIIBbTCCARSgAwIBAgIUNa409I6cGHye4YqeiphmWDaCEXUwCgYIKoZIzj0EAwIw
FTETMBEGA1UEAxMKaG9tZWxhYi1jYTAeFw0yNjA2MTcxNjUzMjVaFw0zNjA2MTQx
NjUzMjVaMBUxEzARBgNVBAMTCmhvbWVsYWItY2EwWTATBgcqhkjOPQIBBggqhkjO
PQMBBwNCAARwkubJPPdhgKcqr+3AEO2tr5I7MhC3zzeAqpmv8glngsweiGznaDhi
Dbf8JFfilbrLEJBSuwHZQPuoNx+3fbvYo0IwQDAOBgNVHQ8BAf8EBAMCAqQwDwYD
VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUod8iYq0+QyetnxfKDprIf3XbWkEwCgYI
KoZIzj0EAwIDRwAwRAIgQ4HOLs5DOqcfAMv8NSImxoYN7TyebnlQAQXSARnIqBMC
IB9RycFvG/rpJuz/LIKi4rf6RARjLcHM/zqhXQJvBw53
-----END CERTIFICATE-----
rbac:
policy.csv: |
g, homelab-admins, role:admin
policy.default: role:readonly
server:
replicas: 1
deploymentStrategy:
type: Recreate
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
# No Service of type LoadBalancer — port-forward only
service:
type: ClusterIP
podAnnotations:
secret.reloader.stakater.com/reload: "oidc-secret"
configmap.reloader.stakater.com/reload: "argocd-tls-certs-cm,argocd-cm"
metrics:
enabled: true
serviceMonitor:
enabled: true
repoServer:
replicas: 1
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
metrics:
enabled: true
serviceMonitor:
enabled: true
applicationSet:
replicas: 1
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
controller:
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
metrics:
enabled: true
serviceMonitor:
enabled: true
redis:
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
notifications:
enabled: false # add back later if alert routing is needed
@@ -0,0 +1,6 @@
apiVersion: v2
name: forgejo-runner
description: Forgejo Actions runner with Docker-in-Docker sidecar for homelab CI/CD
type: application
version: 0.1.0
appVersion: "6"
@@ -0,0 +1,100 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
namespace: {{ .Release.Namespace }}
labels:
app: {{ .Release.Name }}
spec:
replicas: 1
strategy:
type: Recreate # RWO PVCs — old pod must terminate before new one mounts them
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ .Release.Name }}
spec:
tolerations:
{{- toYaml .Values.tolerations | nindent 8 }}
initContainers:
- name: register
image: {{ .Values.runner.image.repository }}:{{ .Values.runner.image.tag }}
command: ["sh", "-c"]
args:
- |
test -f /data/.runner || forgejo-runner register --no-interactive \
--instance {{ .Values.runner.forgejoUrl }} \
--token $(RUNNER_TOKEN) \
--name {{ .Values.runner.name }} \
--labels "{{ .Values.runner.labels }}"
env:
- name: RUNNER_TOKEN
valueFrom:
secretKeyRef:
name: {{ .Values.runner.tokenSecret }}
key: token
volumeMounts:
- name: runner-data
mountPath: /data
- name: homelab-ca
mountPath: /etc/ssl/certs/homelab-ca.pem
subPath: ca.crt
workingDir: /data
containers:
- name: runner
image: {{ .Values.runner.image.repository }}:{{ .Values.runner.image.tag }}
command: ["sh", "-c", "forgejo-runner daemon"]
workingDir: /data
env:
- name: DOCKER_HOST
value: tcp://localhost:2376
- name: DOCKER_TLS_VERIFY
value: "1"
- name: DOCKER_CERT_PATH
value: /docker-certs/client
volumeMounts:
- name: runner-data
mountPath: /data
- name: docker-certs
mountPath: /docker-certs
- name: homelab-ca
mountPath: /etc/ssl/certs/homelab-ca.pem
subPath: ca.crt
resources:
{{- toYaml .Values.runner.resources | nindent 12 }}
- name: dind
image: {{ .Values.dind.image.repository }}:{{ .Values.dind.image.tag }}
securityContext:
privileged: true # required for DinD; cicd namespace is labelled privileged
env:
- name: DOCKER_TLS_CERTDIR
value: /docker-certs
volumeMounts:
- name: docker-certs
mountPath: /docker-certs
- name: dind-storage
mountPath: /var/lib/docker
- name: homelab-ca
mountPath: /etc/ssl/certs/homelab-ca.pem
subPath: ca.crt
resources:
{{- toYaml .Values.dind.resources | nindent 12 }}
volumes:
- name: runner-data
persistentVolumeClaim:
claimName: runner-reg
- name: dind-storage
persistentVolumeClaim:
claimName: runner-dind
- name: docker-certs
emptyDir: {} # DinD regenerates mTLS certs on each start
- name: homelab-ca
secret:
secretName: homelab-ca
@@ -0,0 +1,31 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ .Release.Name }}-egress
namespace: {{ .Release.Namespace }}
spec:
podSelector:
matchLabels:
app: {{ .Release.Name }}
policyTypes: [Egress]
egress:
# Forgejo — same cicd namespace (git push, registry push/pull)
- to:
- podSelector: {}
# CoreDNS
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Internet (action deps, base images) — never LAN or pod network
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 192.168.1.0/24
- 10.244.0.0/16
@@ -0,0 +1,27 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: runner-reg
namespace: {{ .Release.Namespace }}
labels:
app: {{ .Release.Name }}
spec:
accessModes: [ReadWriteOnce]
storageClassName: {{ .Values.persistence.reg.storageClass }}
resources:
requests:
storage: {{ .Values.persistence.reg.size }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: runner-dind
namespace: {{ .Release.Namespace }}
labels:
app: {{ .Release.Name }}
spec:
accessModes: [ReadWriteOnce]
storageClassName: {{ .Values.persistence.dind.storageClass }}
resources:
requests:
storage: {{ .Values.persistence.dind.size }}
@@ -0,0 +1,42 @@
runner:
image:
repository: code.forgejo.org/forgejo/runner
tag: "6" # pin exact release before apply
name: talos-runner
labels: "docker:docker://node:22-bookworm"
forgejoUrl: https://forgejo.riotpiao.homelab.com
# tokenSecret: name of the K8s Secret that holds the runner registration token
# created automatically by the helmfile presync hook (see helmfile.yaml.gotmpl)
tokenSecret: runner-token
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "2"
memory: 4Gi
dind:
image:
repository: docker
tag: "27-dind" # pin exact release before apply
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "2"
memory: 4Gi
persistence:
reg:
storageClass: longhorn
size: 1Gi # .runner registration file + config — survives pod restarts
dind:
storageClass: longhorn
size: 30Gi # docker layer cache — keeps rebuilds fast across restarts
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
@@ -0,0 +1,40 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: api
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
# CI bumps this tag on every push to main (ci.yml step "bump deploy repo")
image: forgejo.riotpiao.homelab.com/rock/api:latest
ports:
- containerPort: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 30
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: api
namespace: api
spec:
selector:
app: api
ports:
- name: http
port: 80
targetPort: 8080
@@ -0,0 +1,22 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: api
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "10"
spec:
project: default
source:
repoURL: https://forgejo.forge.riotpiao.homelab.com/rock/deploy.git
targetRevision: main
path: api
destination:
server: https://kubernetes.default.svc
namespace: api
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
@@ -0,0 +1,19 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: argocd
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
project: default
source:
repoURL: https://forgejo.forge.riotpiao.homelab.com/rock/deploy.git
targetRevision: main
path: argocd
destination:
server: https://kubernetes.default.svc
namespace: argocd
# NO syncPolicy.automated — manual sync required.
# Argo CD managing itself auto-synced is a footgun: a misconfigured commit could
# take down the CD system before anyone can intervene. Approve manually.
@@ -0,0 +1,19 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: forge
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
project: default
source:
repoURL: https://forgejo.forge.riotpiao.homelab.com/rock/deploy.git
targetRevision: main
path: forge
destination:
server: https://kubernetes.default.svc
namespace: forge
# NO syncPolicy.automated — manual sync required.
# Forgejo is what CI uses to push commits; auto-sync would let a bad CI commit
# break the very system CI depends on. Approve syncs manually in the Argo CD UI.
+81
View File
@@ -0,0 +1,81 @@
# .forgejo/workflows/ci.yml
#
# Copy to YOUR APPLICATION REPO at .forgejo/workflows/ci.yml
# (not this infra repo — this is a template).
#
# What this does (on every push to main):
# 1. Run tests — fail here and nothing ships.
# 2. Build a Docker image and push it to the Forgejo built-in OCI registry.
# 3. Clone rock/deploy, bump the image tag in api/deployment.yaml, push the commit.
# 4. Argo CD sees the commit within 3 minutes and rolls out the new version.
#
# Required repo secrets (Forgejo UI → repo → Settings → Actions → Secrets):
# REGISTRY_TOKEN — ci-bot's package:write Forgejo API token
# DEPLOY_TOKEN — ci-bot's repo:write Forgejo API token (scoped to rock/deploy only)
#
# Prerequisites:
# - ci-bot user created in Forgejo (see IAM section §11.5 of talos_version_control.html)
# - ci-bot added as collaborator on rock/deploy with Write access
# - Runner (cicd ns) is online and registered (Block 3 of build runbook)
# - Talos nodes trust the homelab CA (Block 2 of build runbook)
on:
push:
branches: [main]
jobs:
build-push-deploy:
runs-on: docker
steps:
- uses: actions/checkout@v4
# ── 1. Tests ────────────────────────────────────────────────────────────
- name: test
run: make test # replace with your test command; failure stops the pipeline
# ── 2. Build + push OCI image ───────────────────────────────────────────
- name: build and push image
env:
REGISTRY: forgejo.forge.riotpiao.homelab.com
OWNER: rock
run: |
REPO_NAME=${{ github.event.repository.name }}
TAG=$(git rev-parse --short HEAD)
IMAGE="${REGISTRY}/${OWNER}/${REPO_NAME}:${TAG}"
echo "${{ secrets.REGISTRY_TOKEN }}" \
| docker login "${REGISTRY}" -u "${OWNER}" --password-stdin
docker build -t "${IMAGE}" .
docker push "${IMAGE}"
# Pass values to subsequent steps
echo "TAG=${TAG}" >> "$GITHUB_ENV"
echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV"
echo "REPO_NAME=${REPO_NAME}" >> "$GITHUB_ENV"
# ── 3. Bump image tag in the deploy repo ────────────────────────────────
# This is the ONLY write operation CI has on the cluster side.
# Argo CD notices the commit and rolls out the new image.
- name: bump deploy repo
env:
REGISTRY: forgejo.forge.riotpiao.homelab.com
OWNER: rock
run: |
git clone \
"https://ci-bot:${{ secrets.DEPLOY_TOKEN }}@${REGISTRY}/${OWNER}/deploy.git" \
/tmp/deploy
TARGET_FILE="/tmp/deploy/${REPO_NAME}/deployment.yaml"
sed -i \
"s|${REGISTRY}/${OWNER}/${REPO_NAME}:.*|${IMAGE}|" \
"${TARGET_FILE}"
git -C /tmp/deploy \
-c user.name="ci-bot" \
-c user.email="[email protected]" \
commit -am "${REPO_NAME}: deploy ${TAG}"
git -C /tmp/deploy push
+151
View File
@@ -0,0 +1,151 @@
# k8s/talos-ci-cd/forgejo-values.yaml
# Forgejo deployed via the gitea-charts/gitea Helm chart with image override.
# Admin password injected via helmfile --set (FORGEJO_ADMIN_PASSWORD in .env).
# Runner is managed by a separate helmfile release (charts/forgejo-runner/).
#
# Chart docs: https://gitea.com/gitea/helm-chart
# ── Image (Forgejo replaces Gitea — drop-in compatible) ──────────────────────
image:
repository: codeberg.org/forgejo/forgejo
tag: "13" # pin exact release — check codeberg.org/forgejo/forgejo/releases
pullPolicy: IfNotPresent
# ── Bootstrap admin (provisioned by a post-install Job inside the chart) ──────
gitea:
admin:
username: rock
email: [email protected]
# password: injected via helmfile --set (FORGEJO_ADMIN_PASSWORD from .env)
config:
server:
PROTOCOL: http # nginx ingress handles TLS; pod serves plain HTTP
DOMAIN: forgejo.riotpiao.homelab.com
ROOT_URL: https://forgejo.riotpiao.homelab.com/
HTTP_PORT: 3000
START_SSH_SERVER: true
SSH_DOMAIN: forgejo.riotpiao.homelab.com
SSH_PORT: 2222
SSH_LISTEN_PORT: 2222
database:
DB_TYPE: sqlite3
PATH: /data/forgejo.db
repository:
ROOT: /data/git
actions:
ENABLED: true
packages:
ENABLED: true # built-in OCI registry
metrics:
ENABLED: true # Prometheus at /metrics
service:
DISABLE_REGISTRATION: true # no self-signup; Authentik OAuth2 auto-creates accounts
oauth2:
ENABLED: true
PROVIDER: openidconnect
OPENID_CONNECT_DISCOVERY_URL: https://authentik.riotpiao.homelab.com/application/o/forgejo/.well-known/openid-configuration
CLIENT_ID: forgejo
AUTO_DISCOVER_URL: https://authentik.riotpiao.homelab.com/application/o/forgejo/.well-known/openid-configuration
cache:
ADAPTER: memory # no Redis — single-replica SQLite setup
session:
PROVIDER: memory
queue:
TYPE: channel # in-memory queue; no file lock, no LevelDB contention on rollout
metrics:
enabled: true
serviceMonitor:
enabled: true # kube-prometheus-stack discovers ServiceMonitors cluster-wide
# ── Persistence (Longhorn RWO — SQLite lives here) ────────────────────────────
persistence:
enabled: true
storageClass: longhorn
size: 20Gi
accessModes:
- ReadWriteOnce
# ── Deployment strategy ────────────────────────────────────────────────────────
# RWO PVC + SQLite: old pod must terminate before new one mounts the volume.
deployment:
strategy:
type: Recreate
env:
- name: SSL_CERT_DIR
value: /homelab-ca
# ── Cert / CA auto-reload ─────────────────────────────────────────────────────
# nginx serves the wildcard-tls cert — Forgejo itself never reads a TLS secret.
# The only reload trigger is homelab-ca: if the root CA rotates, the mounted
# ConfigMap changes and Forgejo must restart to pick up the new CA bundle for OIDC.
podAnnotations:
configmap.reloader.stakater.com/reload: "homelab-ca"
# ── Services (Cilium LB-IPAM pins both to 192.168.1.165) ─────────────────────
service:
http:
type: LoadBalancer
port: 3000
targetPort: 3000
annotations:
io.cilium/lb-ipam-ips: "192.168.1.165"
io.cilium/lb-ipam-sharing-key: "forgejo"
ssh:
type: LoadBalancer
port: 2222
targetPort: 2222
annotations:
io.cilium/lb-ipam-ips: "192.168.1.165"
io.cilium/lb-ipam-sharing-key: "forgejo"
# ── Resources ─────────────────────────────────────────────────────────────────
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
# ── Node resilience ───────────────────────────────────────────────────────────
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
# ── CA trust ─────────────────────────────────────────────────────────────────
# Go reads SSL_CERT_DIR as an additional cert directory ON TOP OF the default
# cert files (ca-certificates.crt stays intact — no init container needed).
# Setting SSL_CERT_DIR=/homelab-ca makes Go also read homelab-ca.crt from there,
# trusting both the standard Mozilla bundle and our homelab CA.
# Required for OIDC: Forgejo fetches Authentik's discovery endpoint which
# presents a cert signed by homelab-ca.
extraVolumes:
- name: homelab-ca
configMap:
name: homelab-ca
extraVolumeMounts:
- name: homelab-ca
mountPath: /homelab-ca
readOnly: true
# ── Ingress: disabled — rule lives in k8s/ingress/ingress.yaml ───────────────
ingress:
enabled: false
# ── Bundled databases + cache: all disabled — SQLite + memory is the chosen backend ──
postgresql:
enabled: false
postgresql-ha:
enabled: false
mysql:
enabled: false
redis-cluster:
enabled: false # 6-node cluster is overkill for single-replica SQLite Forgejo
# ── Act runner subchart: disabled — managed by the forgejo-runner helmfile release
act_runner:
enabled: false
+49
View File
@@ -0,0 +1,49 @@
# 0. Source your env (Authentik bootstrap token + Vault addr)
set -a && source ~/.authentik/.env && set +a
VAULT_ADDR="http://vault.riotpiao.homelab.com"
AUTHENTIK_URL="http://authentik.riotpiao.homelab.com"
# 1. Fetch the real client_id + client_secret from Authentik
PROVIDER_PK=$(curl -s \
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
"${AUTHENTIK_URL}/api/v3/providers/oauth2/?name=talos-cli-shell" \
| python3 -c "import json,sys; r=json.load(sys.stdin)['results']; print(r[0]['pk'] if r else 'NOT_FOUND')")
echo "Provider PK: ${PROVIDER_PK}"
CLIENT_ID=$(curl -s \
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
"${AUTHENTIK_URL}/api/v3/providers/oauth2/${PROVIDER_PK}/" \
| python3 -c "import json,sys; p=json.load(sys.stdin); print(p['client_id'])")
CLIENT_SECRET=$(curl -s \
-H "Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" \
"${AUTHENTIK_URL}/api/v3/providers/oauth2/${PROVIDER_PK}/" \
| python3 -c "import json,sys; p=json.load(sys.stdin); print(p['client_secret'])")
echo "client_id: ${CLIENT_ID}"
# 2. Get a JWT from Authentik (client credentials flow)
# JWT=$(curl -s -X POST \
# "${AUTHENTIK_URL}/application/o/talos-cli-shell/token/" \
# -d "grant_type=client_credentials&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&scope=openid" \
# | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('access_token', d))")
# echo "JWT: ${JWT}..."
curl -v -s -X POST \
"${AUTHENTIK_URL}/application/o/homelab-mac-cli/token/" \
-d "grant_type=client_credentials&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&scope=openid"
# # 3. Exchange JWT for a Vault token
# VAULT_TOKEN=$(curl -s -X POST \
# "${VAULT_ADDR}/v1/auth/jwt/login" \
# -H "Content-Type: application/json" \
# -d "{\"jwt\": \"${JWT}\", \"role\": \"shell\"}" \
# | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('auth',{}).get('client_token', d))")
# echo "Vault token: ${VAULT_TOKEN:0:20}..."
# # 4. Verify the token works
# curl -s -H "X-Vault-Token: ${VAULT_TOKEN}" \
# "${VAULT_ADDR}/v1/auth/token/lookup-self" \
# | python3 -c "import json,sys; d=json.load(sys.stdin); print('policies:', d['data']['policies'])"