diff --git a/.gitea/workflows/build-and-push.yaml b/.gitea/workflows/build-and-push.yaml new file mode 100644 index 0000000..18b8bcb --- /dev/null +++ b/.gitea/workflows/build-and-push.yaml @@ -0,0 +1,107 @@ +name: Talks slides — image & chart + +# Déclenché uniquement si l’image Docker ou le chart Helm change (évite les builds sur contenu slides seul). +on: + push: + branches: [main, develop, cicd] + tags: ['v*'] + paths: + - 'server/**' + - 'talks-slides-dist/**' + pull_request: + branches: [main, develop] + paths: + - 'server/**' + - 'talks-slides-dist/**' + +env: + GIT_DEFAULT_HASH: sha256 + DOCKER_HOST: 'unix:///var/run/docker.sock' + DOCKER_CERT_PATH: '/certs/client' + IMAGE_REGISTRY: git.specificat.io + IMAGE_NAME: specificat.io/talks-slides + # Même registre OCI Helm que les autres charts (ex. knowledge-mcp). + HELM_OCI_REPOSITORY: oci://git.specificat.io/specificat.io + HELM_DIR: ./talks-slides-dist + +jobs: + vars: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.out.outputs.version }} + steps: + - id: out + env: + RUN_NUMBER: ${{ github.run_number }} + REF_NAME: ${{ github.ref_name }} + run: | + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + echo "version=${REF_NAME#v}" >> $GITHUB_OUTPUT + else + echo "version=0.0.${RUN_NUMBER}" >> $GITHUB_OUTPUT + fi + + build-image: + runs-on: ubuntu-latest + name: Build container image + needs: vars + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + if: github.event_name == 'push' + with: + registry: ${{ env.IMAGE_REGISTRY }} + username: ${{ secrets.CI_USER }} + password: ${{ secrets.CI_TOKEN }} + - name: Build and push image + if: github.event_name == 'push' + env: + VERSION: ${{ needs.vars.outputs.version }} + run: | + set -euo pipefail + IMAGE="${IMAGE_REGISTRY}/${IMAGE_NAME}:${VERSION}" + docker build -f server/Dockerfile -t "${IMAGE}" server/ + docker push "${IMAGE}" + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + docker tag "${IMAGE}" "${IMAGE_REGISTRY}/${IMAGE_NAME}:latest" + docker push "${IMAGE_REGISTRY}/${IMAGE_NAME}:latest" + fi + - name: Build image (PR, no push) + if: github.event_name == 'pull_request' + run: | + set -euo pipefail + docker build -f server/Dockerfile -t talks-slides:ci server/ + + helm: + runs-on: ubuntu-latest + name: Helm chart + needs: vars + steps: + - uses: actions/checkout@v4 + - name: Install Helm + run: | + set -euo pipefail + curl -fsSL -o helm.tgz --user "${{ secrets.CI_USER }}:${{ secrets.CI_TOKEN }}" \ + "https://git.specificat.io/api/packages/Specificat.io/generic/helm/4.1.1/helm-v4.1.1-linux-amd64.tar.gz" + tar -xzf helm.tgz + sudo mv linux-amd64/helm /usr/local/bin/helm + helm version + - name: Helm lint & template + env: + VERSION: ${{ needs.vars.outputs.version }} + run: | + set -euo pipefail + helm lint "${HELM_DIR}" + helm template talks-slides "${HELM_DIR}" --set slides.image.tag="${VERSION}" + - name: Package and push chart (OCI) + if: github.event_name == 'push' + env: + VERSION: ${{ needs.vars.outputs.version }} + run: | + set -euo pipefail + helm package "${HELM_DIR}" --version "${VERSION}" --app-version "${VERSION}" + helm registry login "${IMAGE_REGISTRY}" -u "${{ secrets.CI_USER }}" -p "${{ secrets.CI_TOKEN }}" + helm push "talks-slides-chart-${VERSION}.tgz" "${HELM_OCI_REPOSITORY}" diff --git a/content/css/home.css b/content/css/home.css new file mode 100644 index 0000000..c8290c4 --- /dev/null +++ b/content/css/home.css @@ -0,0 +1,165 @@ +/* Aligné sur content/kubernetes-hell-to-heaven/css/custom.css (specificat.io) */ +:root { + --bg-dark: #050709; + --bg-medium: #0d1115; + --bg-light: #1b1f22; + --bg-gradient-start: #10151a; + --text-primary: #f5f5f5; + --text-muted: rgba(245, 245, 245, 0.9); + --text-subtle: rgba(245, 245, 245, 0.75); + --accent-cyan: #2bc9c6; + --accent-cyan-light: #e9fefd; + --accent-cyan-bg: rgba(43, 201, 198, 0.08); + --accent-cyan-border: rgba(43, 201, 198, 0.55); + --accent-copper: #d19c5a; + --border-subtle: rgba(255, 255, 255, 0.12); + --border-very-subtle: rgba(255, 255, 255, 0.07); + --panel-glow: 0 0 0 1px rgba(43, 201, 198, 0.12); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + height: 100%; +} + +body { + min-height: 100%; + margin: 0; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif; + color: var(--text-primary); + background-color: var(--bg-dark); + background-image: radial-gradient( + circle at 10% 0%, + var(--bg-gradient-start) 0%, + var(--bg-dark) 52%, + var(--bg-light) 100% + ), + radial-gradient( + ellipse 100% 80% at 50% -20%, + rgba(43, 201, 198, 0.12), + transparent 55% + ); +} + +body::after { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + background: radial-gradient( + ellipse 95% 75% at 50% 50%, + transparent 35%, + rgba(5, 7, 9, 0.35) 100% + ); + z-index: 0; +} + +.page { + position: relative; + z-index: 1; + max-width: 960px; + margin: 0 auto; + padding: clamp(1.5rem, 4vw, 2.75rem) clamp(1.25rem, 4vw, 2rem) 3rem; +} + +.masthead { + margin-bottom: 2rem; +} + +.masthead h1 { + margin: 0 0 0.35rem; + font-size: clamp(1.75rem, 4vw, 2.25rem); + font-weight: 700; + letter-spacing: -0.02em; +} + +.masthead p { + margin: 0; + font-size: 1.05rem; + color: var(--text-subtle); + max-width: 42em; +} + +.talks-grid { + display: grid; + gap: 1rem; + grid-template-columns: 1fr; +} + +@media (min-width: 560px) { + .talks-grid { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + } +} + +.talk-card { + display: flex; + flex-direction: column; + padding: 1.15rem 1.25rem; + border-radius: 12px; + border: 1px solid var(--border-subtle); + background: rgba(13, 17, 21, 0.55); + box-shadow: var(--panel-glow), inset 0 1px 0 0 rgba(255, 255, 255, 0.03); + text-decoration: none; + color: inherit; + transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.12s ease; +} + +.talk-card:hover, +.talk-card:focus-visible { + border-color: var(--accent-cyan-border); + box-shadow: var(--panel-glow), 0 0 0 1px rgba(43, 201, 198, 0.18), + inset 0 1px 0 0 rgba(255, 255, 255, 0.05); + transform: translateY(-1px); + outline: none; +} + +.talk-card:focus-visible { + outline: 2px solid var(--accent-cyan); + outline-offset: 2px; +} + +.talk-card h2 { + margin: 0 0 0.4rem; + font-size: 1.1rem; + font-weight: 600; + color: var(--accent-cyan); +} + +.talk-card p { + margin: 0; + font-size: 0.95rem; + line-height: 1.45; + color: var(--text-muted); +} + +.talk-card .meta { + margin-top: 0.75rem; + font-size: 0.8rem; + color: var(--text-subtle); +} + +.site-footer { + margin-top: 2.5rem; + padding-top: 1rem; + border-top: 1px solid var(--border-very-subtle); + font-size: 0.8rem; + color: var(--text-subtle); +} + +.site-footer a { + color: var(--accent-cyan); + text-decoration: none; + font-weight: 500; +} + +.site-footer a:hover, +.site-footer a:focus-visible { + text-decoration: underline; + text-underline-offset: 2px; +} diff --git a/content/index.html b/content/index.html new file mode 100644 index 0000000..6466bcb --- /dev/null +++ b/content/index.html @@ -0,0 +1,32 @@ + + + + + + + Talks — Specificat + + + + +
+
+

