Add reuse service landing page
This commit is contained in:
@@ -17,9 +17,30 @@ app.kubernetes.io/name: {{ include "reuse.fullname" . }}
|
|||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "reuse.landingFullname" -}}
|
||||||
|
{{- printf "%s-landing" (include "reuse.fullname" .) | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "reuse.redirectMiddlewareName" -}}
|
||||||
|
{{- printf "%s-redirect-https" (include "reuse.fullname" .) | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "reuse.landingSelectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "reuse.fullname" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/component: landing
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
{{- define "reuse.image" -}}
|
{{- define "reuse.image" -}}
|
||||||
{{- if not .Values.image.tag -}}
|
{{- if not .Values.image.tag -}}
|
||||||
{{- fail "image.tag is required - pin it in helm/reuse-surface-values.yaml" -}}
|
{{- fail "image.tag is required - pin it in helm/reuse-surface-values.yaml" -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- printf "%s:%s" .Values.image.repository .Values.image.tag -}}
|
{{- printf "%s:%s" .Values.image.repository .Values.image.tag -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "reuse.landingImage" -}}
|
||||||
|
{{- if not .Values.landing.image.tag -}}
|
||||||
|
{{- fail "landing.image.tag is required when landing.enabled=true" -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- printf "%s:%s" .Values.landing.image.repository .Values.landing.image.tag -}}
|
||||||
|
{{- end -}}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ apiVersion: apps/v1
|
|||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ include "reuse.fullname" . }}
|
name: {{ include "reuse.fullname" . }}
|
||||||
labels: {{- include "reuse.labels" . | nindent 4 }}
|
labels:
|
||||||
|
{{- include "reuse.labels" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: api
|
||||||
spec:
|
spec:
|
||||||
replicas: {{ .Values.replicaCount }}
|
replicas: {{ .Values.replicaCount }}
|
||||||
selector:
|
selector:
|
||||||
@@ -14,7 +16,9 @@ spec:
|
|||||||
maxUnavailable: 0
|
maxUnavailable: 0
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels: {{- include "reuse.selectorLabels" . | nindent 8 }}
|
labels:
|
||||||
|
{{- include "reuse.selectorLabels" . | nindent 8 }}
|
||||||
|
app.kubernetes.io/component: api
|
||||||
spec:
|
spec:
|
||||||
enableServiceLinks: false
|
enableServiceLinks: false
|
||||||
securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }}
|
securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||||
|
|||||||
@@ -18,6 +18,24 @@ spec:
|
|||||||
- host: {{ .Values.ingress.host }}
|
- host: {{ .Values.ingress.host }}
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
|
{{- if .Values.landing.enabled }}
|
||||||
|
{{- range .Values.ingress.apiPaths }}
|
||||||
|
- path: {{ .path }}
|
||||||
|
pathType: {{ .pathType }}
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ include "reuse.fullname" $ }}
|
||||||
|
port:
|
||||||
|
number: {{ $.Values.service.port }}
|
||||||
|
{{- end }}
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ include "reuse.landingFullname" . }}
|
||||||
|
port:
|
||||||
|
number: {{ .Values.landing.service.port }}
|
||||||
|
{{- else }}
|
||||||
- path: /
|
- path: /
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
backend:
|
backend:
|
||||||
@@ -25,4 +43,5 @@ spec:
|
|||||||
name: {{ include "reuse.fullname" . }}
|
name: {{ include "reuse.fullname" . }}
|
||||||
port:
|
port:
|
||||||
number: {{ .Values.service.port }}
|
number: {{ .Values.service.port }}
|
||||||
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
135
charts/reuse-surface/templates/landing-configmap.yaml
Normal file
135
charts/reuse-surface/templates/landing-configmap.yaml
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{{- if .Values.landing.enabled }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: {{ include "reuse.landingFullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "reuse.labels" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: landing
|
||||||
|
data:
|
||||||
|
index.html: |
|
||||||
|
{{ if .Values.landing.html }}
|
||||||
|
{{ tpl .Values.landing.html . | nindent 4 }}
|
||||||
|
{{ else }}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
{{- if .Values.landing.noindex }}
|
||||||
|
<meta name="robots" content="noindex,nofollow">
|
||||||
|
{{- end }}
|
||||||
|
{{- if and .Values.landing.redirect.enabled .Values.landing.redirect.target }}
|
||||||
|
<meta http-equiv="refresh" content="{{ .Values.landing.redirect.delaySeconds }}; url={{ .Values.landing.redirect.target }}">
|
||||||
|
{{- end }}
|
||||||
|
<title>{{ .Values.landing.title }}</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--ink: #1d2733;
|
||||||
|
--muted: #536271;
|
||||||
|
--line: #d9e0e7;
|
||||||
|
--paper: #f8fafc;
|
||||||
|
--accent: #2066a8;
|
||||||
|
--accent-dark: #164b7e;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--paper);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 32px 18px;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
width: min(100%, 720px);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: clamp(28px, 5vw, 48px);
|
||||||
|
box-shadow: 0 18px 48px rgba(29, 39, 51, 0.08);
|
||||||
|
}
|
||||||
|
.eyebrow {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: var(--accent-dark);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(2rem, 7vw, 3rem);
|
||||||
|
line-height: 1.05;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
max-width: 62ch;
|
||||||
|
margin: 18px 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 28px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 44px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 0 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.button:focus,
|
||||||
|
.button:hover {
|
||||||
|
background: var(--accent-dark);
|
||||||
|
}
|
||||||
|
.links {
|
||||||
|
margin-top: 26px;
|
||||||
|
padding-top: 22px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<p class="eyebrow">{{ .Values.landing.eyebrow }}</p>
|
||||||
|
<h1>{{ .Values.landing.title }}</h1>
|
||||||
|
<p>{{ .Values.landing.body }}</p>
|
||||||
|
{{- $target := .Values.landing.primaryUrl }}
|
||||||
|
{{- if and .Values.landing.redirect.enabled .Values.landing.redirect.target }}
|
||||||
|
{{- $target = .Values.landing.redirect.target }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if $target }}
|
||||||
|
<div class="actions">
|
||||||
|
<a class="button" href="{{ $target }}">{{ .Values.landing.buttonLabel }}</a>
|
||||||
|
</div>
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.landing.links }}
|
||||||
|
<nav class="links" aria-label="Service links">
|
||||||
|
{{- range .Values.landing.links }}
|
||||||
|
<a href="{{ .url }}">{{ .label }}</a>
|
||||||
|
{{- end }}
|
||||||
|
</nav>
|
||||||
|
{{- end }}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
||||||
|
{{- end }}
|
||||||
59
charts/reuse-surface/templates/landing-deployment.yaml
Normal file
59
charts/reuse-surface/templates/landing-deployment.yaml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{{- if .Values.landing.enabled }}
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "reuse.landingFullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "reuse.labels" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: landing
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "reuse.landingSelectorLabels" . | nindent 6 }}
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
maxSurge: 1
|
||||||
|
maxUnavailable: 0
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{- include "reuse.landingSelectorLabels" . | nindent 8 }}
|
||||||
|
spec:
|
||||||
|
enableServiceLinks: false
|
||||||
|
containers:
|
||||||
|
- name: landing
|
||||||
|
image: {{ include "reuse.landingImage" . | quote }}
|
||||||
|
imagePullPolicy: {{ .Values.landing.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: {{ .Values.landing.service.targetPort }}
|
||||||
|
protocol: TCP
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 3
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 3
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 30
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 3
|
||||||
|
resources: {{- toYaml .Values.landing.resources | nindent 12 }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: landing-page
|
||||||
|
mountPath: /usr/share/nginx/html/index.html
|
||||||
|
subPath: index.html
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: landing-page
|
||||||
|
configMap:
|
||||||
|
name: {{ include "reuse.landingFullname" . }}
|
||||||
|
{{- end }}
|
||||||
18
charts/reuse-surface/templates/landing-service.yaml
Normal file
18
charts/reuse-surface/templates/landing-service.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{{- if .Values.landing.enabled }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "reuse.landingFullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "reuse.labels" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: landing
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
{{- include "reuse.landingSelectorLabels" . | nindent 4 }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: {{ .Values.landing.service.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
{{- end }}
|
||||||
23
charts/reuse-surface/templates/redirect-ingress.yaml
Normal file
23
charts/reuse-surface/templates/redirect-ingress.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{{- if and .Values.ingress.enabled .Values.ingress.redirectHttp.enabled }}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ include "reuse.fullname" . }}-http-redirect
|
||||||
|
labels: {{- include "reuse.labels" . | nindent 4 }}
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||||
|
traefik.ingress.kubernetes.io/router.middlewares: {{ printf "%s-%s@kubernetescrd" .Release.Namespace (include "reuse.redirectMiddlewareName" .) | quote }}
|
||||||
|
spec:
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
rules:
|
||||||
|
- host: {{ .Values.ingress.host }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ include "reuse.fullname" . }}
|
||||||
|
port:
|
||||||
|
number: {{ .Values.service.port }}
|
||||||
|
{{- end }}
|
||||||
11
charts/reuse-surface/templates/redirect-middleware.yaml
Normal file
11
charts/reuse-surface/templates/redirect-middleware.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{{- if and .Values.ingress.enabled .Values.ingress.redirectHttp.enabled }}
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: {{ include "reuse.redirectMiddlewareName" . }}
|
||||||
|
labels: {{- include "reuse.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
redirectScheme:
|
||||||
|
scheme: https
|
||||||
|
permanent: {{ .Values.ingress.redirectHttp.permanent }}
|
||||||
|
{{- end }}
|
||||||
@@ -5,7 +5,9 @@ metadata:
|
|||||||
labels: {{- include "reuse.labels" . | nindent 4 }}
|
labels: {{- include "reuse.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
type: {{ .Values.service.type }}
|
type: {{ .Values.service.type }}
|
||||||
selector: {{- include "reuse.selectorLabels" . | nindent 4 }}
|
selector:
|
||||||
|
{{- include "reuse.selectorLabels" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: api
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
port: {{ .Values.service.port }}
|
port: {{ .Values.service.port }}
|
||||||
|
|||||||
@@ -26,11 +26,48 @@ resources:
|
|||||||
|
|
||||||
envSecretName: reuse-surface-env
|
envSecretName: reuse-surface-env
|
||||||
|
|
||||||
|
landing:
|
||||||
|
enabled: false
|
||||||
|
image:
|
||||||
|
repository: nginxinc/nginx-unprivileged
|
||||||
|
tag: "1.27-alpine"
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
service:
|
||||||
|
port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
noindex: true
|
||||||
|
title: "Railiance service endpoint"
|
||||||
|
eyebrow: "Railiance S5"
|
||||||
|
body: "This endpoint is available for automated clients and operators."
|
||||||
|
buttonLabel: "Continue"
|
||||||
|
primaryUrl: ""
|
||||||
|
redirect:
|
||||||
|
enabled: false
|
||||||
|
target: ""
|
||||||
|
delaySeconds: 5
|
||||||
|
links: []
|
||||||
|
html: ""
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 32Mi
|
||||||
|
limits:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
className: traefik
|
className: traefik
|
||||||
host: reuse.coulomb.social
|
host: reuse.coulomb.social
|
||||||
tls: true
|
tls: true
|
||||||
|
redirectHttp:
|
||||||
|
enabled: false
|
||||||
|
permanent: true
|
||||||
|
apiPaths:
|
||||||
|
- path: /health
|
||||||
|
pathType: Exact
|
||||||
|
- path: /v1
|
||||||
|
pathType: Prefix
|
||||||
annotations:
|
annotations:
|
||||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||||
|
|||||||
@@ -42,8 +42,30 @@ export REUSE_SURFACE_URL=http://127.0.0.1:18001
|
|||||||
| Chart | `charts/reuse-surface` |
|
| Chart | `charts/reuse-surface` |
|
||||||
| Values | `helm/reuse-surface-values.yaml` |
|
| Values | `helm/reuse-surface-values.yaml` |
|
||||||
| Image | `gitea.coulomb.social/coulomb/reuse-surface:<tag>` |
|
| Image | `gitea.coulomb.social/coulomb/reuse-surface:<tag>` |
|
||||||
|
| Landing image | `nginxinc/nginx-unprivileged:1.27-alpine` |
|
||||||
| Secret | `reuse-surface-env` (`REUSE_SURFACE_TOKEN`) |
|
| Secret | `reuse-surface-env` (`REUSE_SURFACE_TOKEN`) |
|
||||||
|
|
||||||
|
## Browser landing page
|
||||||
|
|
||||||
|
`https://reuse.coulomb.social/` serves a static no-login landing page from the
|
||||||
|
Helm-managed `reuse-surface-landing` Deployment and Service. It exists for
|
||||||
|
humans who open the hostname in a browser; it does not change the API service.
|
||||||
|
|
||||||
|
Ingress routing is intentionally split:
|
||||||
|
|
||||||
|
- HTTP `/` redirects permanently to `https://reuse.coulomb.social/`;
|
||||||
|
- HTTPS `/health` and `/v1/*` route to `svc/reuse-surface`;
|
||||||
|
- HTTPS `/` and other non-API browser paths route to
|
||||||
|
`svc/reuse-surface-landing`.
|
||||||
|
|
||||||
|
The rendered page includes `noindex,nofollow`, a short service description, and
|
||||||
|
links to `/health`, `/v1/federated`, and this operator runbook. It must not
|
||||||
|
include `REUSE_SURFACE_TOKEN` or any other runtime secret.
|
||||||
|
|
||||||
|
Rollback: set `landing.enabled: false` in `helm/reuse-surface-values.yaml` and
|
||||||
|
run `KUBECONFIG=~/.kube/config-hosteurope make reuse-deploy`; the ingress will
|
||||||
|
return to routing all `/` traffic to the API service.
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -67,7 +89,10 @@ KUBECONFIG=~/.kube/config-hosteurope make reuse-status
|
|||||||
## Smoke checks
|
## Smoke checks
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
curl -I http://reuse.coulomb.social/
|
||||||
|
curl -k --resolve reuse.coulomb.social:443:92.205.62.239 https://reuse.coulomb.social/
|
||||||
curl -k --resolve reuse.coulomb.social:443:92.205.62.239 https://reuse.coulomb.social/health
|
curl -k --resolve reuse.coulomb.social:443:92.205.62.239 https://reuse.coulomb.social/health
|
||||||
|
curl -k --resolve reuse.coulomb.social:443:92.205.62.239 https://reuse.coulomb.social/v1/federated
|
||||||
|
|
||||||
export REUSE_SURFACE_TOKEN=$(KUBECONFIG=~/.kube/config-hosteurope kubectl get secret reuse-surface-env -n reuse \
|
export REUSE_SURFACE_TOKEN=$(KUBECONFIG=~/.kube/config-hosteurope kubectl get secret reuse-surface-env -n reuse \
|
||||||
-o jsonpath='{.data.REUSE_SURFACE_TOKEN}' | base64 -d)
|
-o jsonpath='{.data.REUSE_SURFACE_TOKEN}' | base64 -d)
|
||||||
|
|||||||
@@ -64,6 +64,40 @@ workplans first.
|
|||||||
headers to a value included in the app's allowed hosts.
|
headers to a value included in the app's allowed hosts.
|
||||||
- [ ] Keep readiness and liveness paths stable and unauthenticated.
|
- [ ] Keep readiness and liveness paths stable and unauthenticated.
|
||||||
|
|
||||||
|
## Endpoint Landing Pages
|
||||||
|
|
||||||
|
- [ ] Give every public S5 endpoint an intentional browser response at `/`.
|
||||||
|
- [ ] For API-only services, serve a static informational landing page at `/`
|
||||||
|
that states the service purpose, avoids any login claim, does not expose or
|
||||||
|
hint at runtime secrets, and links only to non-secret health/status or
|
||||||
|
operator documentation.
|
||||||
|
- [ ] For UI-backed services, a landing page may forward users to the canonical
|
||||||
|
login or application route, but it must also include a visible button for the
|
||||||
|
same destination.
|
||||||
|
- [ ] Preserve existing machine-facing paths. Health probes, API prefixes,
|
||||||
|
OAuth callbacks, and static asset routes must continue to reach their owning
|
||||||
|
backend after the landing page is enabled.
|
||||||
|
- [ ] For API-only endpoints, route explicit API/probe paths such as `/health`
|
||||||
|
and `/v1` to the API service, then use `/` as the landing fallback. Avoid
|
||||||
|
competing exact `/` and prefix `/` rules for different backends.
|
||||||
|
- [ ] Add `noindex` metadata for operator and service landing pages that are
|
||||||
|
not intended as public marketing pages.
|
||||||
|
|
||||||
|
Example UI-backed landing values:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
landing:
|
||||||
|
enabled: true
|
||||||
|
title: "Application sign-in"
|
||||||
|
eyebrow: "app.example.coulomb.social"
|
||||||
|
body: "You are being sent to the application sign-in page."
|
||||||
|
buttonLabel: "Continue to sign-in"
|
||||||
|
redirect:
|
||||||
|
enabled: true
|
||||||
|
target: "/login/"
|
||||||
|
delaySeconds: 5
|
||||||
|
```
|
||||||
|
|
||||||
## Validation And Smoke Tests
|
## Validation And Smoke Tests
|
||||||
|
|
||||||
- [ ] Run `make check-tools`.
|
- [ ] Run `make check-tools`.
|
||||||
@@ -73,8 +107,8 @@ workplans first.
|
|||||||
- [ ] Use the persistent-pod plus `kubectl exec` smoke pattern from
|
- [ ] Use the persistent-pod plus `kubectl exec` smoke pattern from
|
||||||
`docs/operator-recipes.md`.
|
`docs/operator-recipes.md`.
|
||||||
- [ ] Capture app-level deployment evidence: dry-run result, rollout status,
|
- [ ] Capture app-level deployment evidence: dry-run result, rollout status,
|
||||||
HTTPS or service smoke check, migration result when applicable, and rollback
|
HTTPS or service smoke check, landing-page check when enabled, migration
|
||||||
note.
|
result when applicable, and rollback note.
|
||||||
|
|
||||||
## Runbook Baseline
|
## Runbook Baseline
|
||||||
|
|
||||||
@@ -86,6 +120,7 @@ Each S5 app runbook should include:
|
|||||||
- day-to-day operator commands;
|
- day-to-day operator commands;
|
||||||
- image promotion steps;
|
- image promotion steps;
|
||||||
- rollback behavior and migration warning;
|
- rollback behavior and migration warning;
|
||||||
|
- public `/` landing-page behavior and the canonical login or API entrypoints;
|
||||||
- troubleshooting for probes, database URLs, TLS, and app-specific failure
|
- troubleshooting for probes, database URLs, TLS, and app-specific failure
|
||||||
modes;
|
modes;
|
||||||
- backup and restore readiness gate;
|
- backup and restore readiness gate;
|
||||||
|
|||||||
@@ -3,3 +3,22 @@
|
|||||||
|
|
||||||
image:
|
image:
|
||||||
tag: "cb7a6e4"
|
tag: "cb7a6e4"
|
||||||
|
|
||||||
|
landing:
|
||||||
|
enabled: true
|
||||||
|
title: "REUSE federation endpoint"
|
||||||
|
eyebrow: "reuse.coulomb.social"
|
||||||
|
body: "This is the Railiance REUSE capability federation service. It is an API endpoint for automated clients and operators; there is no browser login for this service."
|
||||||
|
buttonLabel: "Open federated index"
|
||||||
|
primaryUrl: "/v1/federated"
|
||||||
|
links:
|
||||||
|
- label: "Health check"
|
||||||
|
url: "/health"
|
||||||
|
- label: "Federated capability index"
|
||||||
|
url: "/v1/federated"
|
||||||
|
- label: "Operator runbook"
|
||||||
|
url: "https://gitea.coulomb.social/coulomb/railiance-apps/src/branch/main/docs/reuse-surface-on-railiance01.md"
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
redirectHttp:
|
||||||
|
enabled: true
|
||||||
|
|||||||
169
workplans/RAILIANCE-WP-0008-service-landing-pages.md
Normal file
169
workplans/RAILIANCE-WP-0008-service-landing-pages.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
---
|
||||||
|
id: RAILIANCE-WP-0008
|
||||||
|
type: workplan
|
||||||
|
title: "Add friendly landing pages for S5 service endpoints"
|
||||||
|
domain: railiance
|
||||||
|
repo: railiance-apps
|
||||||
|
status: finished
|
||||||
|
owner: codex
|
||||||
|
topic_slug: railiance
|
||||||
|
created: "2026-06-15"
|
||||||
|
updated: "2026-06-15"
|
||||||
|
state_hub_workstream_id: "41b9deea-a935-4372-8916-b3981336f597"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Add friendly landing pages for S5 service endpoints
|
||||||
|
|
||||||
|
Create a small, reusable landing-page pattern for public S5 application
|
||||||
|
endpoints. The page should give humans a clear next step without changing the
|
||||||
|
machine-facing API contract:
|
||||||
|
|
||||||
|
- UI-backed applications may show a friendly page with a short redirect notice
|
||||||
|
and a visible **Continue** button to the correct login or application route.
|
||||||
|
- API-only services should show a proper no-login informational page at `/`,
|
||||||
|
with links to health/status/runbook documentation where appropriate.
|
||||||
|
|
||||||
|
First rollout target: **`https://reuse.coulomb.social`**, which hosts the
|
||||||
|
`reuse-surface` federation API and intentionally has no browser login UI.
|
||||||
|
|
||||||
|
## Current Context
|
||||||
|
|
||||||
|
`RAILIANCE-WP-0007` deployed the `reuse-surface` Helm release and runbook for
|
||||||
|
`reuse.coulomb.social`. The release is useful to machines and operators, but a
|
||||||
|
human visiting `/` should not be left with an API-shaped response or a dead end.
|
||||||
|
|
||||||
|
The implementation must preserve the existing CLI/API surface:
|
||||||
|
|
||||||
|
- `/health` remains suitable for Kubernetes probes and external smoke checks.
|
||||||
|
- `/v1/*` API routes continue to reach the `reuse-surface` service.
|
||||||
|
- Authenticated federation operations continue to use `REUSE_SURFACE_TOKEN` and
|
||||||
|
must not expose token material in the landing page, chart values, or logs.
|
||||||
|
|
||||||
|
## Design Constraints
|
||||||
|
|
||||||
|
- Keep the page static and non-secret.
|
||||||
|
- Prefer explicit Ingress path routing over changing API semantics when a
|
||||||
|
service has no upstream web UI.
|
||||||
|
- Use exact `/` routing for the landing page when possible, so API prefixes are
|
||||||
|
unaffected.
|
||||||
|
- Add `noindex` metadata for operator/service landing pages that are not meant
|
||||||
|
to be public marketing surfaces.
|
||||||
|
- Keep the pattern reusable for later services with login destinations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Define Landing Page Contract
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0008-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "ca47efdb-ed0e-47bf-90f3-5a78c6861ebf"
|
||||||
|
```
|
||||||
|
|
||||||
|
Document the endpoint contract in `docs/s5-app-onboarding-checklist.md` or a
|
||||||
|
small companion doc:
|
||||||
|
|
||||||
|
- API-only service: static informational page at `/`, no login claim, no token
|
||||||
|
hints, links to health/status and operator runbook.
|
||||||
|
- UI-backed service: landing page may auto-forward after a short delay and must
|
||||||
|
include a visible button to the canonical login/application route.
|
||||||
|
- All variants must preserve health, API, OAuth callback, and asset paths.
|
||||||
|
|
||||||
|
Done when future S5 app workplans can cite a single landing-page rule instead
|
||||||
|
of rediscovering the behavior per service.
|
||||||
|
|
||||||
|
## Add Reusable Helm Support
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0008-T02
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "44dfe49f-a3f4-4381-a4ca-eb9218099868"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a reusable Helm pattern for a static landing page, either as shared snippets
|
||||||
|
or as chart-local templates following one documented convention. The pattern
|
||||||
|
should support:
|
||||||
|
|
||||||
|
- enable/disable flag in values;
|
||||||
|
- static HTML content from a ConfigMap;
|
||||||
|
- a lightweight static HTTP container or equivalent service;
|
||||||
|
- exact `/` Ingress path to the landing service;
|
||||||
|
- prefix routes for API and app paths to the existing backend;
|
||||||
|
- optional redirect target and button label for UI-backed applications.
|
||||||
|
|
||||||
|
Done when `helm template` can render both disabled and enabled landing-page
|
||||||
|
variants without changing non-landing deployments.
|
||||||
|
|
||||||
|
## Implement API-Only Landing Page For reuse.coulomb.social
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0008-T03
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "4e90315d-f0f0-49ca-b44f-533c23a44e96"
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable the landing-page pattern in `charts/reuse-surface` and
|
||||||
|
`helm/reuse-surface-values.yaml` for `reuse.coulomb.social`.
|
||||||
|
|
||||||
|
Content should make clear that this endpoint is the Railiance REUSE capability
|
||||||
|
federation service, not a login application. Include non-secret operator links
|
||||||
|
or text for:
|
||||||
|
|
||||||
|
- service purpose;
|
||||||
|
- `/health`;
|
||||||
|
- `/v1/federated`;
|
||||||
|
- `docs/reuse-surface-on-railiance01.md`;
|
||||||
|
- noindex metadata.
|
||||||
|
|
||||||
|
Done when a browser request to `/` receives the landing page while `/health`
|
||||||
|
and `/v1/federated` still reach the API service.
|
||||||
|
|
||||||
|
## Add Login-Forward Variant For UI Applications
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0008-T04
|
||||||
|
status: done
|
||||||
|
priority: medium
|
||||||
|
state_hub_task_id: "80c843b2-68d0-4f8a-aba6-39db1a2a6f70"
|
||||||
|
```
|
||||||
|
|
||||||
|
Prepare the values contract and at least one documented example for services
|
||||||
|
that do have a browser UI. The page should explain that the user is being sent
|
||||||
|
to the application and include a visible button to the configured login or
|
||||||
|
application route.
|
||||||
|
|
||||||
|
Done when a future UI-backed app can set only values such as `redirectTarget`,
|
||||||
|
`buttonLabel`, and explanatory copy, without editing templates.
|
||||||
|
|
||||||
|
## Verify, Deploy, And Update Runbook
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RAILIANCE-WP-0008-T05
|
||||||
|
status: done
|
||||||
|
priority: medium
|
||||||
|
state_hub_task_id: "0fc250a8-8fea-4c88-bd81-ad7ecf6143a1"
|
||||||
|
```
|
||||||
|
|
||||||
|
Render, deploy, and smoke-test the reuse landing page on railiance01:
|
||||||
|
|
||||||
|
- `make reuse-dry-run`;
|
||||||
|
- deploy with pinned image and non-secret landing values;
|
||||||
|
- verify `/`, `/health`, and `/v1/federated` over the public hostname;
|
||||||
|
- confirm no secret values appear in manifests or rendered HTML;
|
||||||
|
- update `docs/reuse-surface-on-railiance01.md` with the landing-page behavior
|
||||||
|
and rollback notes.
|
||||||
|
|
||||||
|
Done when the public browser experience is friendly and the existing
|
||||||
|
machine-facing API checks still pass.
|
||||||
|
|
||||||
|
Completed 2026-06-15: deployed Helm revision 5 to namespace `reuse`.
|
||||||
|
`reuse-surface-landing` is Running, HTTP `/` redirects to HTTPS, HTTPS `/`
|
||||||
|
returns `200 text/html`, `/health` returns `200 application/json`,
|
||||||
|
`/v1/federated` returns `200 application/json` with 12 capabilities, and the
|
||||||
|
fetched landing page contains no token references. Follow-up fix separated the
|
||||||
|
API and landing Service selectors with `app.kubernetes.io/component` labels and
|
||||||
|
changed ingress routing to explicit API paths (`/health`, `/v1`) plus landing
|
||||||
|
fallback `/`.
|
||||||
Reference in New Issue
Block a user