initial: action/site-publish @v1
This commit is contained in:
@@ -0,0 +1,107 @@
|
|||||||
|
# action/site-publish
|
||||||
|
|
||||||
|
Composite Gitea Action that publishes a website to the fritzlab k8s cluster.
|
||||||
|
Supports `static`, `hugo`, `mkdocs` (content → Garage S3 + ExternalName Service)
|
||||||
|
and `docker` (Dockerfile → Deployment + headless Service). Handles manifest
|
||||||
|
rendering, TLS via cert-manager, and Garage bucket aliases.
|
||||||
|
|
||||||
|
Renamed from `fritzlab/publish-site` → `action/site-publish` as part of the
|
||||||
|
2026 action-org consolidation.
|
||||||
|
|
||||||
|
## Convention
|
||||||
|
|
||||||
|
Bucket name = repo name = canonical domain. Sibling hostnames (e.g. `www.`,
|
||||||
|
`ipv6.`) are declared as `aliases:` in `site.yaml` — the action registers each
|
||||||
|
as a Garage `globalAlias` on the bucket and adds it to the Ingress + Certificate
|
||||||
|
on every deploy. Manual edits to manifests in the apps repo are clobbered;
|
||||||
|
edit `site.yaml` instead.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Scaffold a new site (handles repo creation + Garage bucket):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./new-site.sh --name my-site.vino.network --domain my-site.vino.network --type static
|
||||||
|
```
|
||||||
|
|
||||||
|
Or do it manually. `site.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
domain: my-site.vino.network
|
||||||
|
type: static # static | hugo | mkdocs | docker
|
||||||
|
# content_dir: html # subdirectory containing content (default: repo root)
|
||||||
|
# aliases: # additional hostnames (each gets a globalAlias on the bucket)
|
||||||
|
# - www.my-site.vino.network
|
||||||
|
# tidy: true # set false to skip HTML tidy
|
||||||
|
# enabled: true # set false to decommission
|
||||||
|
```
|
||||||
|
|
||||||
|
`.gitea/workflows/publish.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Publish
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: fritzlab
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: https://code.fritzlab.net/action/site-publish@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CI_BOT_TOKEN }}
|
||||||
|
s3-access-key: ${{ secrets.GARAGE_S3_ACCESS_KEY }}
|
||||||
|
s3-secret-key: ${{ secrets.GARAGE_S3_SECRET_KEY }}
|
||||||
|
garage-admin-token: ${{ secrets.GARAGE_ADMIN_TOKEN }}
|
||||||
|
```
|
||||||
|
|
||||||
|
DNS: subdomains of `vino.network` are covered by the wildcard CNAME to
|
||||||
|
`traefik.edge.svc…`. For other zones, add an explicit CNAME:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-site.fritzlab.net 300 IN CNAME traefik.edge.svc.k8s.sjc001.fritzlab.net.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
| Input | Required | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `token` | yes | | Gitea token for apps repo push |
|
||||||
|
| `s3-access-key` | static/hugo/mkdocs | | Garage `ci-deploy-key` access key id |
|
||||||
|
| `s3-secret-key` | static/hugo/mkdocs | | Garage `ci-deploy-key` secret key |
|
||||||
|
| `s3-endpoint` | no | `http://garage.storage.svc:3900` | Garage S3 endpoint |
|
||||||
|
| `garage-admin-token` | static/hugo/mkdocs (only if site has `aliases`) | | Garage admin API token (`admin-token` from `garage-rpc-secret` in `storage` ns) |
|
||||||
|
| `garage-admin-endpoint` | no | `http://garage.storage.svc:3903` | Garage admin API endpoint |
|
||||||
|
| `registry-password` | docker | inputs.token | Container registry password |
|
||||||
|
| `username` | no | `ci-bot` | Gitea username |
|
||||||
|
|
||||||
|
Org secrets in `websites`: `CI_BOT_TOKEN`, `GARAGE_S3_ACCESS_KEY`,
|
||||||
|
`GARAGE_S3_SECRET_KEY`, `GARAGE_ADMIN_TOKEN`.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
- **`new-site.sh`** — create a new site: Gitea repo, Garage bucket, web hosting enabled.
|
||||||
|
- **`scripts/publish.py decommission <site>`** — remove a site's manifests from apps repo. Bucket purge is manual.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
push to websites/<repo>
|
||||||
|
→ CI runs site-publish action
|
||||||
|
→ reads site.yaml, builds content (static copy / hugo / mkdocs), runs tidy
|
||||||
|
→ aws s3 sync → Garage bucket named after the repo
|
||||||
|
→ admin API: ensures every alias from site.yaml is a globalAlias on the bucket
|
||||||
|
→ renders manifests in fritzlab/apps from templates: ExternalName Service →
|
||||||
|
garage.storage.svc, Traefik Ingress (canonical + aliases), cert-manager
|
||||||
|
Certificate (canonical + aliases as SANs), kustomization
|
||||||
|
→ commits + pushes apps repo only if diff is non-empty
|
||||||
|
→ ArgoCD syncs → site live with TLS
|
||||||
|
```
|
||||||
|
|
||||||
|
The Ingress + Certificate are re-rendered on every deploy from `site.yaml`.
|
||||||
|
There is no "first-deploy vs. update" branching — every deploy is idempotent.
|
||||||
|
|
||||||
|
No nginx pods, no per-site Docker images. Garage matches `Host:` header to
|
||||||
|
bucket name (or any of its globalAliases), so every site shares a single
|
||||||
|
ExternalName target.
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
name: Publish Site
|
||||||
|
description: Build and deploy a site (static, hugo, mkdocs, or docker) to fritzlab k8s.
|
||||||
|
inputs:
|
||||||
|
token:
|
||||||
|
description: Gitea token (ci-bot) for apps repo push and API operations
|
||||||
|
required: true
|
||||||
|
s3-access-key:
|
||||||
|
description: Garage ci-deploy-key access key id (required for static/hugo/mkdocs)
|
||||||
|
required: false
|
||||||
|
s3-secret-key:
|
||||||
|
description: Garage ci-deploy-key secret access key (required for static/hugo/mkdocs)
|
||||||
|
required: false
|
||||||
|
registry-password:
|
||||||
|
description: Container registry password (required for docker type; defaults to token)
|
||||||
|
required: false
|
||||||
|
s3-endpoint:
|
||||||
|
description: Garage S3 endpoint URL
|
||||||
|
required: false
|
||||||
|
default: http://garage.storage.svc:3900
|
||||||
|
garage-admin-token:
|
||||||
|
description: Garage admin API token (required for static/hugo/mkdocs to reconcile bucket aliases)
|
||||||
|
required: false
|
||||||
|
garage-admin-endpoint:
|
||||||
|
description: Garage admin API endpoint URL
|
||||||
|
required: false
|
||||||
|
default: http://garage.storage.svc:3903
|
||||||
|
username:
|
||||||
|
description: Gitea username for git operations
|
||||||
|
required: false
|
||||||
|
default: ci-bot
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Setup
|
||||||
|
shell: bash
|
||||||
|
run: python3 ${{ github.action_path }}/scripts/setup.py
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
shell: bash
|
||||||
|
run: python3 ${{ github.action_path }}/scripts/publish.py build
|
||||||
|
env:
|
||||||
|
SITE_REPO: ${{ github.repository }}
|
||||||
|
SITE_DIR: ${{ github.workspace }}
|
||||||
|
ACTION_DIR: ${{ github.action_path }}
|
||||||
|
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
||||||
|
REGISTRY_PASSWORD: ${{ inputs.registry-password || inputs.token }}
|
||||||
|
CI_BOT_USER: ${{ inputs.username }}
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
shell: bash
|
||||||
|
run: python3 ${{ github.action_path }}/scripts/publish.py deploy
|
||||||
|
env:
|
||||||
|
SITE_REPO: ${{ github.repository }}
|
||||||
|
SITE_DIR: ${{ github.workspace }}
|
||||||
|
ACTION_DIR: ${{ github.action_path }}
|
||||||
|
CI_BOT_TOKEN: ${{ inputs.token }}
|
||||||
|
CI_BOT_USER: ${{ inputs.username }}
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ inputs.s3-access-key }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ inputs.s3-secret-key }}
|
||||||
|
AWS_DEFAULT_REGION: sjc001
|
||||||
|
GARAGE_S3_ENDPOINT: ${{ inputs.s3-endpoint }}
|
||||||
|
GARAGE_ADMIN_ENDPOINT: ${{ inputs.garage-admin-endpoint }}
|
||||||
|
GARAGE_ADMIN_TOKEN: ${{ inputs.garage-admin-token }}
|
||||||
|
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
||||||
Executable
+170
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# new-site.sh — Create a new static site: Gitea repo + Garage bucket.
|
||||||
|
#
|
||||||
|
# Usage: ./new-site.sh --name <name> --domain <domain> [--type static|hugo|mkdocs]
|
||||||
|
#
|
||||||
|
# Requires: tea CLI configured; kubectl with sjc001 context (for bucket setup).
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 --name <name> --domain <domain> [--type static|hugo|mkdocs]" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
NAME=""
|
||||||
|
DOMAIN=""
|
||||||
|
TYPE="static"
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--name) NAME="$2"; shift 2 ;;
|
||||||
|
--domain) DOMAIN="$2"; shift 2 ;;
|
||||||
|
--type) TYPE="$2"; shift 2 ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -z "$NAME" ] || [ -z "$DOMAIN" ] && usage
|
||||||
|
|
||||||
|
ORG="websites"
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||||
|
|
||||||
|
# --- Garage bucket setup (one-off, via in-cluster Job) ---
|
||||||
|
echo "Creating Garage bucket '${NAME}' and enabling web hosting..."
|
||||||
|
cat <<EOF | kubectl --context sjc001 -n storage apply -f - >/dev/null
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: bucket-setup-${NAME//./-}
|
||||||
|
namespace: storage
|
||||||
|
spec:
|
||||||
|
ttlSecondsAfterFinished: 120
|
||||||
|
backoffLimit: 0
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
containers:
|
||||||
|
- name: setup
|
||||||
|
image: alpine:3.20
|
||||||
|
command: [/bin/sh, -euc]
|
||||||
|
args:
|
||||||
|
- |
|
||||||
|
apk add --no-cache curl jq >/dev/null
|
||||||
|
H=http://garage.storage.svc:3903
|
||||||
|
AUTH="Authorization: Bearer \$TOKEN"
|
||||||
|
# ci-deploy-key id is fixed per cluster — update if re-keyed
|
||||||
|
CI_KID=GKbe265a2b8ea53695ae734787
|
||||||
|
BUCKET=${NAME}
|
||||||
|
# Create bucket (idempotent: 409 if exists)
|
||||||
|
curl -s -X POST -H "\$AUTH" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"globalAlias\":\"\$BUCKET\"}" "\$H/v2/CreateBucket" -o /dev/null
|
||||||
|
BID=\$(curl -sf -H "\$AUTH" "\$H/v2/GetBucketInfo?globalAlias=\$BUCKET" | jq -r .id)
|
||||||
|
echo "bucket id: \$BID"
|
||||||
|
curl -s -X POST -H "\$AUTH" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"bucketId\":\"\$BID\",\"accessKeyId\":\"\$CI_KID\",\"permissions\":{\"read\":true,\"write\":true,\"owner\":false}}" \
|
||||||
|
"\$H/v2/AllowBucketKey" -o /dev/null
|
||||||
|
curl -s -X POST -H "\$AUTH" -H "Content-Type: application/json" \
|
||||||
|
-d '{"websiteAccess":{"enabled":true,"indexDocument":"index.html","errorDocument":"404.html"}}' \
|
||||||
|
"\$H/v2/UpdateBucket?id=\$BID" -o /dev/null
|
||||||
|
curl -sf -H "\$AUTH" "\$H/v2/GetBucketInfo?globalAlias=\$BUCKET" | jq '{websiteAccess, keys: (.keys | map(.name))}'
|
||||||
|
env:
|
||||||
|
- name: TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: garage-rpc-secret
|
||||||
|
key: admin-token
|
||||||
|
EOF
|
||||||
|
kubectl --context sjc001 -n storage wait --for=condition=complete --timeout=60s job/bucket-setup-${NAME//./-}
|
||||||
|
kubectl --context sjc001 -n storage logs job/bucket-setup-${NAME//./-}
|
||||||
|
kubectl --context sjc001 -n storage delete job bucket-setup-${NAME//./-} --ignore-not-found >/dev/null
|
||||||
|
|
||||||
|
# --- Gitea repo ---
|
||||||
|
echo "Creating Gitea repo ${ORG}/${NAME}..."
|
||||||
|
tea repo create --owner "$ORG" --name "$NAME" --init
|
||||||
|
|
||||||
|
echo "Cloning..."
|
||||||
|
git clone "ssh://git@code.fritzlab.net/${ORG}/${NAME}.git" "$WORK_DIR"
|
||||||
|
cd "$WORK_DIR"
|
||||||
|
|
||||||
|
# --- Starter content + site.yaml ---
|
||||||
|
case "$TYPE" in
|
||||||
|
static)
|
||||||
|
mkdir -p html
|
||||||
|
cat > html/index.html <<'HTML'
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Welcome</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello from fritzlab</h1>
|
||||||
|
<p>Served from Garage S3.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML
|
||||||
|
cat > site.yaml <<YAML
|
||||||
|
domain: ${DOMAIN}
|
||||||
|
type: static
|
||||||
|
content_dir: html
|
||||||
|
YAML
|
||||||
|
;;
|
||||||
|
hugo)
|
||||||
|
hugo new site . --force
|
||||||
|
cat > site.yaml <<YAML
|
||||||
|
domain: ${DOMAIN}
|
||||||
|
type: hugo
|
||||||
|
YAML
|
||||||
|
;;
|
||||||
|
mkdocs)
|
||||||
|
cat > mkdocs.yml <<YAML
|
||||||
|
site_name: ${NAME}
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
YAML
|
||||||
|
mkdir -p docs
|
||||||
|
cat > docs/index.md <<MD
|
||||||
|
# ${NAME}
|
||||||
|
|
||||||
|
Welcome.
|
||||||
|
MD
|
||||||
|
cat > site.yaml <<YAML
|
||||||
|
domain: ${DOMAIN}
|
||||||
|
type: mkdocs
|
||||||
|
YAML
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- CI workflow ---
|
||||||
|
mkdir -p .gitea/workflows
|
||||||
|
cat > .gitea/workflows/publish.yaml <<'WORKFLOW'
|
||||||
|
name: Publish
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: fritzlab
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: https://code.fritzlab.net/fritzlab/publish-static-site@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CI_BOT_TOKEN }}
|
||||||
|
s3-access-key: ${{ secrets.GARAGE_S3_ACCESS_KEY }}
|
||||||
|
s3-secret-key: ${{ secrets.GARAGE_S3_SECRET_KEY }}
|
||||||
|
WORKFLOW
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git commit -m "Initial site scaffold"
|
||||||
|
git push
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Site created: ${ORG}/${NAME}"
|
||||||
|
echo "First build will trigger on push."
|
||||||
|
echo
|
||||||
|
echo "DNS: ${DOMAIN} is covered by the *.vino.network wildcard (→ traefik.edge)."
|
||||||
|
echo "For a domain outside vino.network, add an explicit CNAME:"
|
||||||
|
echo " ${DOMAIN} 300 IN CNAME traefik.edge.svc.k8s.sjc001.fritzlab.net."
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""Build phase — content prep (static) or docker image build."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from utils import EXCLUDE_FILES, die, env, parse_site_yaml, run
|
||||||
|
|
||||||
|
|
||||||
|
def build_static(site_dir, cfg):
|
||||||
|
build_dir = site_dir / "build"
|
||||||
|
html_dir = build_dir / "html"
|
||||||
|
|
||||||
|
if build_dir.exists():
|
||||||
|
shutil.rmtree(build_dir)
|
||||||
|
|
||||||
|
content_dir = cfg["content_dir"]
|
||||||
|
src = site_dir / content_dir if content_dir else site_dir
|
||||||
|
|
||||||
|
if cfg["type"] == "static":
|
||||||
|
print(f"Copying static content from {src}")
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
tmp_path = Path(tmp) / "html"
|
||||||
|
shutil.copytree(src, tmp_path, dirs_exist_ok=True)
|
||||||
|
for name in EXCLUDE_FILES:
|
||||||
|
p = tmp_path / name
|
||||||
|
if p.is_dir():
|
||||||
|
shutil.rmtree(p)
|
||||||
|
elif p.exists():
|
||||||
|
p.unlink()
|
||||||
|
build_dir.mkdir(parents=True)
|
||||||
|
shutil.move(str(tmp_path), str(html_dir))
|
||||||
|
|
||||||
|
elif cfg["type"] == "hugo":
|
||||||
|
print(f"Building Hugo site from {src}")
|
||||||
|
run(f"hugo --source {src} --destination {html_dir}")
|
||||||
|
|
||||||
|
elif cfg["type"] == "mkdocs":
|
||||||
|
print(f"Building MkDocs site from {src}")
|
||||||
|
run(f"cd {src} && mkdocs build -d {html_dir}")
|
||||||
|
|
||||||
|
if cfg.get("tidy", True):
|
||||||
|
print("Running tidy on HTML files...")
|
||||||
|
for html_file in html_dir.rglob("*.html"):
|
||||||
|
subprocess.run(
|
||||||
|
["tidy", "-modify", "-quiet",
|
||||||
|
"--wrap", "0", "--indent", "auto", "--indent-spaces", "2",
|
||||||
|
"--drop-empty-elements", "no", "--tidy-mark", "no",
|
||||||
|
"--show-warnings", "no", str(html_file)],
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Build complete — content at {html_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
def docker_login():
|
||||||
|
password = os.environ.get("REGISTRY_PASSWORD", "")
|
||||||
|
user = env("CI_BOT_USER", "ci-bot")
|
||||||
|
if not password:
|
||||||
|
die("REGISTRY_PASSWORD is required for docker builds")
|
||||||
|
subprocess.run(
|
||||||
|
f"echo '{password}' | docker login code.fritzlab.net -u {user} --password-stdin",
|
||||||
|
shell=True, check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_docker(site_dir, cfg):
|
||||||
|
docker_login()
|
||||||
|
image = cfg["image"]
|
||||||
|
run_number = env("GITHUB_RUN_NUMBER", "0")
|
||||||
|
tags = [f"{image}:latest", f"{image}:{run_number}"]
|
||||||
|
|
||||||
|
cmd_parts = ["docker", "build"]
|
||||||
|
for tag in tags:
|
||||||
|
cmd_parts += ["-t", tag]
|
||||||
|
cmd_parts += ["--network", "host", "--provenance=false"]
|
||||||
|
for key, val in cfg.get("build_args", {}).items():
|
||||||
|
cmd_parts += ["--build-arg", f"{key}={val}"]
|
||||||
|
cmd_parts.append(str(site_dir))
|
||||||
|
|
||||||
|
run(" ".join(cmd_parts))
|
||||||
|
print(f"Docker build complete — tags: {', '.join(tags)}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_build():
|
||||||
|
site_dir = Path(env("SITE_DIR"))
|
||||||
|
cfg = parse_site_yaml(site_dir)
|
||||||
|
|
||||||
|
if not cfg["enabled"]:
|
||||||
|
print("Site disabled — skipping build")
|
||||||
|
return
|
||||||
|
|
||||||
|
if cfg["type"] == "docker":
|
||||||
|
build_docker(site_dir, cfg)
|
||||||
|
else:
|
||||||
|
build_static(site_dir, cfg)
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
"""Deploy phase — S3 sync or docker push, manifest rendering, alias reconcile."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.error import HTTPError, URLError
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
from utils import (
|
||||||
|
DEFAULT_S3_ENDPOINT,
|
||||||
|
GITEA_HOST,
|
||||||
|
NAMESPACE,
|
||||||
|
clone_apps,
|
||||||
|
commit_and_push,
|
||||||
|
die,
|
||||||
|
env,
|
||||||
|
k8s_name,
|
||||||
|
parse_site_yaml,
|
||||||
|
render_templates,
|
||||||
|
run,
|
||||||
|
)
|
||||||
|
|
||||||
|
GARAGE_ADMIN_ENDPOINT = os.environ.get(
|
||||||
|
"GARAGE_ADMIN_ENDPOINT", "http://garage.storage.svc:3903"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CACHE_CONTROL = "public, max-age=0, must-revalidate"
|
||||||
|
|
||||||
|
|
||||||
|
def s3_sync(site_name, site_dir):
|
||||||
|
endpoint = os.environ.get("GARAGE_S3_ENDPOINT", DEFAULT_S3_ENDPOINT)
|
||||||
|
html_dir = site_dir / "build" / "html"
|
||||||
|
if not html_dir.exists():
|
||||||
|
die(f"build/html not found — did the build step run? ({html_dir})")
|
||||||
|
env("AWS_ACCESS_KEY_ID")
|
||||||
|
env("AWS_SECRET_ACCESS_KEY")
|
||||||
|
os.environ.setdefault("AWS_DEFAULT_REGION", "sjc001")
|
||||||
|
print(f"Syncing {html_dir} → s3://{site_name} via {endpoint}")
|
||||||
|
# `sync --delete` handles new/changed/orphaned files. `cp --recursive`
|
||||||
|
# then re-uploads everything to refresh metadata (cache-control,
|
||||||
|
# content-type) on objects sync skipped because nothing changed.
|
||||||
|
# Cost: a no-op deploy still re-uploads every byte. Sites here are
|
||||||
|
# small enough that that's free; correctness wins over throughput.
|
||||||
|
# AWS CLI guesses Content-Type from file extension on local→S3 uploads,
|
||||||
|
# so a fresh upload always carries the right MIME type.
|
||||||
|
run(
|
||||||
|
f"aws --endpoint-url {endpoint} s3 sync {html_dir}/ s3://{site_name}/ "
|
||||||
|
f"--delete --only-show-errors "
|
||||||
|
f"--cache-control '{CACHE_CONTROL}'"
|
||||||
|
)
|
||||||
|
print("Re-stamping metadata on all objects...")
|
||||||
|
run(
|
||||||
|
f"aws --endpoint-url {endpoint} s3 cp {html_dir}/ s3://{site_name}/ "
|
||||||
|
f"--recursive --only-show-errors "
|
||||||
|
f"--cache-control '{CACHE_CONTROL}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def garage_admin(method, path, token, body=None):
|
||||||
|
url = f"{GARAGE_ADMIN_ENDPOINT}{path}"
|
||||||
|
data = json.dumps(body).encode() if body is not None else None
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = Request(url, data=data, method=method, headers=headers)
|
||||||
|
with urlopen(req) as resp:
|
||||||
|
raw = resp.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_bucket_aliases(site_name, aliases, admin_token):
|
||||||
|
"""Add cfg['aliases'] as Garage globalAliases on the site bucket.
|
||||||
|
|
||||||
|
Idempotent: skips aliases already present. Never removes aliases not in
|
||||||
|
the desired set (safety — orphan removal is manual).
|
||||||
|
"""
|
||||||
|
if not aliases:
|
||||||
|
return
|
||||||
|
if not admin_token:
|
||||||
|
print(" (no GARAGE_ADMIN_TOKEN — skipping bucket alias reconcile)")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = garage_admin("GET", f"/v2/GetBucketInfo?globalAlias={site_name}",
|
||||||
|
admin_token)
|
||||||
|
except (HTTPError, URLError) as e:
|
||||||
|
print(f" WARNING: bucket lookup failed: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
bucket_id = info.get("id")
|
||||||
|
existing = set(info.get("globalAliases") or [])
|
||||||
|
print(f" Bucket {site_name} ({bucket_id[:12]}…) currently aliases: {sorted(existing)}")
|
||||||
|
|
||||||
|
for alias in aliases:
|
||||||
|
if alias in existing:
|
||||||
|
continue
|
||||||
|
print(f" Adding globalAlias: {alias}")
|
||||||
|
try:
|
||||||
|
garage_admin("POST", "/v2/AddBucketAlias", admin_token,
|
||||||
|
{"bucketId": bucket_id, "globalAlias": alias})
|
||||||
|
except HTTPError as e:
|
||||||
|
body = e.read().decode(errors="replace") if hasattr(e, "read") else ""
|
||||||
|
print(f" ERROR adding alias {alias}: {e} {body}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def docker_push(cfg):
|
||||||
|
image = cfg["image"]
|
||||||
|
run_number = env("GITHUB_RUN_NUMBER", "0")
|
||||||
|
for tag in [f"{image}:latest", f"{image}:{run_number}"]:
|
||||||
|
run(f"docker push {tag}")
|
||||||
|
|
||||||
|
|
||||||
|
def docker_tag_cleanup(cfg, token):
|
||||||
|
"""Keep the 3 newest numeric tags, delete the rest via Gitea API."""
|
||||||
|
image = cfg["image"]
|
||||||
|
parts = image.split("/")
|
||||||
|
if len(parts) < 3:
|
||||||
|
print(f"Cannot parse image for tag cleanup: {image}")
|
||||||
|
return
|
||||||
|
org = parts[1]
|
||||||
|
pkg_name = parts[2]
|
||||||
|
|
||||||
|
url = f"https://{GITEA_HOST}/api/v1/packages/{org}?type=container"
|
||||||
|
req = Request(url, headers={"Authorization": f"token {token}"})
|
||||||
|
try:
|
||||||
|
with urlopen(req) as resp:
|
||||||
|
packages = json.loads(resp.read())
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: failed to list packages for cleanup: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
numeric_versions = []
|
||||||
|
for pkg in packages:
|
||||||
|
if pkg.get("name") == pkg_name:
|
||||||
|
ver = pkg.get("version", "")
|
||||||
|
if ver.isdigit():
|
||||||
|
numeric_versions.append(int(ver))
|
||||||
|
|
||||||
|
numeric_versions.sort()
|
||||||
|
to_delete = numeric_versions[:-3] if len(numeric_versions) > 3 else []
|
||||||
|
|
||||||
|
for ver in to_delete:
|
||||||
|
del_url = f"https://{GITEA_HOST}/api/v1/packages/{org}/container/{pkg_name}/{ver}"
|
||||||
|
print(f" Deleting {pkg_name}:{ver}")
|
||||||
|
req = Request(del_url, method="DELETE", headers={"Authorization": f"token {token}"})
|
||||||
|
try:
|
||||||
|
urlopen(req)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Warning: failed to delete {pkg_name}:{ver}: {e}")
|
||||||
|
|
||||||
|
if to_delete:
|
||||||
|
print(f" Cleaned up {len(to_delete)} old tags")
|
||||||
|
else:
|
||||||
|
print(" No old tags to clean up")
|
||||||
|
|
||||||
|
|
||||||
|
def docker_cleanup():
|
||||||
|
run("docker system prune --all --force")
|
||||||
|
|
||||||
|
|
||||||
|
def render_site_manifests(site_name, action_dir, app_dir, manifests_dir, cfg, extra=None):
|
||||||
|
"""Always re-render manifests from current site.yaml. Templates own
|
||||||
|
domain + aliases, so changes propagate without manual edits."""
|
||||||
|
manifests_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
template_vars = {
|
||||||
|
"site": site_name,
|
||||||
|
"site_k8s": k8s_name(site_name),
|
||||||
|
"domain": cfg["domain"],
|
||||||
|
"aliases": cfg["aliases"],
|
||||||
|
"namespace": NAMESPACE,
|
||||||
|
"site_type": cfg["type"],
|
||||||
|
}
|
||||||
|
if extra:
|
||||||
|
template_vars.update(extra)
|
||||||
|
render_templates(action_dir, template_vars, app_dir, manifests_dir, cfg["type"])
|
||||||
|
|
||||||
|
|
||||||
|
def deploy_static(site_name, site_dir, action_dir, token, cfg):
|
||||||
|
s3_sync(site_name, site_dir)
|
||||||
|
ensure_bucket_aliases(site_name, cfg["aliases"], os.environ.get("GARAGE_ADMIN_TOKEN"))
|
||||||
|
|
||||||
|
apps_dir = clone_apps(token)
|
||||||
|
app_dir = apps_dir / "sjc001" / "websites" / site_name
|
||||||
|
manifests_dir = app_dir / "manifests"
|
||||||
|
|
||||||
|
render_site_manifests(site_name, action_dir, app_dir, manifests_dir, cfg)
|
||||||
|
|
||||||
|
commit_and_push(apps_dir, f"Deploy {site_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def deploy_docker(site_name, site_dir, action_dir, token, cfg):
|
||||||
|
docker_push(cfg)
|
||||||
|
|
||||||
|
run_number = env("GITHUB_RUN_NUMBER", "0")
|
||||||
|
image = cfg["image"]
|
||||||
|
apps_dir = clone_apps(token)
|
||||||
|
app_dir = apps_dir / "sjc001" / "websites" / site_name
|
||||||
|
manifests_dir = app_dir / "manifests"
|
||||||
|
|
||||||
|
render_site_manifests(site_name, action_dir, app_dir, manifests_dir, cfg, extra={
|
||||||
|
"image": image,
|
||||||
|
"port": cfg["port"],
|
||||||
|
"health_path": cfg["health_path"],
|
||||||
|
"replicas": cfg["replicas"],
|
||||||
|
})
|
||||||
|
# Pin to this build's tag (kustomize edit appends an `images:` override).
|
||||||
|
run(f"cd {manifests_dir} && kustomize edit set image {image}={image}:{run_number}")
|
||||||
|
|
||||||
|
commit_and_push(apps_dir, f"Deploy {site_name} #{run_number}")
|
||||||
|
|
||||||
|
docker_tag_cleanup(cfg, token)
|
||||||
|
docker_cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def decommission(site_name, token):
|
||||||
|
"""Remove manifests from apps repo."""
|
||||||
|
user = env("CI_BOT_USER", "ci-bot")
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
apps_dir = Path(tmp)
|
||||||
|
run(f"git clone --depth 1 https://{user}:{token}@{GITEA_HOST}/fritzlab/apps.git {apps_dir}")
|
||||||
|
site_path = apps_dir / "sjc001" / "websites" / site_name
|
||||||
|
if not site_path.exists():
|
||||||
|
print(f"No manifests for {site_name} — nothing to remove")
|
||||||
|
return
|
||||||
|
shutil.rmtree(site_path)
|
||||||
|
run(f"git -C {apps_dir} config user.name {user}")
|
||||||
|
run(f"git -C {apps_dir} config user.email {user}@fritzlab.net")
|
||||||
|
commit_and_push(apps_dir, f"Decommission {site_name}")
|
||||||
|
print(f"Bucket {site_name} and its objects are NOT purged automatically.")
|
||||||
|
print(f" garage bucket delete {site_name} --yes")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_deploy():
|
||||||
|
site_repo = env("SITE_REPO")
|
||||||
|
site_dir = Path(env("SITE_DIR"))
|
||||||
|
action_dir = Path(env("ACTION_DIR"))
|
||||||
|
token = env("CI_BOT_TOKEN")
|
||||||
|
site_name = site_repo.split("/", 1)[1]
|
||||||
|
|
||||||
|
cfg = parse_site_yaml(site_dir)
|
||||||
|
|
||||||
|
if not cfg["enabled"]:
|
||||||
|
print("Site disabled — running decommission...")
|
||||||
|
decommission(site_name, token)
|
||||||
|
return
|
||||||
|
|
||||||
|
if cfg["type"] == "docker":
|
||||||
|
deploy_docker(site_name, site_dir, action_dir, token, cfg)
|
||||||
|
else:
|
||||||
|
deploy_static(site_name, site_dir, action_dir, token, cfg)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Entry point for publish-site action.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 publish.py build
|
||||||
|
python3 publish.py deploy
|
||||||
|
python3 publish.py decommission <site-name>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from utils import die
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
die("Usage: publish.py {build|deploy|decommission}")
|
||||||
|
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
|
||||||
|
if cmd == "build":
|
||||||
|
from build import cmd_build
|
||||||
|
cmd_build()
|
||||||
|
elif cmd == "deploy":
|
||||||
|
from deploy import cmd_deploy
|
||||||
|
cmd_deploy()
|
||||||
|
elif cmd == "decommission":
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
die("Usage: publish.py decommission <site-name>")
|
||||||
|
from deploy import decommission
|
||||||
|
token = os.environ.get("GITEA_TOKEN") or os.environ.get("CI_BOT_TOKEN")
|
||||||
|
if not token:
|
||||||
|
die("Set GITEA_TOKEN or CI_BOT_TOKEN")
|
||||||
|
decommission(sys.argv[2], token)
|
||||||
|
else:
|
||||||
|
die(f"Unknown command: {cmd}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""Ensure required CLI tools are installed."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_aws():
|
||||||
|
if shutil.which("aws"):
|
||||||
|
subprocess.run(["aws", "--version"], check=True)
|
||||||
|
return
|
||||||
|
print("Installing awscli...")
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, "-m", "pip", "install", "--quiet", "--break-system-packages", "awscli"],
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
local_bin = os.path.expanduser("~/.local/bin")
|
||||||
|
os.environ["PATH"] = f"{local_bin}:{os.environ['PATH']}"
|
||||||
|
github_path = os.environ.get("GITHUB_PATH")
|
||||||
|
if github_path:
|
||||||
|
with open(github_path, "a") as f:
|
||||||
|
f.write(f"{local_bin}\n")
|
||||||
|
subprocess.run(["aws", "--version"], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_docker():
|
||||||
|
if shutil.which("docker"):
|
||||||
|
subprocess.run(["docker", "version", "--format", "{{.Client.Version}}"], check=True)
|
||||||
|
return
|
||||||
|
print("WARNING: docker CLI not found — docker builds will fail", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_jinja2():
|
||||||
|
try:
|
||||||
|
import jinja2
|
||||||
|
except ImportError:
|
||||||
|
print("Installing jinja2 + pyyaml...")
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, "-m", "pip", "install", "--quiet", "--break-system-packages", "jinja2", "pyyaml"],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ensure_jinja2()
|
||||||
|
ensure_aws()
|
||||||
|
ensure_docker()
|
||||||
|
print("Setup complete")
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
"""Shared utilities for the publish-site action."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
APPS_REPO = "fritzlab/apps"
|
||||||
|
GITEA_HOST = "code.fritzlab.net"
|
||||||
|
NAMESPACE = "websites"
|
||||||
|
DEFAULT_S3_ENDPOINT = "http://garage.storage.svc:3900"
|
||||||
|
|
||||||
|
EXCLUDE_FILES = {
|
||||||
|
".git", ".gitea", ".gitignore", "site.yaml",
|
||||||
|
"build", "Makefile", "README.md", "CLAUDE.md",
|
||||||
|
"Dockerfile", ".dockerignore", "go.mod", "go.sum",
|
||||||
|
}
|
||||||
|
|
||||||
|
VALID_TYPES = {"static", "hugo", "mkdocs", "docker"}
|
||||||
|
|
||||||
|
|
||||||
|
def k8s_name(name):
|
||||||
|
"""Sanitize for DNS-1035 label (dots → dashes)."""
|
||||||
|
return name.replace(".", "-")
|
||||||
|
|
||||||
|
|
||||||
|
def env(key, default=None):
|
||||||
|
val = os.environ.get(key, default)
|
||||||
|
if val is None:
|
||||||
|
die(f"Missing required env var: {key}")
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def die(msg):
|
||||||
|
print(f"ERROR: {msg}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd, **kwargs):
|
||||||
|
print(f" $ {cmd}")
|
||||||
|
return subprocess.run(cmd, shell=True, check=True, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_site_yaml(site_dir):
|
||||||
|
path = Path(site_dir) / "site.yaml"
|
||||||
|
if not path.exists():
|
||||||
|
die("site.yaml not found in repo root")
|
||||||
|
|
||||||
|
with open(path) as f:
|
||||||
|
cfg = yaml.safe_load(f)
|
||||||
|
|
||||||
|
if not cfg.get("domain"):
|
||||||
|
die("domain is required in site.yaml")
|
||||||
|
|
||||||
|
site_type = cfg.get("type", "static")
|
||||||
|
if site_type not in VALID_TYPES:
|
||||||
|
die(f"Unknown site type: {site_type} (valid: {', '.join(sorted(VALID_TYPES))})")
|
||||||
|
|
||||||
|
site = {
|
||||||
|
"domain": cfg["domain"],
|
||||||
|
"type": site_type,
|
||||||
|
"enabled": cfg.get("enabled", True),
|
||||||
|
"aliases": cfg.get("aliases") or [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if site_type == "docker":
|
||||||
|
if not cfg.get("image"):
|
||||||
|
die("image is required in site.yaml for type: docker")
|
||||||
|
site["image"] = cfg["image"]
|
||||||
|
site["port"] = cfg.get("port", 8080)
|
||||||
|
site["build_args"] = cfg.get("build_args") or {}
|
||||||
|
site["health_path"] = cfg.get("health_path", "/healthz")
|
||||||
|
site["replicas"] = cfg.get("replicas", 1)
|
||||||
|
else:
|
||||||
|
site["content_dir"] = cfg.get("content_dir", "")
|
||||||
|
site["tidy"] = cfg.get("tidy", True)
|
||||||
|
|
||||||
|
print("Site config:")
|
||||||
|
for k, v in site.items():
|
||||||
|
print(f" {k}: {v}")
|
||||||
|
return site
|
||||||
|
|
||||||
|
|
||||||
|
def clone_apps(token):
|
||||||
|
user = env("CI_BOT_USER", "ci-bot")
|
||||||
|
apps_dir = Path("/tmp/apps-deploy")
|
||||||
|
if apps_dir.exists():
|
||||||
|
shutil.rmtree(apps_dir)
|
||||||
|
run(f"git clone --depth 1 https://{user}:{token}@{GITEA_HOST}/{APPS_REPO}.git {apps_dir}")
|
||||||
|
run(f"git -C {apps_dir} config user.name {user}")
|
||||||
|
run(f"git -C {apps_dir} config user.email {user}@fritzlab.net")
|
||||||
|
return apps_dir
|
||||||
|
|
||||||
|
|
||||||
|
def render_templates(action_dir, template_vars, app_dir, manifests_dir, site_type):
|
||||||
|
"""Render Jinja2 templates, selecting the right set for the site type."""
|
||||||
|
templates_dir = Path(action_dir) / "templates"
|
||||||
|
jinja_env = Environment(
|
||||||
|
loader=FileSystemLoader(str(templates_dir)),
|
||||||
|
keep_trailing_newline=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if site_type == "docker":
|
||||||
|
service_tmpl = "service-docker.yaml.j2"
|
||||||
|
tmpl_names = ["app.yaml.j2", "certificate.yaml.j2", "ingress.yaml.j2",
|
||||||
|
"kustomization.yaml.j2", service_tmpl, "deployment.yaml.j2"]
|
||||||
|
else:
|
||||||
|
service_tmpl = "service-static.yaml.j2"
|
||||||
|
tmpl_names = ["app.yaml.j2", "certificate.yaml.j2", "ingress.yaml.j2",
|
||||||
|
"kustomization.yaml.j2", service_tmpl]
|
||||||
|
|
||||||
|
for tmpl_name in tmpl_names:
|
||||||
|
tmpl = jinja_env.get_template(tmpl_name)
|
||||||
|
rendered = tmpl.render(**template_vars)
|
||||||
|
out_name = tmpl_name.replace(".j2", "")
|
||||||
|
# service-docker.yaml / service-static.yaml → service.yaml
|
||||||
|
if out_name.startswith("service-"):
|
||||||
|
out_name = "service.yaml"
|
||||||
|
dest = app_dir / out_name if tmpl_name == "app.yaml.j2" else manifests_dir / out_name
|
||||||
|
dest.write_text(rendered)
|
||||||
|
print(f" Rendered {tmpl_name} -> {dest}")
|
||||||
|
|
||||||
|
|
||||||
|
def commit_and_push(apps_dir, message):
|
||||||
|
run(f"git -C {apps_dir} add -A")
|
||||||
|
result = subprocess.run(
|
||||||
|
f"git -C {apps_dir} diff --cached --quiet",
|
||||||
|
shell=True, check=False,
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("No manifest changes to commit")
|
||||||
|
return False
|
||||||
|
run(f"git -C {apps_dir} commit -m '{message}'")
|
||||||
|
run(f"git -C {apps_dir} push")
|
||||||
|
print("Manifests pushed — ArgoCD will sync")
|
||||||
|
return True
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: {{ site }}
|
||||||
|
namespace: argocd
|
||||||
|
finalizers:
|
||||||
|
- resources-finalizer.argocd.argoproj.io
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: ssh://git@code.fritzlab.net/fritzlab/apps.git
|
||||||
|
targetRevision: main
|
||||||
|
path: sjc001/websites/{{ site }}/manifests
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: {{ namespace }}
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: {{ site_k8s }}-tls
|
||||||
|
namespace: {{ namespace }}
|
||||||
|
spec:
|
||||||
|
secretName: {{ site_k8s }}-tls
|
||||||
|
issuerRef:
|
||||||
|
name: letsencrypt
|
||||||
|
kind: ClusterIssuer
|
||||||
|
dnsNames:
|
||||||
|
- {{ domain }}
|
||||||
|
{%- for alias in aliases %}
|
||||||
|
- {{ alias }}
|
||||||
|
{%- endfor %}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ site_k8s }}
|
||||||
|
namespace: {{ namespace }}
|
||||||
|
labels:
|
||||||
|
app: {{ site_k8s }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ replicas }}
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: {{ site_k8s }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: {{ site_k8s }}
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 65532
|
||||||
|
runAsGroup: 65532
|
||||||
|
fsGroup: 65532
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: {{ site_k8s }}
|
||||||
|
image: {{ image }}:latest
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: {{ port }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 64Mi
|
||||||
|
cpu: 50m
|
||||||
|
limits:
|
||||||
|
memory: 256Mi
|
||||||
|
livenessProbe:
|
||||||
|
httpGet: {path: {{ health_path }}, port: http}
|
||||||
|
periodSeconds: 30
|
||||||
|
timeoutSeconds: 3
|
||||||
|
readinessProbe:
|
||||||
|
httpGet: {path: {{ health_path }}, port: http}
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 3
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ site_k8s }}
|
||||||
|
namespace: {{ namespace }}
|
||||||
|
{%- if site_type != "docker" %}
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.middlewares: retry-upstream@file
|
||||||
|
{%- endif %}
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- {{ domain }}
|
||||||
|
{%- for alias in aliases %}
|
||||||
|
- {{ alias }}
|
||||||
|
{%- endfor %}
|
||||||
|
secretName: {{ site_k8s }}-tls
|
||||||
|
rules:
|
||||||
|
- host: {{ domain }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ site_k8s }}
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
{%- for alias in aliases %}
|
||||||
|
- host: {{ alias }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ site_k8s }}
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
{%- endfor %}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
{% if site_type == "docker" %}
|
||||||
|
- deployment.yaml
|
||||||
|
{% endif %}
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
||||||
|
- certificate.yaml
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ site_k8s }}
|
||||||
|
namespace: {{ namespace }}
|
||||||
|
spec:
|
||||||
|
clusterIP: None
|
||||||
|
selector:
|
||||||
|
app: {{ site_k8s }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: http
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ site_k8s }}
|
||||||
|
namespace: {{ namespace }}
|
||||||
|
spec:
|
||||||
|
type: ExternalName
|
||||||
|
externalName: garage.storage.svc.k8s.sjc001.fritzlab.net
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
Reference in New Issue
Block a user