Talks

+

Présentations (Reveal.js) — même charte visuelle que le site Specificat.

+
+ +
+ +

Kubernetes Operators

+

De l’enfer au paradis — Kubernetes, opérateurs et boucle de réconciliation.

+ Dossier · kubernetes-hell-to-heaven +
+
+ + +
+ + + diff --git a/kubernetes-hell-to-heaven/css/custom.css b/content/kubernetes-hell-to-heaven/css/custom.css similarity index 100% rename from kubernetes-hell-to-heaven/css/custom.css rename to content/kubernetes-hell-to-heaven/css/custom.css diff --git a/kubernetes-hell-to-heaven/index.html b/content/kubernetes-hell-to-heaven/index.html similarity index 100% rename from kubernetes-hell-to-heaven/index.html rename to content/kubernetes-hell-to-heaven/index.html diff --git a/flux/examples/helmrelease.yaml b/flux/examples/helmrelease.yaml new file mode 100644 index 0000000..377d66b --- /dev/null +++ b/flux/examples/helmrelease.yaml @@ -0,0 +1,55 @@ +# Exemple Flux — à intégrer dans le dépôt cluster (adapter namespaces / secrets). +# Chart poussé par la CI : helm push talks-slides-chart-.tgz oci://git.specificat.io/specificat.io +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: OCIRepository +metadata: + name: talks-slides-chart + namespace: flux-system +spec: + interval: 10m + url: oci://git.specificat.io/specificat.io/talks-slides-chart + secretRef: + name: gitea-registry-oci + ref: + semver: ">=0.0.1" +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: talks-slides + namespace: product +spec: + interval: 10m + targetNamespace: product + chartRef: + kind: OCIRepository + name: talks-slides-chart + namespace: flux-system + install: + remediation: + retries: 3 + upgrade: + remediation: + retries: 3 + values: + slides: + image: + repository: git.specificat.io/specificat.io/talks-slides + tag: latest # {"$imagepolicy": "product:talks-slides"} + imagePullSecrets: + - name: prd-gitea-registry-secret + ingress: + enabled: true + className: nginx + hosts: + - host: slides.specificat.io + paths: + - path: / + pathType: Prefix + # TLS via cert-manager (annotation + bloc tls pour le secret à créer) + certManager: + enabled: true + clusterIssuer: letsencrypt-prod + tls: + secretName: "" diff --git a/flux/examples/image-automation.yaml b/flux/examples/image-automation.yaml new file mode 100644 index 0000000..b32986b --- /dev/null +++ b/flux/examples/image-automation.yaml @@ -0,0 +1,51 @@ +# Optionnel : Image Automation pour l’image applicative (pas le chart). +# Adapte sourceRef GitRepository, update.path et la branche à ton dépôt cluster. +# Alternative souvent plus simple : ne pousser que l’image en CI et laisser le chart +# en OCI avec semver ; Flux réconcilie le chart ; le contenu des slides est déjà +# mis à jour par git pull dans le conteneur (pas besoin de redéployer à chaque commit). +# Prérequis : image-reflector-controller + image-automation-controller. +--- +apiVersion: image.toolkit.fluxcd.io/v1beta2 +kind: ImageRepository +metadata: + name: talks-slides + namespace: product +spec: + image: git.specificat.io/specificat.io/talks-slides + interval: 1m + secretRef: + name: prd-gitea-registry-secret +--- +apiVersion: image.toolkit.fluxcd.io/v1beta2 +kind: ImagePolicy +metadata: + name: talks-slides + namespace: product +spec: + imageRepositoryRef: + name: talks-slides + policy: + semver: + range: ">=0.0.1-0" +--- +apiVersion: image.toolkit.fluxcd.io/v1beta2 +kind: ImageUpdateAutomation +metadata: + name: talks-slides + namespace: product +spec: + interval: 1m + sourceRef: + kind: GitRepository + name: flux-system + namespace: flux-system + git: + commit: + author: + name: Flux + email: flux@specificat.io + push: + branch: main + update: + path: ./clusters/product + strategy: Setters diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..fadca22 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,2 @@ +# Contexte minimal : seuls Dockerfile, nginx/ et refresh.sh sont nécessaires. +# Le dépôt applicatif est cloné dans l'image (voir Dockerfile). diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..31ce61c --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,22 @@ +FROM nginx:alpine + +RUN apk add --no-cache git + +WORKDIR /usr/share/nginx/html + +# Image figée au clone ; le conteneur met à jour via refresh.sh (git pull origin main). +# Seul le dossier content/ est extrait (sparse checkout) : léger et aligné sur la racine Nginx. +ARG TALKS_REPO_URL=https://git.specificat.io/arnault/Talks.git +ARG TALKS_BRANCH=main +ARG TALKS_SPARSE_DIR=content + +RUN git clone --filter=blob:none --sparse --branch "${TALKS_BRANCH}" --single-branch "${TALKS_REPO_URL}" . \ + && git sparse-checkout init --cone \ + && git sparse-checkout set "${TALKS_SPARSE_DIR}" \ + && git config --global --add safe.directory /usr/share/nginx/html + +COPY nginx/default.conf /etc/nginx/conf.d/default.conf +COPY refresh.sh /refresh.sh +RUN chmod +x /refresh.sh + +CMD sh -c "/refresh.sh & exec nginx -g 'daemon off;'" diff --git a/server/nginx/default.conf b/server/nginx/default.conf new file mode 100644 index 0000000..59ed759 --- /dev/null +++ b/server/nginx/default.conf @@ -0,0 +1,12 @@ +server { + listen 80; + server_name localhost; + # Racine web = contenu statique sparse (dossier content/ du dépôt). + root /usr/share/nginx/html/content; + index index.html index.htm; + + location / { + add_header Cache-Control "no-cache"; + try_files $uri $uri/ =404; + } +} diff --git a/server/refresh.sh b/server/refresh.sh new file mode 100644 index 0000000..737842a --- /dev/null +++ b/server/refresh.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# Répertoire git : /usr/share/nginx/html (sparse checkout limité à content/). +cd /usr/share/nginx/html || exit 1 + +while true; do + echo "Pulling latest changes from origin/main (sparse content/)..." + git pull origin main || echo "git pull failed, will retry in 300s" + sleep 300 +done diff --git a/talks-slides-dist/.helmignore b/talks-slides-dist/.helmignore new file mode 100644 index 0000000..29becec --- /dev/null +++ b/talks-slides-dist/.helmignore @@ -0,0 +1,4 @@ +# Patterns to ignore when building packages. +.DS_Store +.git/ +*.tgz diff --git a/talks-slides-dist/Chart.yaml b/talks-slides-dist/Chart.yaml new file mode 100644 index 0000000..08df2c9 --- /dev/null +++ b/talks-slides-dist/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: talks-slides-chart +description: Helm chart for Talks slides (Nginx + contenu git) +type: application +version: 0.1.0 +appVersion: "0.1.0" +keywords: + - slides + - nginx + - reveal +maintainers: [] diff --git a/talks-slides-dist/templates/NOTES.txt b/talks-slides-dist/templates/NOTES.txt new file mode 100644 index 0000000..daf230c --- /dev/null +++ b/talks-slides-dist/templates/NOTES.txt @@ -0,0 +1,2 @@ +Déploiement Talks slides (Nginx). +Vérifiez l’Ingress et le DNS pour l’URL publique. diff --git a/talks-slides-dist/templates/_helpers.tpl b/talks-slides-dist/templates/_helpers.tpl new file mode 100644 index 0000000..04a7314 --- /dev/null +++ b/talks-slides-dist/templates/_helpers.tpl @@ -0,0 +1,41 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "talks-slides.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "talks-slides.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "talks-slides.labels" -}} +helm.sh/chart: {{ include "talks-slides.name" . }}-{{ .Chart.Version | replace "+" "_" }} +app.kubernetes.io/name: {{ include "talks-slides.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "talks-slides.selectorLabels" -}} +app.kubernetes.io/name: {{ include "talks-slides.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/talks-slides-dist/templates/deployment.yaml b/talks-slides-dist/templates/deployment.yaml new file mode 100644 index 0000000..3a6671f --- /dev/null +++ b/talks-slides-dist/templates/deployment.yaml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "talks-slides.fullname" . }} + labels: + {{- include "talks-slides.labels" . | nindent 4 }} +spec: + {{- if not .Values.slides.autoscaling.enabled }} + replicas: {{ .Values.slides.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "talks-slides.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "talks-slides.selectorLabels" . | nindent 8 }} + {{- with .Values.slides.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.slides.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + seccompProfile: + type: RuntimeDefault + containers: + - name: nginx + image: "{{ .Values.slides.image.repository }}:{{ .Values.slides.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.slides.image.pullPolicy }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: false + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + {{- toYaml .Values.slides.resources | nindent 12 }} + {{- with .Values.slides.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.slides.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.slides.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/talks-slides-dist/templates/ingress.yaml b/talks-slides-dist/templates/ingress.yaml new file mode 100644 index 0000000..4d94237 --- /dev/null +++ b/talks-slides-dist/templates/ingress.yaml @@ -0,0 +1,45 @@ +{{- if .Values.slides.ingress.enabled -}} +{{- $tlsOn := or .Values.slides.ingress.tls.enabled .Values.slides.ingress.certManager.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "talks-slides.fullname" . }} + labels: + {{- include "talks-slides.labels" . | nindent 4 }} + {{- if or .Values.slides.ingress.certManager.enabled .Values.slides.ingress.annotations }} + annotations: + {{- if .Values.slides.ingress.certManager.enabled }} + cert-manager.io/cluster-issuer: {{ .Values.slides.ingress.certManager.clusterIssuer | quote }} + {{- end }} + {{- with .Values.slides.ingress.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} +spec: + {{- if .Values.slides.ingress.className }} + ingressClassName: {{ .Values.slides.ingress.className }} + {{- end }} + {{- if $tlsOn }} + tls: + - hosts: + {{- range .Values.slides.ingress.hosts }} + - {{ .host | quote }} + {{- end }} + secretName: {{ .Values.slides.ingress.tls.secretName | default (printf "%s-tls" (include "talks-slides.fullname" .)) }} + {{- end }} + rules: + {{- range .Values.slides.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "talks-slides.fullname" $ }} + port: + number: {{ $.Values.slides.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/talks-slides-dist/templates/service.yaml b/talks-slides-dist/templates/service.yaml new file mode 100644 index 0000000..2d9afa7 --- /dev/null +++ b/talks-slides-dist/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "talks-slides.fullname" . }} + labels: + {{- include "talks-slides.labels" . | nindent 4 }} +spec: + type: {{ .Values.slides.service.type }} + ports: + - port: {{ .Values.slides.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "talks-slides.selectorLabels" . | nindent 4 }} diff --git a/talks-slides-dist/values.yaml b/talks-slides-dist/values.yaml new file mode 100644 index 0000000..27edbc0 --- /dev/null +++ b/talks-slides-dist/values.yaml @@ -0,0 +1,55 @@ +# Image : git.specificat.io/specificat.io/talks-slides +# Avec release "talks-slides" et nameOverride "talks-slides", le fullname reste cohérent. +nameOverride: "talks-slides" +fullnameOverride: "" + +slides: + image: + repository: git.specificat.io/specificat.io/talks-slides + tag: "latest" + pullPolicy: IfNotPresent + + imagePullSecrets: + - name: prd-gitea-registry-secret + + replicaCount: 1 + + service: + type: ClusterIP + port: 80 + + resources: + limits: + memory: 128Mi + requests: + cpu: 10m + memory: 32Mi + + autoscaling: + enabled: false + + nodeSelector: {} + affinity: {} + tolerations: [] + podLabels: {} + + ingress: + enabled: true + className: "" + annotations: {} + hosts: + - host: slides.specificat.io + paths: + - path: / + pathType: Prefix + # TLS : soit manuel (tls.enabled + secretName), soit via cert-manager (certManager.enabled). + # Le nom d’hôte vient de hosts[].host (réutilisé pour le bloc tls.hosts). + certManager: + enabled: false + # Ex. letsencrypt-prod — requis si certManager.enabled est true + clusterIssuer: "" + tls: + enabled: false + # Secret TLS dans le namespace (créé par cert-manager ou importé à la main). + # Vide : suffixe -tls sur le nom complet de la release (ex. talks-slides-tls). + secretName: ""