diff --git a/docs/analysis/ci-flow-values-vs-native-config.md b/docs/analysis/ci-flow-values-vs-native-config.md new file mode 100644 index 0000000..aa0f6ce --- /dev/null +++ b/docs/analysis/ci-flow-values-vs-native-config.md @@ -0,0 +1,163 @@ +# Miksi oma CI-kirjastoprojekti — ei perus-Gitea-tiedostoja + +> Liittyy: [architecture.md](../architecture.md), [config-model.md](../config-model.md), [design-rationale.md](../design-rationale.md) + +--- + +## Taso 1: Miksi kirjastoprojekti, ei repo-kohtaisia workflow'ta + +Kirjasto (`gitea-ci-library`) on oma projektinsa, koska sen tarjoamat palvelut +ovat **cross-cutting** — samat kaikissa mikropalveluissa. Ilman kirjastoa +jokainen repo kopioisi identtisen CI-logiikan. + +### Mitä kirjasto tarjoaa "ilmaiseksi" + +| Palvelu | Tiedosto/skripti | Rivimäärä | Jos 20 projektia kopioisi | +|---|---|---|---| +| Commit-statusraportointi | `report-status.sh` | ~45 | 900 riviä | +| Workflow-ketjutus + pollaus | `dispatch-workflow.sh` | ~80 | 1 600 riviä | +| HTML-raporttien MinIO-pushaus | `push-reports.sh` | ~50 | 1 000 riviä | +| GitOps-deploy (YAML-muokkaus + commit + cross-repo-trace) | `deploy.yml` | ~100 | 2 000 riviä | +| Test flow -ketjutus (dispatch + poll + cross-repo-status) | `ci-master.yml` + `test.yml` | ~150 | 3 000 riviä | +| Feature-branch CI (build-ekosysteemin valinta, concurrency) | `ci-feature.yml` | ~80 | 1 600 riviä | + +**Yhteensä ilman kirjastoa: ~10 000 riviä identtistä CI-koodia 20 repossa.** + +Yksi bugikorjaus (esim. `push-reports.sh`:n retry-logiikka) = 20 PR:ää, 20 review'tä, 20 mergeä. +Osa jää tekemättä → eri projektit käyttäytyvät eri tavalla → CI-epäluotettavuus leviää. + +**Kirjaston kanssa:** Yksi PR, yksi review, yksi merge. Kaikki projektit saavat korjauksen +`uses: org/gitea-ci-library/.gitea/workflows/ci-feature.yml@v1`-viittauksen kautta. + +### Miksi juuri nämä asiat ansaitsevat oman projektin + +Nämä kolme huolta ovat **pitkälle mietitty kokonaisuus**, jonka toteuttaminen +repo-kohtaisesti olisi massiivista tautologiaa: + +1. **Dynaaminen test plan K8s-ympäristössä** — Deploy developmentiin → integraatiotestit + → deploy stagingiin → e2e-testit. Jokainen steppi dispatchaa toisen repon workflow'n, + pollaa sen valmistumista, ja raportoi cross-repo-statuksen takaisin root-committiin. + +2. **HTML-testiraporttien tallennus** — Cucumber, JaCoCo, Maven Site. Pushataan MinIO:hon + deterministisellä URL:llä. URL linkitetään commit-statusviestiin. Retention CronJob + siivoaa vanhat. Tämä on kokonainen alijärjestelmä, ei yksi steppi. + +3. **GitOps-deployment** — Päivitä YAML-arvo Helm-repossa, committaa, pushaa, + raportoi cross-repo-status molempiin suuntiin. Sama mekaniikka jokaisessa + mikropalvelussa — vain `projectFolder` ja `fileName` vaihtelee. + +Näiden toteuttaminen per repo olisi kuin jokainen mikropalvelu toteuttaisi oman +tietokantakerroksensa sen sijaan että käyttäisi jaettua kirjastoa. + +--- + +## Taso 2: Miksi `ci-flow-values.yaml` — Gitean natiivikonfiguraatio ei riitä + +Gitean natiivimekanismit hoitavat salaisuudet ja infra-arvot. +`ci-flow-values.yaml` hoitaa rakenteisen, versioidun, projekti-spesifin konfiguraation. +Ne täydentävät toisiaan, eivät kilpaile. + +### Mitä Gitea tarjoaa natiivisti + +| Mekanismi | Tyyppi | Sijainti | Rajoite | +|-----------|--------|----------|---------| +| `workflow_call` `inputs` | Skalaari (`string`, `boolean`, `number`) | Kutsuvan workflow'n parametrit | **Ei tue listoja, objekteja, nested-rakenteita** | +| Org/repo secrets | `string` (salattu) | Gitea UI | Flat key-value, ei rakennetta | +| Org/repo variables | `string` | Gitea UI | Flat key-value, ei rakennetta | +| `.gitea/workflows/*.yml` | YAML | Projektin repo | Täysi workflow-logiikka, ei dataa | + +### Ongelma 1: skalaarityypit eivät kanna rakenteista konfiguraatiota + +`ci-flow-values.yaml` sisältää mm: + +```yaml +test-flow: + - deploy: development + wait: true + - test: + name: "integration fast" + repo: tests/integration + tags: "@temperature and not @slow" + - test: + name: e2e + repo: tests/e2e +``` + +Tämän ilmaiseminen Gitea `inputs`:na vaatisi jokaisen kentän erillisenä parametrina: + +```yaml +with: + test_flow_0_deploy: "development" + test_flow_1_test_name: "integration fast" + test_flow_1_test_repo: "tests/integration" + test_flow_1_test_tags: "@temperature and not @slow" + # …hajoaa heti kun array pituus vaihtelee +``` + +**Gitean `inputs` ei tue dynaamisen pituisia listoja.** Projekteilla on 0–n test flow -steppiä. + +### Ongelma 2: konfiguraatio kuuluu repoon, ei Gitean UI:hin + +Jos kaikki asetukset vietäisiin Gitea org/repo variables -mekanismiin: + +- **Versionhallinta katoaa.** Konfiguraation historiaa ei voi tarkastella (`git log`), reviewata (PR), tai rollbackata (`git revert`). +- **Konfiguraatio irtoaa koodista.** Kun koodi muuttuu ja tarvitsee uuden konfiguraatioavaimen, muutos tehdään Gitean UI:ssa — eri ajankohtana kuin koodimuutos. CI hajoaa välissä. +- **Ei PR-prosessia konfiguraatiolle.** Gitean variables/secrets eivät kulje review'n läpi. +- **Siirtäminen ympäristöjen välillä vaikeutuu.** Staging- ja production-Gitealla on eri variablet. Ei `git cherry-pick` -mekanismia. + +### Miksi kaksi tiedostoa, ei yksi + +Eli miksi projektilla on **sekä** `.gitea/workflows/ci.yml` **että** `ci-flow-values.yaml`? + +``` +.gitea/workflows/ci.yml → MITEN ajetaan (workflow-valinta + parametrit) +ci-flow-values.yaml → MITÄ ajetaan (projektin data) +``` + +| `.gitea/workflows/ci.yml` | `ci-flow-values.yaml` | +|---------------------------|----------------------| +| Sama kaikissa projekteissa | Uniikki per projekti | +| Asuu projektin repossa (ohut kutsuja) | Asuu projektin repossa | +| Kopioitavissa, mutta EI sisällä logiikkaa | Versioituu projektin mukana | +| Välittää parametrit `with:` → kirjastolle | Sisältää: docker-nimi, sonar-projekti, test-flow-array | + +### Työnjako Gitea natiivin ja `ci-flow-values.yaml`:n välillä + +| Vaatimus | Gitea natiivi | `ci-flow-values.yaml` | +|----------|--------------|----------------------| +| Nested-rakenteet (listat, objekit) | Ei (`inputs` = skalaari) | Kyllä (YAML) | +| Versionhallinta + PR-review | Ei (UI-pohjainen) | Kyllä (`git`) | +| Projekti omistaa konffinsa | Osittain (repo variables) | Kyllä | +| Infra-tason salaisuudet | Kyllä (org secrets) | **Ei** | +| Jaetut infra-arvot | Kyllä (org variables) | **Ei** | + +--- + +## Yhteenveto: moottori + consumer-tiedostot + +``` +gitea-ci-library (MOOTTORI) Jokainen consumer-repo +┌──────────────────────────────┐ ┌──────────────────────────────┐ +│ Reusable workflowt │ │ .gitea/workflows/ci.yml │ +│ ci-feature.yml │◄─────────│ uses: .../ci-feature.yml@v1 │ +│ ci-master.yml │ kutsuu │ │ +│ deploy.yml │ │ ci-flow-values.yaml │ +│ test.yml │◄─────────│ build.ecosystem: maven │ +│ │ lukee │ docker.imageName: ... │ +│ Jaetut skriptit │ │ test-flow: [...] │ +│ report-status.sh │ └──────────────────────────────┘ +│ dispatch-workflow.sh │ +│ push-reports.sh │ Gitea org +│ tag-commit.sh │ ┌──────────────────────────────┐ +└──────────────────────────────┘ │ secrets: tokenit, salasanat │ + │ variables: MinIO URL, Sonar │ +Kirjasto EI sisällä ci.yml:ää └──────────────────────────────┘ +consumerille — se on moottori, +joka syö consumerin tuomia tiedostoja. ci.yml on kopioitavissa, + identtinen kaikilla consumereilla. +``` + +Salaisuudet (tokenit, salasanat) ja jaetut infra-arvot (MinIO URL, SonarQube URL) +kuuluvat Gitean natiivimekanismeihin (org secrets/variables). +CI-logiikka kuuluu kirjastoon (moottori). +Projektikohtainen data (`ci-flow-values.yaml`) ja ohut kutsuja (`ci.yml`) kuuluvat consumer-repoon. diff --git a/git-pages/Chart.yaml b/git-pages/Chart.yaml new file mode 100644 index 0000000..7455afb --- /dev/null +++ b/git-pages/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: git-pages +description: Codeberg git-pages for CI HTML reports (apex site, Traefik BasicAuth publish) +type: application +version: 0.1.0 +appVersion: "0.9.1" diff --git a/git-pages/README.md b/git-pages/README.md new file mode 100644 index 0000000..96b0e5f --- /dev/null +++ b/git-pages/README.md @@ -0,0 +1,74 @@ +# git-pages + +Jaettu **Gitea CI -raporttien** tallennus- ja lukupaikka: HTML-raportit (esim. Cucumber) +commit-kohtaisiin polkuihin, selaimella avattavina linkkeinä Gitean commitin CI-job +raportista. Kuvaus ja perustelut: [docs/](docs/). + +--- + +## Käyttöönotto + +### 1. Secretit + +[docs/secrets.md](docs/secrets.md) + +### 2. Instanssin values-tiedosto + +`values.yaml` sisältää jaetut vakiot. Ympäristökohtaiset arvot omaan tiedostoon, esim. +`dev-values.yaml` / `prod-values.yaml`: + +```yaml +ingress: + host: pages.example.com # julkinen host (luku + julkaisu) + +certificate: + issuerRef: + name: letsencrypt-prod # cert-manager ClusterIssuer / Issuer + kind: ClusterIssuer + +persistence: + storageClass: "" # tyhjä = klusterin oletus + size: 5Gi + +retention: + giteaApiUrl: https://gitea.example.com + rules: + default: + maxAgeDays: 90 + keepMin: 5 + branches: + main: + maxAgeDays: 365 + keepMin: 20 +``` + +Esimerkki dev-ympäristöstä: [dev-values.yaml](dev-values.yaml). + +### 3. Helm-asennus + +Repojuuresta (sama `NS` kuin [docs/secrets.md](docs/secrets.md)): + +```bash +NS=git-pages +VALUES=git-pages/dev-values.yaml + +helm upgrade --install git-pages ./git-pages \ + -n "$NS" --create-namespace \ + -f "$VALUES" + +helm template git-pages ./git-pages -f "$VALUES" +``` + +--- + +## CI-julkaisu + +Workflow tarvitsee vähintään: + +| Muuttuja | Kuvaus | +|----------|--------| +| `PAGES_HOST` | Sama kuin `ingress.host` (julkinen lukulinkki) | +| `GIT_PAGES_PUBLISH_URL` | `https://{ingress.host}` | +| `GIT_PAGES_PUBLISH_TOKEN` | Gitea Actions secret — sama arvo kuin publish-auth-secretissa | + +Julkaisu repossa: `scripts/publish-git-pages.sh` (dogfood: `.gitea/workflows/ci-engine.yml`). diff --git a/git-pages/dev-values.yaml b/git-pages/dev-values.yaml new file mode 100644 index 0000000..80fc812 --- /dev/null +++ b/git-pages/dev-values.yaml @@ -0,0 +1,28 @@ +# Dev instance — overrides values.yaml constants. +# helm upgrade --install git-pages ./git-pages -n git-pages -f dev-values.yaml + +ingress: + host: ci-reports.helm-dev.keskikuja.site + +certificate: + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + +persistence: + storageClass: "" + size: 5Gi + +retention: + giteaApiUrl: https://gitea.app.keskikuja.site + rules: + default: + maxAgeDays: 90 + keepMin: 5 + branches: + main: + maxAgeDays: 365 + keepMin: 20 + master: + maxAgeDays: 365 + keepMin: 20 diff --git a/git-pages/docs/architecture.md b/git-pages/docs/architecture.md new file mode 100644 index 0000000..347a320 --- /dev/null +++ b/git-pages/docs/architecture.md @@ -0,0 +1,143 @@ +# Architecture — git-pages + +> Komponentit, datavirrat ja rajapinnat. Miksi näin on rakennettu: [design-rationale.md](design-rationale.md). +> Secretit: [secrets.md](secrets.md). Teknologiat: [tech-stack.md](tech-stack.md). + +Tämä dokumentti koskee vain `git-pages/`-palvelua — ei juuren `gitea-ci-library`-kirjastoa. + +--- + +## Yleiskuvaus + +git-pages on jaettu **HTML-raporttiarkisto**: yksi apex-host, monta Gitea-repoa, commit-kohtaiset +raporttipolut. Julkaisija (esim. CI) puskaa sisällön tar-arkistona; lukija avaa raportin +selaimella commit-linkistä. + +Codeberg git-pages ajaa `PAGES_INSECURE=1` — sovellus ei tee forge-authia. Julkaisu- ja +TLS-rajaukset ovat Kubernetes-kerroksessa (Traefik, cert-manager, Secretit). + +--- + +## Komponentit + +| Komponentti | Rooli | +|-------------|-------| +| **git-pages Pod** | Codeberg git-pages `0.9.1`, filesystem-storage `/app/data` | +| **PVC** | Raporttisisältö (`{owner}/{repo}/reports/{sha8}/…`) | +| **Service** | ClusterIP :3000 git-pagesille | +| **Traefik IngressRoute** | Julkaisu (PATCH/PUT + BasicAuth) ja luku (GET/HEAD) eri säännöillä | +| **Traefik Middleware** | `git-pages-publish-auth` (BasicAuth), HTTPS-redirect | +| **cert-manager Certificate** | TLS → Secret `git-pages-tls` | +| **CronJob `git-pages-retention`** | Päivittäinen siivous (PVC + Gitea branch-lista) | + +| Secret | Rooli | +|--------|-------| +| `git-pages-publish-auth` | htpasswd julkaisuun (Traefik) | +| `git-pages-retention-gitea` | Gitea PAT branch-listaan (CronJob) | + +--- + +## URL ja sisältö + +Julkinen osoite = **selvä URL** + **Gitea-yhteensopiva polku**: + +``` +https://pages.example.com/acme-corp/backend-api/reports/abc12345/index.html + └─ selvä URL ─┘ └────────── Gitea-yhteensopiva polku ──────────┘ +``` + +Levyllä (apex index-site): + +``` +/app/data/ + {owner}/ + {repo}/ + reports/ + {sha8}/ + index.html + .meta # branch, sha, published_at +``` + +Apex-juuri `/` on tyhjä — ei landing-sivua. + +--- + +## Järjestelmäkaavio + +```mermaid +flowchart TB + subgraph ext["Ulkoiset"] + PUB["Julkaisija\n(CI)"] + BR["Selain"] + GITEA["Gitea API\n(branch-lista)"] + end + + subgraph edge["Reuna"] + TRAEFIK["Traefik\nIngressRoute + Middleware"] + CM["cert-manager\nTLS"] + end + + subgraph cluster["git-pages"] + GP["git-pages Pod"] + PVC["PVC /app/data"] + CJ["CronJob retention"] + end + + PUB -->|"PATCH/PUT + BasicAuth\ntar"| TRAEFIK + BR -->|"GET/HEAD"| TRAEFIK + TRAEFIK --> GP + CM --> TRAEFIK + GP --> PVC + CJ -->|"read branches"| GITEA + CJ --> PVC +``` + +--- + +## Julkaisu + +1. Julkaisija paketoi `{owner}/{repo}/reports/{sha8}/` tar-arkistoksi (sis. `.meta`) +2. `PATCH` tai `PUT` apex-URL:iin (`https://{host}/`) + `Content-Type: application/x-tar` +3. Traefik tarkistaa BasicAuth (`publish` + token) → välittää git-pagesille +4. git-pages kirjoittaa PVC:lle + +Julkaisu kulkee aina julkisen ingressin kautta — ei suoraa ClusterIP-kirjoitusta ulkopuolelta. + +--- + +## Luku + +1. Selain avaa commit-statuslinkin (GET/HEAD) +2. Traefik välittää git-pagesille ilman julkaisu-Middlewarea +3. git-pages palauttaa HTML:n polusta + +Luku-auth (OIDC) ei ole toteutettu — GET/HEAD on julkinen, jos URL tunnetaan. +Katso [design-rationale.md — Luku-auth](design-rationale.md#luku-auth). + +--- + +## Retention + +CronJob `git-pages-retention` (oletus kerran päivässä): + +1. Skaalaa git-pages deployment hetkeksi pois (RWO-PVC) +2. Käy läpi `reports/{sha8}/.meta` +3. **Poistettu branch** — jos `.meta.branch` ei ole Gitean branch-listassa → poista (aina) +4. **Aktiivinen branch** — `maxAgeDays` + `keepMin` (`retention.rules` instanssiarvoissa) +5. Skaalaa deployment takaisin + +Gitea API: `GET /api/v1/repos/{owner}/{repo}/branches` — `read:repository` PAT. +Katso [secrets.md](secrets.md). + +--- + +## Rajapinnat + +| Suunta | Protokolla | Auth | Kuvaus | +|--------|------------|------|--------| +| Julkaisija → Traefik | HTTPS PATCH/PUT | BasicAuth `publish` | tar → apex site | +| Selain → Traefik | HTTPS GET/HEAD | — (tänään) | HTML-raportti | +| CronJob → Gitea | HTTPS GET | PAT `read:repository` | branch-lista per repo | +| Traefik → git-pages | HTTP :3000 | — | sisäverkko | + +git-pages ei käytä Gitea forge-API:a julkaisuun eikä `pages`-branchia. diff --git a/git-pages/docs/design-rationale.md b/git-pages/docs/design-rationale.md new file mode 100644 index 0000000..ec4e0a6 --- /dev/null +++ b/git-pages/docs/design-rationale.md @@ -0,0 +1,180 @@ +# Design Rationale — git-pages + +> Miksi git-pages on rakennettu näin. Arvot, periaatteet ja reunaehdot. +> +> Tämä dokumentti on **normatiivinen** `git-pages/`-alikansiolle. Se ei kuvaa juuren +> `gitea-ci-library`-kirjastoa eikä sen workfloweja. +> +> Liittyvät dokumentit: [architecture.md](architecture.md), [tech-stack.md](tech-stack.md), [secrets.md](secrets.md). + +--- + +## Miksi tämä on olemassa + +### Ongelma + +CI-testiajoista syntyy HTML-raportteja. Esimerkiksi Cucumber-testiraportti toimii +elävänä dokumentaationa git commitin tilasta. + +Gitea ei tarjoa web-selaimella selattavaa arkistoa näille HTML-raporteille. + +git-pages ratkaisee tämän ongelman. + +| Vaihtoehto | Miksi ei riitä | +|---|---| +| **Gitea Actions -artifactit** | Vain ZIP-lataus — HTML ei renderöidy selaimessa | +| **Gitea `pages`-branch** | Yksi branch per repo; rinnakkaiset buildit törmäävät saman branchin pushissa | +| **Gitea Releases** | Sotkee julkaisuhistorian satojen CI-buildien raporteilla | + +### Ongelma URL:ssa (hylätty malli) + +Alkuvaiheen malli sitoi hostin repoon: `https://{owner}.{host}/{repo}/...` +(subdomain per owner). Julkinen linkki piti sitten “kääntää” Gitea-tyyliseksi poluksi +Traefik-rewritellä (`/{owner}/{repo}/...` → eri `Host` + lyhyempi polku). + +Tämä oli ongelmallinen: + +- per-owner middleware / rewrite kube-resursseina +- julkaisu-URL ja lukemis-URL eri muodossa +- wildcard-TLS tai monimutkainen cert-hallinta +- vaikea selittää kehittäjälle mistä host tulee + +### Ratkaisu — `selvä_url` + Gitea-yhteensopiva polku + +URL rakennetaan kahdesta erillisestä osasta, ei yhdestä sekavasta kaavasta: + +| Osa | Mistä | Esimerkki | +|-----|-------|-----------| +| **Selvä URL** | Organisaation kiinteä pages-host | `https://pages.example.com` | +| **Gitea-yhteensopiva polku** | Repo (`{owner}/{repo}`) + commit | `/acme-corp/backend-api/reports/abc12345/index.html` | + +**Julkinen linkki** = selvä URL + polku (yksi merkkijono commit-statusiin, ei rewritea): + +`https://pages.example.com/acme-corp/backend-api/reports/abc12345/index.html` + +Polku vastaa Gitea Pages -käytäntöä (`/{owner}/{repo}/...`). Host on aina sama — +ei `{owner}.pages...`-subdomainia. + +Julkaisija (CI tai muu asiakas) lähettää tar-arkiston PATCH/PUT:lla. Lukija hakee +HTML:n GET:llä. Ei Gitea-git-integraatiota eikä `pages`-branchia. + +**Codebergin security-malli ei sovellu tähän käyttöön** — forge-auth (Gitea PAT + +`write:repository`), DNS TXT -haaste ja muut git-pagesin sisäänrakennetut valtuutus- +mekanismit on ohitettu kokonaan (`PAGES_INSECURE=1`). Niiden sijaan Kubernetes-kerros +hoitaa rajauksen: Traefik BasicAuth julkaisuun, cert-manager TLS:ään, erillinen +publish-token ([secrets.md](secrets.md)). Sovellus palvelee sisältöä; klusteri päättää +kuka saa kirjoittaa. + +--- + +## Suunnitteluperiaatteet + +### 1. Selvä URL + Gitea-yhteensopiva polku + +Julkinen osoite = kiinteä apex-host + polku `/{owner}/{repo}/reports/{sha8}/...`. +Apex-juuri `/` on tyhjä tarkoituksella — ei landing-sivua. + +**Miksi:** Kehittäjä näkee Gitea-tyylisen polun; infra näkee yhden hostin. Ei Traefik- +rewritea, ei per-owner subdomaineja, ei erillistä “julkaisu-URL vs. lukemis-URL” -kaavaa. +Yksi TLS-sertifikaatti, yksi IngressRoute, yksi PVC. + +### 2. Julkaisu ja luku erotettu + +Julkaisu (PATCH/PUT) vaatii Traefik BasicAuthin. Luku (GET/HEAD) on erillinen reitti — +katso [Luku-auth](#luku-auth) alla. + +**Miksi:** git-pages ajaa `PAGES_INSECURE=1` — sovellus ei validoi julkaisuoikeuksia. +Kirjoitusoikeus on eksplisiittisesti Traefik Middlewaressä (`git-pages-publish-auth`). + +### 3. Yksi publish-token, kaksi säilöä + +Sama plaintext-token: klusterin Secretissä htpasswd-hashina, julkaisijan secret-holvissa +(esim. CI-alustan Actions-secret). + +**Miksi:** Ei Gitea PAT:ia eikä `write:repository` -oikeutta. Token antaa vain +julkaisuoikeuden tähän palveluun. Yksi arvo, kaksi paikkaa — ks. [secrets.md](secrets.md). + +### 4. Secretit erillisessä hallinnassa + +`git-pages-publish-auth` luodaan ennen käyttöönottoa — ei osana sovelluksen konfiguraatiotiedostoja. + +**Miksi:** Salaisuudet eivät kulje versionoiduissa arvoissa. Rotaatio ja SealedSecrets +pysyvät operaattorin hallussa. Ks. [secrets.md](secrets.md). + +### 5. Minimaalinen parametrisointi + +Instance-arvot (`host`, `issuer`, PVC) `{env}-values.yaml`:ssa. Resurssinimet, +secret-nimet ja Traefik-wire kovakoodattu templatessa. + +**Miksi:** Parametrisoi vain se, mikä vaihtelee instanssien välillä (host, TLS-issuer, +levy). Vakioidut nimet ja wire pysyvät ennustettavina kaikissa asennuksissa. + +--- + +## Puutteet + +Tietoisesti avoimet asiat — eivät estä nykyistä julkaisu- ja lukumallia. + +### Luku-auth + +Julkaisu on suojattu (Traefik BasicAuth). **Luku ei ole:** GET/HEAD on julkinen — kuka +tuntee URL:n voi lukea raportin. + +Tavoite: Traefik OIDC GET/HEAD-reitille (Gitea OAuth2 -provider). Session säilyy — +commit-statuslinkki toimii kirjautumisen jälkeen ilman uutta julkaisuoikeutta. + +Ei toteutettu. Julkaisu- ja luku-reitit pysyvät erillisinä; OIDC lisätään vain lukupuolelle. + +### Retention + +CronJob `git-pages-retention` (kerran päivässä) skannaa PVC:n ja poistaa vanhentuneet +raportit. Julkaisija kirjoittaa `reports/{sha8}/.meta` (branch, sha, published_at). + +| Sääntö | Konfiguroitavissa? | Kuvaus | +|--------|-------------------|--------| +| **Poistettu branch** | Ei — aina | Jos `.meta.branch` ei ole Giteassa enää, raportti poistetaan | +| **maxAgeDays** | Kyllä (`dev-values`) | Aktiivisen branchin raportit vanhemmat kuin N päivää | +| **keepMin** | Kyllä (`dev-values`) | Aktiivisella branchilla pidetään vähintään N uusinta | + +Poistettujen branchien siivous ei tarvitse parametreja. Jäljelle jääneille branchille +säännöt tulevat `retention.rules` (`default` + `branches.{name}`). + +**Puutteet:** tagattuja commiteja ei suojata erikseen. RWO-PVC vaatii lyhyen katkon +(deployment skaalataan 0:ksi ajon ajaksi). + +Secret: `git-pages-retention-gitea` — Gitea PAT branch-listaan. Ks. [secrets.md](secrets.md). + +--- + +## Rajat + +- **Ei forge-integraatiota** — ei `pages`-branchia, ei Gitea API -hakua, ei forge-authia +- **Ei julkaisijalogiikkaa** — kuka julkaisee ja milloin on julkaisijan vastuulla +- **Ei sisäverkon ohitusjulkaisua** — julkaisu kulkee julkisen ingressin kautta (BasicAuth) + +--- + +## Teknologiavalinnat + +| Valinta | Miksi | +|---------|-------| +| **Codeberg git-pages** `0.9.1` | Natiivi apex index-site + tar-pohjainen PATCH/PUT -julkaisu | +| **Filesystem + PVC** | Yksinkertainen, yksi replica, ei erillistä objektivarastoa | +| **Traefik IngressRoute + Middleware** | Julkaisuauth erillään sovelluksesta; GET/HEAD eri säännöllä | +| **cert-manager** | TLS automaattisesti (`git-pages-tls`) | +| **Helm v3** | Toistettava asennus; instanssikohtaiset arvot erillisessä values-tiedostossa | + +--- + +## Mitä tietoisesti hylättiin + +| Hylätty | Syy | +|---------|-----| +| **deadnews/gitea-pages** | Vetää sisällön Gitea API:sta — ei sopinut CI-push-malliin | +| **Gitea `pages`-branch** | Race condition rinnakkaisissa buildeissa | +| **Per-owner subdomain** (`{owner}.pages...`) | Ongelmallinen URL; vaatii rewrite-middlewarea polun kääntämiseen | +| **Traefik path→host -rewrite** | Korvattu apex + Gitea-polulla — yksi selvä URL commit-linkissä | +| **Gitea forge-auth / PAT** | `write:repository` liian laaja oikeus vain raporttijulkaisuun | +| **DNS TXT -haaste julkaisuun** | Operatiivinen kompleksisuus ilman hyötyä BasicAuthiin verrattuna | +| **Helm-managed publish Secret** | Arvot values-tiedostoihin on kielletty; manuaalinen lähde totuudelle | +| **Image tag `v0.9.1`** | Oikea tagi on `0.9.1` (ei `v`-etuliitettä) | diff --git a/git-pages/docs/secrets.md b/git-pages/docs/secrets.md new file mode 100644 index 0000000..0fa8dd4 --- /dev/null +++ b/git-pages/docs/secrets.md @@ -0,0 +1,96 @@ +# Secrets - Prerequisites + +`git-pages` requires the following Kubernetes Secrets to exist before the cluster +consumers can use them. These secrets are not managed by Helm — create them manually or +via external secret management (SealedSecrets, External Secrets Operator, Vault). + +TLS (`git-pages-tls`) is issued by cert-manager — not a manual prerequisite. + +--- + +## Secret Inventory + +| Secret | Keys | Consumers | Required | +|--------|------|-----------|----------| +| `git-pages-publish-auth` | `users` | Traefik Middleware `git-pages-publish-auth` | Always | +| `git-pages-retention-gitea` | `token` | CronJob `git-pages-retention` | Always | + +--- + +## Tokens + +### Publish token + +Ei Gitea PAT. Satunnainen merkkijono (`GIT_PAGES_PUBLISH_TOKEN`), joka menee kahteen paikkaan: + +| Paikka | Muoto | +|--------|-------| +| Kubernetes `git-pages-publish-auth` | htpasswd: `publish:` | +| Gitea Actions secret `GIT_PAGES_PUBLISH_TOKEN` | sama plaintext | + +CI käyttää sitä Traefik BasicAuthiin. Ei git-kirjoitusoikeutta. + +**Vienti Giteaan:** Organization or Repository → Settings → Actions → Secrets → +`GIT_PAGES_PUBLISH_TOKEN` = publish-tokenin plaintext (org secret, jos usea repo julkaisee). + +### Retention token + +Gitea PAT CronJobille. CronJob listaa branchit repokohtaisesti +(`GET /api/v1/repos/{owner}/{repo}/branches`) ja poistaa raportit, joiden `.meta.branch` +ei ole enää Giteassa. + +Tarvitaan vain **`read:repository`**. Ei `write:repository`. Tokenin omistajan täytyy +nähdä kaikki repot, joista raportteja on levyllä. + +**Ei viedä Giteaan** — vain Kubernetes Secret `git-pages-retention-gitea`. + +**PAT Giteassa (read only):** + +1. Kirjaudu Gitea-käyttäjällä, jolla on luku kaikkiin raporttirepoihin +2. **Settings** → **Applications** → **Generate New Token** +3. Token name: esim. `git-pages-retention` +4. Scopes: valitse vain **`read:repository`** — älä valitse `write:repository` eikä muita +5. **Generate Token** → kopioi token heti (näytetään vain kerran) +6. Aseta shelliin: `export GITEA_RETENTION_TOKEN='gitea_pat_…'` + +--- + +## Create Secrets + +```bash +NS=git-pages + +# Publish +GIT_PAGES_PUBLISH_TOKEN="$(openssl rand -base64 24)" + +kubectl create secret generic git-pages-publish-auth \ + --from-literal=users="$(docker run --rm httpd:2-alpine htpasswd -nb publish "$GIT_PAGES_PUBLISH_TOKEN")" \ + -n $NS + +echo "Gitea Actions → GIT_PAGES_PUBLISH_TOKEN:" +echo "$GIT_PAGES_PUBLISH_TOKEN" + +# Retention (PAT luotu yllä Giteassa) +kubectl create secret generic git-pages-retention-gitea \ + --from-literal=token="$GITEA_RETENTION_TOKEN" \ + -n $NS + +kubectl get secrets -n $NS +``` + +--- + +## Secret Management (Production) + +Secrets can be created manually with the snippets above, or migrated to a secret management +solution. The `kubectl create` blocks are the rolling source — replace them with the target +tool's equivalent when ready: + +| Approach | Replaces `kubectl create` with | +|----------|-------------------------------| +| Manual rotation | Re-run the same snippets with new values | +| SealedSecrets | `kubeseal` encrypted manifest | +| External Secrets Operator | `ExternalSecret` CR pointing to the vault | +| Vault / other | Vault agent / CSI driver injection | + +Structure of `docs/secrets.md` stays identical regardless of the chosen approach. diff --git a/git-pages/docs/tech-stack.md b/git-pages/docs/tech-stack.md new file mode 100644 index 0000000..bc3f099 --- /dev/null +++ b/git-pages/docs/tech-stack.md @@ -0,0 +1,81 @@ +# Tech Stack — git-pages + +> Mitä teknologioita `git-pages/` Helm chart käyttää ja edellyttää. Tämä dokumentti koskee vain +> `git-pages/`-alikansiota monorepossa — ei juuren `gitea-ci-library`-kirjastoa. + +--- + +## Sovellus + +| Teknologia | Versio | Käyttö | +|---|---|---| +| **git-pages** (Codeberg) | `0.9.1` | Staattinen sisältö, apex index-site (`/.index`), HTTP PATCH/PUT -julkaisu | +| **Filesystem storage** | — | Sisältö PVC:llä (`/app/data`) | +| **TOML** | — | Sovellusconfig ConfigMapissa (`config.toml`) | + +Image: `codeberg.org/git-pages/git-pages:0.9.1` (ei `v`-etuliitettä tagissa). + +Chart ajaa `PAGES_INSECURE=1` — julkaisuvaltuutus Traefik Middlewaressä, ei forge-authia. + +--- + +## Alusta ja verkko + +| Teknologia | Versio / minimi | Käyttö | +|---|---|---| +| **Kubernetes** | 1.24+ | Deployment, Service, PVC, Secret | +| **Helm** | v3 | Chart asennus ja päivitys | +| **Traefik** | CRD `traefik.io/v1alpha1` | IngressRoute, Middleware (`basicAuth`, HTTPS-redirect) | +| **cert-manager** | — | TLS-sertifikaatti (`git-pages-tls`) | + +--- + +## Pysyvyys + +| Teknologia | Käyttö | +|---|---| +| **PersistentVolumeClaim** | Raporttisisältö (`ReadWriteOnce`) | +| **storageClass** | Instance-kohtainen (`dev-values.yaml`) | + +--- + +## Esiehdot (klusteri) + +| Resurssi | Lähde | Dokumentti | +|---|---|---| +| `git-pages-publish-auth` | Manuaalinen Secret | [secrets.md](secrets.md) | +| `git-pages-tls` | cert-manager Certificate | Automaattinen asennuksessa | +| `ClusterIssuer` | Klusteri (esim. `letsencrypt-prod`) | `dev-values.yaml` | + +--- + +## Asennustyökalut (operaattori) + +| Työkalu | Käyttö | +|---|---| +| **kubectl** | Secretin luonti | +| **openssl** | Publish-tokenin generointi (`rand -base64 24`) | +| **Docker** (`httpd:2-alpine`) | htpasswd-rivin generointi | + +--- + +## Konfiguraatio + +| Tiedosto | Kerros | Sisältö | +|---|---|---| +| `values.yaml` | Chart-vakiot | Image, resurssit, Traefik entrypointit | +| `{env}-values.yaml` | Instanssi | Host, issuer, PVC koko/storageClass | + +Esimerkki: `helm upgrade --install git-pages ./git-pages -n git-pages -f dev-values.yaml` + +--- + +## Mitä EI käytetä + +| Teknologia | Syy | +|---|---| +| **Gitea `pages`-branch** | Ei Gitea git-pages -integraatiota | +| **deadnews/gitea-pages** | Ei Gitea API -hakua | +| **MinIO / S3** | Erillinen raporttivarasto — ei tämän chartin scope | +| **Gitea forge-auth / PAT** | Julkaisu BasicAuth-tokenilla (`git-pages-publish-auth`) | +| **Helm-managed publish Secret** | `publishAuth.create: false` — secret manuaalisesti | diff --git a/git-pages/files/retention-cleanup.sh b/git-pages/files/retention-cleanup.sh new file mode 100644 index 0000000..9dba6ff --- /dev/null +++ b/git-pages/files/retention-cleanup.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +set -euo pipefail + +DATA_ROOT="${DATA_ROOT:-/app/data}" +CONFIG="${RETENTION_CONFIG:-/etc/retention/retention.json}" +GITEA_API_URL="${GITEA_API_URL:?GITEA_API_URL is required}" +GITEA_TOKEN="${GITEA_TOKEN:?GITEA_TOKEN is required}" + +[ -d "$DATA_ROOT" ] || { echo "ERROR: data root missing: $DATA_ROOT" >&2; exit 1; } +[ -f "$CONFIG" ] || { echo "ERROR: config missing: $CONFIG" >&2; exit 1; } + +default_max_age=$(jq -r '.default.maxAgeDays' "$CONFIG") +default_keep_min=$(jq -r '.default.keepMin' "$CONFIG") + +gitea_get() { + curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Accept: application/json" "${GITEA_API_URL}${1}" +} + +branch_names_for_repo() { + local owner="$1" repo="$2" page=1 + while true; do + local resp count + resp=$(gitea_get "/api/v1/repos/${owner}/${repo}/branches?limit=50&page=${page}") + count=$(echo "$resp" | jq 'length') + [ "$count" -eq 0 ] && break + echo "$resp" | jq -r '.[].name' + [ "$count" -lt 50 ] && break + page=$((page + 1)) + done +} + +rule_max_age() { + local branch="$1" v + v=$(jq -r --arg b "$branch" '.branches[$b].maxAgeDays // empty' "$CONFIG") + [ -n "$v" ] && echo "$v" || echo "$default_max_age" +} + +rule_keep_min() { + local branch="$1" v + v=$(jq -r --arg b "$branch" '.branches[$b].keepMin // empty' "$CONFIG") + [ -n "$v" ] && echo "$v" || echo "$default_keep_min" +} + +delete_report_dir() { + echo "DELETE $1 ($2)" + rm -rf "$1" +} + +age_days() { + local meta="$1" published epoch_pub now + published=$(jq -r '.published_at // empty' "$meta") + if [ -z "$published" ]; then + epoch_pub=$(stat -c %Y "$(dirname "$meta")" 2>/dev/null || stat -f %m "$(dirname "$meta")") + else + epoch_pub=$(date -u -d "$published" +%s 2>/dev/null || echo 0) + fi + now=$(date -u +%s) + [ "$epoch_pub" -eq 0 ] && echo 99999 && return + echo $(( (now - epoch_pub) / 86400 )) +} + +CACHE_DIR=$(mktemp -d) +trap 'rm -rf "$CACHE_DIR"' EXIT + +active_branches_file() { + local owner="$1" repo="$2" + local cache="${CACHE_DIR}/${owner}__${repo}.branches" + if [ ! -f "$cache" ]; then + branch_names_for_repo "$owner" "$repo" | sort -u > "$cache" + fi + echo "$cache" +} + +branch_active() { + grep -Fxq "$2" "$1" +} + +parse_report_path() { + local report_dir="$1" rel rest + rel="${report_dir#"${DATA_ROOT}/"}" + REPO_OWNER="${rel%%/*}" + rest="${rel#*/}" + REPO_NAME="${rest%%/*}" +} + +# Pass 1: deleted branches (always — no config) +while IFS= read -r meta; do + report_dir=$(dirname "$meta") + parse_report_path "$report_dir" + branch=$(jq -r '.branch // empty' "$meta") + [ -n "$branch" ] || continue + + cache=$(active_branches_file "$REPO_OWNER" "$REPO_NAME") + if ! branch_active "$cache" "$branch"; then + delete_report_dir "$report_dir" "branch deleted: ${branch}" + fi +done < <(find "$DATA_ROOT" -path '*/reports/*/.meta' -type f 2>/dev/null) + +# Pass 2: age + keepMin for active branches (newest first) +SURVIVORS="${CACHE_DIR}/survivors.tsv" +: > "$SURVIVORS" + +while IFS= read -r meta; do + report_dir=$(dirname "$meta") + [ -d "$report_dir" ] || continue + parse_report_path "$report_dir" + branch=$(jq -r '.branch // empty' "$meta") + [ -n "$branch" ] || continue + + cache=$(active_branches_file "$REPO_OWNER" "$REPO_NAME") + branch_active "$cache" "$branch" || continue + + printf '%s\t%s\t%s\t%s\n' "${REPO_OWNER}/${REPO_NAME}" "$branch" "$report_dir" "$(age_days "$meta")" >> "$SURVIVORS" +done < <(find "$DATA_ROOT" -path '*/reports/*/.meta' -type f 2>/dev/null) + +current_key="" +kept=0 + +while IFS=$'\t' read -r repo_key branch report_dir days; do + key="${repo_key}|${branch}" + if [ "$key" != "$current_key" ]; then + current_key="$key" + kept=0 + max_age=$(rule_max_age "$branch") + keep_min=$(rule_keep_min "$branch") + fi + + if [ "$days" -gt "$max_age" ]; then + delete_report_dir "$report_dir" "older than ${max_age}d (branch ${branch})" + continue + fi + + kept=$((kept + 1)) + if [ "$kept" -gt "$keep_min" ]; then + delete_report_dir "$report_dir" "exceeds keepMin ${keep_min} (branch ${branch})" + fi +done < <(sort -t $'\t' -k1,1 -k2,2 -k4,4n "$SURVIVORS") + +echo "Retention cleanup finished." diff --git a/git-pages/files/retention-run.sh b/git-pages/files/retention-run.sh new file mode 100644 index 0000000..8fb4636 --- /dev/null +++ b/git-pages/files/retention-run.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Scale down git-pages, run PVC cleanup (RWO), scale back up. +set -euo pipefail + +NAMESPACE="${NAMESPACE:?NAMESPACE is required}" +DEPLOYMENT="${DEPLOYMENT:?DEPLOYMENT is required}" + +echo "Scaling ${DEPLOYMENT} to 0..." +kubectl scale "deployment/${DEPLOYMENT}" --replicas=0 -n "$NAMESPACE" +kubectl wait --for=delete pod \ + -l "app.kubernetes.io/name=git-pages,app.kubernetes.io/instance=${INSTANCE}" \ + -n "$NAMESPACE" --timeout=180s + +/scripts/retention-cleanup.sh + +echo "Scaling ${DEPLOYMENT} to 1..." +kubectl scale "deployment/${DEPLOYMENT}" --replicas=1 -n "$NAMESPACE" + +echo "Retention job done." diff --git a/git-pages/templates/NOTES.txt b/git-pages/templates/NOTES.txt new file mode 100644 index 0000000..61f07ec --- /dev/null +++ b/git-pages/templates/NOTES.txt @@ -0,0 +1,16 @@ +git-pages installed. + +See docs/secrets.md for secret prerequisites (K8s + Gitea Actions). + +Install: + helm upgrade --install git-pages ./git-pages -n {{ .Release.Namespace }} -f dev-values.yaml + +Host: https://{{ .Values.ingress.host }} +Reports: https://{{ .Values.ingress.host }}/{owner}/{repo}/reports/{sha8}/index.html + +Publish (CI): + PATCH https://{{ .Values.ingress.host }}/ + Authorization: Basic publish: + +Upgrade: helm upgrade {{ .Release.Name }} ./git-pages -n {{ .Release.Namespace }} -f dev-values.yaml +Uninstall: helm uninstall {{ .Release.Name }} -n {{ .Release.Namespace }} diff --git a/git-pages/templates/_helpers.tpl b/git-pages/templates/_helpers.tpl new file mode 100644 index 0000000..08418db --- /dev/null +++ b/git-pages/templates/_helpers.tpl @@ -0,0 +1,39 @@ +{{- define "git-pages.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "git-pages.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 }} + +{{- define "git-pages.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "git-pages.labels" -}} +helm.sh/chart: {{ include "git-pages.chart" . }} +{{ include "git-pages.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "git-pages.selectorLabels" -}} +app.kubernetes.io/name: {{ include "git-pages.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{- define "git-pages.componentLabels" -}} +{{ include "git-pages.labels" . }} +app.kubernetes.io/component: pages-server +{{- end }} diff --git a/git-pages/templates/certificate.yaml b/git-pages/templates/certificate.yaml new file mode 100644 index 0000000..a4d7377 --- /dev/null +++ b/git-pages/templates/certificate.yaml @@ -0,0 +1,15 @@ +{{- if and .Values.ingress.enabled .Values.certificate.enabled }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "git-pages.fullname" . }}-tls + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +spec: + secretName: git-pages-tls + dnsNames: + - {{ .Values.ingress.host | quote }} + issuerRef: + name: {{ .Values.certificate.issuerRef.name }} + kind: {{ .Values.certificate.issuerRef.kind }} +{{- end }} diff --git a/git-pages/templates/configmap.yaml b/git-pages/templates/configmap.yaml new file mode 100644 index 0000000..a13fe92 --- /dev/null +++ b/git-pages/templates/configmap.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "git-pages.fullname" . }}-config + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +data: + config.toml: | + log-format = "text" + + [server] + pages = "tcp/:3000" + caddy = "-" + metrics = "tcp/:3002" + + [storage] + type = "fs" + + [storage.fs] + root = "/app/data" diff --git a/git-pages/templates/deployment.yaml b/git-pages/templates/deployment.yaml new file mode 100644 index 0000000..535a845 --- /dev/null +++ b/git-pages/templates/deployment.yaml @@ -0,0 +1,68 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "git-pages.fullname" . }} + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + {{- include "git-pages.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "git-pages.componentLabels" . | nindent 8 }} + spec: + securityContext: + fsGroup: 1000 + containers: + - name: git-pages + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - git-pages + args: + - -config + - /etc/git-pages/config.toml + {{- if .Values.pagesInsecure }} + env: + - name: PAGES_INSECURE + value: "1" + {{- end }} + ports: + - name: http + containerPort: 3000 + protocol: TCP + - name: metrics + containerPort: 3002 + protocol: TCP + volumeMounts: + - name: config + mountPath: /etc/git-pages + readOnly: true + {{- if .Values.persistence.enabled }} + - name: data + mountPath: /app/data + {{- end }} + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 3 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumes: + - name: config + configMap: + name: {{ include "git-pages.fullname" . }}-config + {{- if .Values.persistence.enabled }} + - name: data + persistentVolumeClaim: + claimName: {{ include "git-pages.fullname" . }}-data + {{- end }} diff --git a/git-pages/templates/ingressroute.yaml b/git-pages/templates/ingressroute.yaml new file mode 100644 index 0000000..efb19b6 --- /dev/null +++ b/git-pages/templates/ingressroute.yaml @@ -0,0 +1,48 @@ +{{- if .Values.ingress.enabled }} +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: {{ include "git-pages.fullname" . }} + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +spec: + entryPoints: + - {{ .Values.ingress.entryPoints.websecure }} + routes: + - match: >- + Host(`{{ .Values.ingress.host }}`) && + (Method(`PATCH`) || Method(`PUT`)) + kind: Rule + middlewares: + - name: {{ include "git-pages.fullname" . }}-publish-auth + services: + - name: {{ include "git-pages.fullname" . }} + port: {{ .Values.service.port }} + - match: Host(`{{ .Values.ingress.host }}`) && (Method(`GET`) || Method(`HEAD`)) + kind: Rule + services: + - name: {{ include "git-pages.fullname" . }} + port: {{ .Values.service.port }} + tls: + secretName: git-pages-tls +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: {{ include "git-pages.fullname" . }}-http + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +spec: + entryPoints: + - {{ .Values.ingress.entryPoints.web }} + routes: + - match: >- + Host(`{{ .Values.ingress.host }}`) && + !PathPrefix(`/.well-known/acme-challenge/`) + kind: Rule + middlewares: + - name: {{ include "git-pages.fullname" . }}-https-redirect + services: + - name: {{ include "git-pages.fullname" . }} + port: {{ .Values.service.port }} +{{- end }} diff --git a/git-pages/templates/middleware.yaml b/git-pages/templates/middleware.yaml new file mode 100644 index 0000000..8cc55e5 --- /dev/null +++ b/git-pages/templates/middleware.yaml @@ -0,0 +1,22 @@ +{{- if .Values.ingress.enabled }} +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: {{ include "git-pages.fullname" . }}-publish-auth + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +spec: + basicAuth: + secret: git-pages-publish-auth +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: {{ include "git-pages.fullname" . }}-https-redirect + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +spec: + redirectScheme: + scheme: https + permanent: true +{{- end }} diff --git a/git-pages/templates/publish-auth-secret.yaml b/git-pages/templates/publish-auth-secret.yaml new file mode 100644 index 0000000..e6d36ca --- /dev/null +++ b/git-pages/templates/publish-auth-secret.yaml @@ -0,0 +1,12 @@ +{{- if and .Values.publishAuth.create .Values.publishAuth.htpasswdUsers }} +apiVersion: v1 +kind: Secret +metadata: + name: git-pages-publish-auth + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +type: Opaque +stringData: + users: | + {{ .Values.publishAuth.htpasswdUsers }} +{{- end }} diff --git a/git-pages/templates/pvc.yaml b/git-pages/templates/pvc.yaml new file mode 100644 index 0000000..a377929 --- /dev/null +++ b/git-pages/templates/pvc.yaml @@ -0,0 +1,17 @@ +{{- if .Values.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "git-pages.fullname" . }}-data + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.persistence.accessMode }} + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size }} +{{- end }} diff --git a/git-pages/templates/retention-configmap.yaml b/git-pages/templates/retention-configmap.yaml new file mode 100644 index 0000000..9f90370 --- /dev/null +++ b/git-pages/templates/retention-configmap.yaml @@ -0,0 +1,15 @@ +{{- if .Values.persistence.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: git-pages-retention + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +data: + retention.json: | + {{- .Values.retention.rules | toJson | nindent 4 }} + retention-cleanup.sh: | + {{- .Files.Get "files/retention-cleanup.sh" | nindent 4 }} + retention-run.sh: | + {{- .Files.Get "files/retention-run.sh" | nindent 4 }} +{{- end }} diff --git a/git-pages/templates/retention-cronjob.yaml b/git-pages/templates/retention-cronjob.yaml new file mode 100644 index 0000000..5963733 --- /dev/null +++ b/git-pages/templates/retention-cronjob.yaml @@ -0,0 +1,78 @@ +{{- if .Values.persistence.enabled }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: git-pages-retention + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +spec: + schedule: {{ .Values.retention.schedule | quote }} + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 1 + template: + metadata: + labels: + app.kubernetes.io/name: git-pages-retention + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + serviceAccountName: git-pages-retention + restartPolicy: OnFailure + containers: + - name: retention + image: "{{ .Values.retention.image.repository }}:{{ .Values.retention.image.tag }}" + imagePullPolicy: {{ .Values.retention.image.pullPolicy }} + securityContext: + runAsUser: 0 + command: + - bash + - -c + - | + set -euo pipefail + apt-get update -qq + apt-get install -y --no-install-recommends curl jq >/dev/null + chmod +x /scripts/retention-run.sh /scripts/retention-cleanup.sh + /scripts/retention-run.sh + env: + - name: NAMESPACE + value: {{ .Release.Namespace | quote }} + - name: DEPLOYMENT + value: {{ include "git-pages.fullname" . | quote }} + - name: INSTANCE + value: {{ .Release.Name | quote }} + - name: DATA_ROOT + value: /app/data + - name: RETENTION_CONFIG + value: /etc/retention/retention.json + - name: GITEA_API_URL + value: {{ required "retention.giteaApiUrl is required" .Values.retention.giteaApiUrl | quote }} + - name: GITEA_TOKEN + valueFrom: + secretKeyRef: + name: git-pages-retention-gitea + key: token + volumeMounts: + - name: data + mountPath: /app/data + - name: scripts + mountPath: /scripts + - name: config + mountPath: /etc/retention + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "git-pages.fullname" . }}-data + - name: scripts + configMap: + name: git-pages-retention + defaultMode: 0755 + - name: config + configMap: + name: git-pages-retention + items: + - key: retention.json + path: retention.json +{{- end }} diff --git a/git-pages/templates/retention-rbac.yaml b/git-pages/templates/retention-rbac.yaml new file mode 100644 index 0000000..9407887 --- /dev/null +++ b/git-pages/templates/retention-rbac.yaml @@ -0,0 +1,37 @@ +{{- if .Values.persistence.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: git-pages-retention + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: git-pages-retention + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +rules: + - apiGroups: ["apps"] + resources: ["deployments", "deployments/scale"] + verbs: ["get", "patch", "update"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: git-pages-retention + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: git-pages-retention +subjects: + - kind: ServiceAccount + name: git-pages-retention + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/git-pages/templates/service.yaml b/git-pages/templates/service.yaml new file mode 100644 index 0000000..91d0da1 --- /dev/null +++ b/git-pages/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "git-pages.fullname" . }} + labels: + {{- include "git-pages.componentLabels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + selector: + {{- include "git-pages.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP diff --git a/git-pages/values.yaml b/git-pages/values.yaml new file mode 100644 index 0000000..10bf696 --- /dev/null +++ b/git-pages/values.yaml @@ -0,0 +1,55 @@ +# Constants — shared across all instances. Do not put per-env values here. +# Per instance: use {env}-values.yaml (e.g. dev-values.yaml): +# helm install git-pages ./git-pages -n git-pages -f dev-values.yaml + +nameOverride: "" +fullnameOverride: "" + +image: + repository: codeberg.org/git-pages/git-pages + tag: "0.9.1" + pullPolicy: IfNotPresent + +pagesInsecure: true + +service: + type: ClusterIP + port: 3000 + +persistence: + enabled: true + accessMode: ReadWriteOnce + +resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 1000m + memory: 512Mi + +ingress: + enabled: true + entryPoints: + websecure: websecure + web: web + +certificate: + enabled: true + +# Optional Helm-managed secret — prefer manual create (see docs/secrets.md). +publishAuth: + create: false + htpasswdUsers: "" + +retention: + schedule: "0 3 * * *" + image: + repository: bitnami/kubectl + tag: "1.31.4" + pullPolicy: IfNotPresent + rules: + default: + maxAgeDays: 90 + keepMin: 5 + branches: {} diff --git a/scripts/publish-git-pages.sh b/scripts/publish-git-pages.sh new file mode 100755 index 0000000..05a5b79 --- /dev/null +++ b/scripts/publish-git-pages.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Publish a report directory to git-pages apex index-site via Traefik (BasicAuth). +# Public URL: https://{PAGES_HOST}/{owner}/{repo}/reports/{sha8}/index.html +set -euo pipefail + +REPORT_DIR="${1:-}" +PAGES_HOST="${PAGES_HOST:-}" +GIT_PAGES_PUBLISH_URL="${GIT_PAGES_PUBLISH_URL:-https://pages.helm-dev.keskikuja.site}" +GIT_PAGES_PUBLISH_TOKEN="${GIT_PAGES_PUBLISH_TOKEN:-}" +GIT_PAGES_PUBLISH_USER="${GIT_PAGES_PUBLISH_USER:-publish}" +REPO_SLUG="${GITHUB_REPOSITORY:-}" + +[ -n "$REPORT_DIR" ] || { echo "ERROR: report directory argument required" >&2; exit 1; } +[ -d "$REPORT_DIR" ] || { echo "ERROR: not a directory: $REPORT_DIR" >&2; exit 1; } +[ -n "$PAGES_HOST" ] || { echo "ERROR: PAGES_HOST is not set" >&2; exit 1; } +[ -n "$GIT_PAGES_PUBLISH_TOKEN" ] || { echo "ERROR: GIT_PAGES_PUBLISH_TOKEN is not set" >&2; exit 1; } +[ -n "$REPO_SLUG" ] || { echo "ERROR: GITHUB_REPOSITORY is not set" >&2; exit 1; } +[ -n "${GITHUB_SHA:-}" ] || { echo "ERROR: GITHUB_SHA is not set" >&2; exit 1; } + +OWNER="${REPO_SLUG%%/*}" +REPO="${REPO_SLUG##*/}" +SHA8="${GITHUB_SHA:0:8}" +REPORT_URL="https://${PAGES_HOST}/${OWNER}/${REPO}/reports/${SHA8}/index.html" + +PUBLISH_BASE="${GIT_PAGES_PUBLISH_URL%/}" +PUBLISH_SITE_URL="${PUBLISH_BASE}/" + +WORK=$(mktemp -d) +TAR=$(mktemp) +trap 'rm -rf "$WORK" "$TAR"' EXIT + +mkdir -p "$WORK/${OWNER}/${REPO}/reports/${SHA8}" +cp -a "$REPORT_DIR/." "$WORK/${OWNER}/${REPO}/reports/${SHA8}/" +cat > "$WORK/${OWNER}/${REPO}/reports/${SHA8}/.meta" <&2 + cat /tmp/git-pages-publish-response.txt >&2 + exit 1 + ;; +esac + +echo "$REPORT_URL" diff --git a/tmp/0006-ci-feature-example/roles-and-files.md b/tmp/0006-ci-feature-example/roles-and-files.md new file mode 100644 index 0000000..a73ee29 --- /dev/null +++ b/tmp/0006-ci-feature-example/roles-and-files.md @@ -0,0 +1,449 @@ +# ci-engine.yml — moottori ja consumerin job-tiedostot + +> **Tila:** DRAFT — työstettävä esimerkki, ei lopullinen suunnitelma. +> **Liittyy:** ticket 0006, [analysis/ci-flow-values-vs-native-config.md](../../docs/analysis/ci-flow-values-vs-native-config.md) + +Esimerkkiprojekti: `temperature-store` (Java/Maven-mikropalvelu) + +Provider (`gitea-ci-library`) tarjoaa **vain yhden tiedoston**: `ci-engine.yml`. +Consumer tuo omat job-tiedostonsa ja datansa. Moottori päättelee itse mitä ajaa. + +--- + +## Tiedostot ja roolit + +``` +temperature-store/ ← CONSUMER +├── .gitea/workflows/ +│ └── ci.yml ← [A] Kutsuja: molemmat jobit (feature + master) yhdessä +├── ci-flow-values.yaml ← [B] Projektin data — uniikki per projekti +└── pom.xml + +gitea-ci-library/ ← PROVIDER (= MOOTTORI) +├── .gitea/workflows/ +│ └── ci-engine.yml ← [C] AINOA workflow — kaikki pipelinetyypit +├── scripts/ +│ ├── report-status.sh +│ ├── dispatch-workflow.sh +│ ├── push-reports.sh +│ └── tag-commit.sh +└── ci-flow-values.yaml ← Provider-testaus (dogfood — kirjasto testaa itsensä) +``` + +**Roolit:** + +| Tiedosto | Omistaja | Rooli | +|----------|----------|-------| +| `ci-engine.yml` | **Provider** | Moottori: build-logiikka, konffaus, komennot | +| `ci.yml` | **Consumer** | Kutsuja: molemmat jobit yhdessä tiedostossa, `if:` valitsee | +| `ci-flow-values.yaml` | **Consumer** | Data: ekosysteemi, docker-nimi, test-flow jne | +| `scripts/*.sh` | **Provider** | Jaetut työkalut | + +--- + +## [A] Consumerin `.gitea/workflows/ci.yml` — rooli: **kutsuja** + +```yaml +# temperature-store/.gitea/workflows/ci.yml +# +# Rooli: KUTSUJA. +# - Valitsee branchin perusteella oikean jobin (feature / master) +# - Välittää KONTIT moottorille (moottori ei tunne yhtään konttia) +# - Välittää config-filen polun moottorille + +name: CI + +on: + push: + branches: ["**"] + workflow_dispatch: + +jobs: + feature: + if: github.ref != 'refs/heads/master' + uses: org/gitea-ci-library/.gitea/workflows/ci-engine.yml@v1 + secrets: inherit + with: + config-file: ci-flow-values.yaml + maven-image: maven:3.9-eclipse-temurin-21 # ← Consumer omistaa kontit + + master: + if: github.ref == 'refs/heads/master' + uses: org/gitea-ci-library/.gitea/workflows/ci-engine.yml@v1 + secrets: inherit + with: + config-file: ci-flow-values.yaml + maven-image: maven:3.9-eclipse-temurin-21 + dind-image: docker:26-dind # ← Master tarvitsee DinD:n +``` + +**Mitä tässä tapahtuu:** +1. Push mihin tahansa branchiin → `ci.yml` triggeröityy +2. `if: github.ref != 'refs/heads/master'` → `feature`-job +3. `if: github.ref == 'refs/heads/master'` → `master`-job +4. Molemmat kutsuvat samaa moottoria (`ci-engine.yml@v1`) samoilla parametreilla +5. Moottori päättelee itse branchista mitä pipelinea ajaa + +**Miksi yksi tiedosto, kaksi jobia:** +- Yksi paikka katsoa mitä CI tekee — `ci.yml` on koko projektin CI-määrittely +- `if:`-ehto on riittävä erottelemaan featuren ja masterin +- Kopioitavissa sellaisenaan — consumer ei muokkaa mitään + +--- + +## [B] Consumerin `ci-flow-values.yaml` — rooli: **projektidata** + +```yaml +# temperature-store/ci-flow-values.yaml +# +# Rooli: DATA ("mikä tämä projekti on, mitä se tarvitsee") +# Tämä tiedosto on jokaisessa projektissa ERI. +# Kaikki non-secret-arvot tänne. Tokenit → Gitea org secrets. + +build: + ecosystem: maven # maven | gradle | npm — moottori valitsee kontin ja komennot + +docker: + registry: gitea + imageName: temperature-store + +sonarqube: + url: https://sonar.example.com + projectKey: temperature-store + +deployment: + projectFolder: microservices + fileName: values-{.environment}.yaml + property: container.version + +test-flow: + - deploy: development + wait: true + - test: + name: "integration fast" + environment: integration + repo: tests/integration + workflow: test.yml + ref: main + tags: "@temperature and not @slow" +``` + +**Mitä `ci-engine.yml` lukee tästä (MVP, tiketti 0006):** +- `build.ecosystem` — määrittää kontin ja komennot + +**Non-secret vs secret:** +- Tänne: ekosysteemi, image-nimi, URL:t, test-flow — versioitavaa +- Gitea org secrets: `GITEA_TOKEN`, `SONAR_TOKEN`, `DEPLOY_TOKEN` — salaisuudet + +--- + +## [C] Providerin `ci-engine.yml` — rooli: **moottori** + +Providerin **ainoa** workflow. Moottori koostuu kolmesta kerroksesta: + +| Kerros | Mistä tulee | Sisältö | +|--------|-------------|---------| +| **Kontit** | `with:` parametrit consumerilta | `maven-image`, `dind-image`, … — moottori ei tunne yhtään konttia | +| **Pipeline runko** | `ci-engine.yml` sisäinen | Feature-skeleton: start → test → coverage → end
Master-skeleton: start → build → deploy → [test-flow] → end | +| **Test plan + data** | `ci-flow-values.yaml` | `build.ecosystem` → komennot
`test-flow[]` → test plan (vain master)
`docker.*`, `deployment.*` (vain master) | + +```yaml +# gitea-ci-library/.gitea/workflows/ci-engine.yml +# +# MOOTTORI — kolme kerrosta: +# 1. Kontit: ${{ inputs.maven-image }} jne (consumerilta) +# 2. Pipeline runko: job-graafi erikseen featurelle ja masterille (täällä) +# 3. Data: ci-flow-values.yaml → ekosysteemi, test plan, docker (consumerilta) + +name: CI Engine + +on: + workflow_call: + inputs: + config-file: + required: true + type: string # ← Polku ci-flow-values.yaml:aan + maven-image: # ← KONTTI: consumerilta + required: false + type: string + dind-image: # ← KONTTI: consumerilta (vain master) + required: false + type: string + node-image: # ← KONTTI: consumerilta (vain npm) + required: false + type: string + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: false + +jobs: + # ═══════════════════════════════════════════════════════════════ + # YHTEINEN: konfiguraation luku (data-kerros) + # Raportoi: pending → config luettu → success + # ═══════════════════════════════════════════════════════════════ + start: + runs-on: ubuntu-latest + outputs: + ecosystem: ${{ steps.read-config.outputs.ecosystem }} + steps: + - uses: actions/checkout@v4 + + - name: Report pipeline start + run: | + bash scripts/report-status.sh \ + "pending" "Pipeline starting" \ + "$GITEA_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_NUMBER" \ + "ci/start" + + - name: Read project config + id: read-config + run: | + ECOSYSTEM=$(yq '.build.ecosystem' "${{ inputs.config-file }}") + [ -z "$ECOSYSTEM" ] || [ "$ECOSYSTEM" = "null" ] && \ + echo "ERROR: build.ecosystem not set" >&2 && exit 1 + echo "ecosystem=$ECOSYSTEM" >> $GITHUB_OUTPUT + + - name: Report pipeline started + if: success() + run: | + bash scripts/report-status.sh \ + "success" "Pipeline started" \ + "$GITEA_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_NUMBER" \ + "ci/start" + + - name: Report start failure + if: failure() + run: | + bash scripts/report-status.sh \ + "failure" "Pipeline start failed" \ + "$GITEA_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_NUMBER" \ + "ci/start" + + # ═══════════════════════════════════════════════════════════════ + # FEATURE-SKELETON: testit + coverage + # Ajetaan kun github.ref != refs/heads/master + # Jokainen steppi raportoi: alussa pending, lopussa success/failure + # ═══════════════════════════════════════════════════════════════ + unit-test: + needs: start + if: github.ref != 'refs/heads/master' + runs-on: ubuntu-latest + container: + image: ${{ inputs.maven-image }} + steps: + - uses: actions/checkout@v4 + + - name: Report unit-test start + run: | + bash scripts/report-status.sh \ + "pending" "Unit tests running" \ + "$GITEA_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_NUMBER" \ + "ci/unit-test" + + - name: Run unit tests + id: test + env: + ECOSYSTEM: ${{ needs.start.outputs.ecosystem }} + run: | + case "$ECOSYSTEM" in + maven|gradle) mvn test ;; + npm) npm test ;; + esac + + - name: Report unit-test success + if: success() + run: | + bash scripts/report-status.sh \ + "success" "Unit tests passed" \ + "$GITEA_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_NUMBER" \ + "ci/unit-test" + + - name: Report unit-test failure + if: failure() + run: | + bash scripts/report-status.sh \ + "failure" "Unit tests failed" \ + "$GITEA_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_NUMBER" \ + "ci/unit-test" + + code-coverage: + needs: [start, unit-test] + if: github.ref != 'refs/heads/master' + runs-on: ubuntu-latest + container: + image: ${{ inputs.maven-image }} + steps: + - uses: actions/checkout@v4 + + - name: Report coverage start + run: | + bash scripts/report-status.sh \ + "pending" "Coverage running" \ + "$GITEA_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_NUMBER" \ + "ci/code-coverage" + + - name: Run coverage + id: coverage + env: + ECOSYSTEM: ${{ needs.start.outputs.ecosystem }} + run: | + case "$ECOSYSTEM" in + maven) mvn jacoco:report ;; + gradle) gradle jacocoTestReport ;; + npm) npm run coverage ;; + esac + + - name: Report coverage success + if: success() + run: | + bash scripts/report-status.sh \ + "success" "Coverage report generated" \ + "$GITEA_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_NUMBER" \ + "ci/code-coverage" + + - name: Report coverage failure + if: failure() + run: | + bash scripts/report-status.sh \ + "failure" "Coverage failed" \ + "$GITEA_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_NUMBER" \ + "ci/code-coverage" + + # ═══════════════════════════════════════════════════════════════ + # MASTER-SKELETON: build + deploy + test plan (tiketti 0009) + # github.ref == refs/heads/master + # Kontti: ${{ inputs.maven-image }} + ${{ inputs.dind-image }} + # Test plan: ci-flow-values.yaml → test-flow[] + # ═══════════════════════════════════════════════════════════════ + # build-container: + # needs: start + # if: github.ref == 'refs/heads/master' + # container: + # image: ${{ inputs.dind-image }} ← KONTTI consumerilta + # ... + # + # deploy: + # needs: build-container + # if: github.ref == 'refs/heads/master' + # ...lukee deployment.* configista... + # + # test-flow: + # needs: deploy + # if: github.ref == 'refs/heads/master' + # ...lukee test-flow[] configista... ← TEST PLAN configista + + # ═══════════════════════════════════════════════════════════════ + # LOPETUS: kaikille yhteinen + # Raportoi: pending → aggregoi tulokset → success/failure + # ═══════════════════════════════════════════════════════════════ + end: + needs: [unit-test, code-coverage] + if: always() + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Report pipeline ending + run: | + bash scripts/report-status.sh \ + "pending" "Pipeline finishing" \ + "$GITEA_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_NUMBER" \ + "ci/end" + + - name: Report final status + run: | + BUILD_URL="$GITEA_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_NUMBER" + if [ "${{ needs.unit-test.result }}" = "success" ] && \ + [ "${{ needs.code-coverage.result }}" = "success" ]; then + STATE="success"; DESC="All checks passed" + else + STATE="failure"; DESC="Some checks failed" + fi + bash scripts/report-status.sh "$STATE" "$DESC" "$BUILD_URL" "ci/end" +``` + +**Moottorin kolme kerrosta käytännössä:** + +1. **Kontit** (consumerilta `with:` → moottori `inputs:` → job `container.image`) + - Moottori ei sisällä yhtään kontti-imagea + - Consumer päättää versiot: `maven:3.9-eclipse-temurin-21` + +2. **Pipeline runko** (moottorin sisäinen job-graafi) + - `if: github.ref != 'refs/heads/master'` → feature-skeleton + - `if: github.ref == 'refs/heads/master'` → master-skeleton (myöhemmät tiketit) + +3. **Data** (`ci-flow-values.yaml`) + - `build.ecosystem` → mitä komentoja ajetaan + - `test-flow[]` → test plan (vain master, tiketti 0009) + +**Mitä tässä EI ole (myöhemmät tiketit):** +- `push-reports.sh` — raporttien julkaisu MinIO:hon (tiketti 0013) +- `isContainerBuild()` / kontin buildaus (tiketti 0009) +- SonarQube quality gate (tiketti 0009) +- Test flow -ketjutus `test-flow[]`:sta (tiketti 0008) + +--- + +## Yhteenveto + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ CONSUMER: temperature-store │ +│ │ +│ ci.yml ci-flow-values.yaml │ +│ ┌──────────────────────────┐ ┌────────────────────────┐ │ +│ │ jobs: │ │ build: │ │ +│ │ feature (if !master) ───┼─┐ │ ecosystem: maven │ │ +│ │ with: │ │ │ docker: │ │ +│ │ config-file + kontit │ │ │ imageName: ... │ │ +│ │ │ │ │ test-flow: [...] │ │ +│ │ master (if master) ────┼─┤ └────────────────────────┘ │ +│ │ with: │ │ │ +│ │ config-file + kontit │ │ │ +│ └──────────────────────────┘ │ │ +└───────────────────────────────┼───────────────────────────────────────┘ + │ with: kontit + config-file + ▼ +┌──────────────────────────────────────────────────────────────────────┐ +│ PROVIDER: gitea-ci-library (ci-engine.yml) │ +│ │ +│ Kerros 1: KONTIT (consumerilta) │ +│ ┌──────────────────────────────────────────┐ │ +│ │ inputs: maven-image, dind-image, ... │ │ +│ └──────────────────────────────────────────┘ │ +│ │ +│ Kerros 2: PIPELINE RUNKO (moottorin sisäinen) │ +│ ┌──────────────────────────────────────────┐ │ +│ │ FEATURE-SKELETON MASTER-SKELETON │ │ +│ │ start ──────────────────── start │ │ +│ │ ↓ ↓ │ │ +│ │ unit-test build-container│ │ +│ │ ↓ ↓ │ │ +│ │ code-coverage deploy │ │ +│ │ ↓ ↓ │ │ +│ │ end test-flow[] ───│── Kerros 3: TEST PLAN │ +│ │ ↓ │ │ +│ │ end │ │ +│ └──────────────────────────────────────────┘ │ +│ │ +│ Kerros 3: DATA (ci-flow-values.yaml) │ +│ ┌──────────────────────────────────────────┐ │ +│ │ ecosystem → komennot │ │ +│ │ test-flow[] → test plan (vain master) │ │ +│ │ docker.*, deployment.* (vain master) │ │ +│ └──────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +### Päätöstaulukko + +| Asia | Sijainti | Kerros | +|------|----------|:--:| +| Kontit (`maven-image`, `dind-image`) | Consumer (`ci.yml` `with:`) | 1 | +| Pipeline runko (job-graafi) | Provider (`ci-engine.yml`) | 2 | +| `build.ecosystem` → komennot | Consumer (`ci-flow-values.yaml`) | 3 | +| `test-flow[]` → test plan | Consumer (`ci-flow-values.yaml`) | 3 | +| `docker.*`, `deployment.*` | Consumer (`ci-flow-values.yaml`) | 3 | +| Skriptit | Provider (`scripts/`) | — | +| Tokenit, salasanat | Gitea org secrets | — | diff --git a/tmp/data-flow-design.md b/tmp/data-flow-design.md new file mode 100644 index 0000000..18832a7 --- /dev/null +++ b/tmp/data-flow-design.md @@ -0,0 +1,282 @@ +# Data flow — periaatetaso + +> Tila: DRAFT — kevyt suunnitteludokumentti, muokataan tarpeen mukaan. +> Ei ADR. Ei normatiivinen. Kuvaa periaatteen, ei implementaatiota. +> +> Lähde: [docs/design-rationale.md](../docs/design-rationale.md) — erityisesti periaatteet 1 (commit-status), 3 (konfiguraatio repossa), 5 (raportit MinIO:ssa) ja 7 (cross-repo traceability). + +## Tarkoitus + +Selventää **kuka omistaa mitä datan** ja **miten se liikkuu** consumer → provider → Gitea -ketjussa. + +**Ydinperiaate:** Branch-päätös on consumerin vastuulla. Provider on branch-agnostinen — se saa polun config-tiedostoon ja suorittaa sen määrittämän pipelinen. Consumer valitsee kumman jobin ja minkä pipeline-as-conf -tiedoston ajetaan; engine lukee vain sen mitä `with:` välittää. + +--- + +## Roolit ja tiedostot + +| Rooli | Tiedosto(t) | Mitä tietää / mitä tekee | +|-------|-------------|--------------------------| +| **Consumer** | `ci.yml` | Trigger, branch (`if:`), valitsee pipeline-as-conf -tiedoston ja kontit, kutsuu engineä `uses:` | +| **Consumer** | `*-conf.yaml` (pipeline-as-conf) | Projektin data: mitä pipeline ajaa (ekosysteemi, test-flow, docker jne.) — versioitu repossa | +| **Provider** | `ci-engine.yml` | Gitea workflow (`.gitea/workflows/`); `workflow_call` only; pipeline-määrittely | +| **Provider** | jaettu suorituskoodi | Uudelleenkäytettävä logiikka — **ei** Gitea workflow; engine kutsuu steppien sisällä | +| **Gitea org** | secrets | Tokenit, salasanat — ei koskaan repoon; `secrets: inherit` | +| **Gitea org** | variables (kapea poikkeus) | Org-laajuiset infra-endpointit (esim. `MINIO_BASE_URL`, `GITEA_SERVER_URL`) — valinnainen shortcut | + +### Secrets vs. variables vs. conf + +Kolmitasoinen jako — helppo sääntö kehittäjälle: + +| Tyyppi | Missä | Esimerkkejä | +|--------|-------|-------------| +| **Secrets** | Gitea org secrets | `GITEA_TOKEN`, `SONAR_TOKEN`, `DEPLOY_TOKEN` | +| **Projektidata** | Consumer `*-conf.yaml` | `build.ecosystem`, `docker.imageName`, `sonarqube.*`, `test-flow`, `deployment.*` | +| **Org-infra** | Gitea org variables (valinnainen) | `MINIO_BASE_URL`, `GITEA_SERVER_URL` — sama kaikille, harvoin muuttuu | + +1. Salainen? → org secret +2. Projektin asia? → conf-tiedosto (periaate 3) +3. Koko orgin sama infra-endpoint? → org variable, vain jos et halua toistaa sitä jokaisessa confissa + +Org variables on **kapea poikkeus** infra-endpointeille — ei korvike conf-tiedostolle. Projektikohtaiset arvot (docker-nimi, test-flow, Sonar projectKey) kuuluvat aina confiin. + +### Pipeline vs. jaettu suorituskoodi + +Kaksi eri artefaktityyppiä provider-puolella: + +``` +Provider (gitea-ci-library) +├── .gitea/workflows/ci-engine.yml ← pipeline (Gitea workflow, consumer kutsuu uses:) +└── jaettu suorituskoodi ← uudelleenkäytettävä logiikka (EI .gitea/workflows/:ssa) + +Consumer (mikropalvelu) +├── .gitea/workflows/ci.yml ← ohut kutsuja (Gitea workflow) +└── ci-feature.yaml ← data (ei suoritettavaa koodia) +``` + +- **Pipeline-tiedosto** (`ci-engine.yml`) on Gitean workflow — sen täytyy olla `.gitea/workflows/`:ssa, jotta `uses:` löytää sen. +- **Jaettu suorituskoodi** on providerin omistamaa logiikkaa (status-raportointi, raporttien julkaisu, dispatch jne.). Consumer ei kopioi sitä — korjaus provider-repoon, kaikki consumerit hyötyvät `@v1`:stä. +- **Toteutus** (kieli, tiedostomuoto, sijainti repossa) ei kuulu tähän dokumenttiin. + +### Binding: consumer → provider + +Ei dynaamista löytämistä — consumer kovakoodaa `uses:`-polun: + +``` +org/gitea-ci-library/.gitea/workflows/ci-engine.yml@v1 +``` + +| Vaihe | Mitä tapahtuu | +|-------|---------------| +| 1 | Push consumer-repoon → Gitea lukee consumerin `ci.yml` | +| 2 | `if:` valitsee jobin | +| 3 | `uses:` → Gitea hakee provider-reposta `ci-engine.yml` annetulla `@ref`:llä | +| 4 | Provider vaatii `on: workflow_call` | +| 5 | `with:` → providerin `inputs`; checkout = **consumer-repo** (lähdekoodi + config-file) | +| 6 | Engine tuo provider-artefaktit runnerille tagilla `@v1` | + +**`@ref`:** `@v1` (tag providerin main-haarassa). Pinnaa engine-version — consumer ei seuraa providerin kehityshaaraa. + +### Dogfood-erikoistapaus + +`gitea-ci-library` sisältää sekä consumer- että provider-tiedostot samassa repossa. **Roolit eivät muutu** — vain fyysinen sijainti on poikkeuksellinen. + +**Dogfood-sääntö:** Consumerin `ci.yml` viittaa provideriin **samalla `uses:`-polulla** kuin mikä tahansa ulkoinen mikropalvelu. Ei suhteellista polkua (`./.gitea/workflows/...`). + +``` +Ulkoinen mikropalvelu (puhdas consumer) gitea-ci-library (dogfood) +┌──────────────────────────────┐ ┌──────────────────────────────┐ +│ ci.yml │ │ ci.yml ← consumer │ +│ ci-feature.yaml │ │ ci-feature.yaml ← consumer │ +│ ci-main.yaml │ │ ci-main.yaml ← consumer │ +│ │ │ ci-engine.yml ← provider │ +│ uses: org/gitea-ci-library/ │ │ uses: org/gitea-ci-library/ │ +│ .../ci-engine.yml@v1 │ │ .../ci-engine.yml@v1 │ +└──────────────────────────────┘ │ suorituskoodi ← provider │ + └──────────────────────────────┘ +``` + +Ulkoinen mikropalvelu on puhdas consumer (vain `ci.yml` + conf-tiedostot). Provider tulee `uses:`-viittauksella kirjastosta — dogfood mukaan lukien. + +--- + +## Kuusi data-virtaa + +``` +1. CONSUMER → PROVIDER (push) + Consumer tietää branchin → valitsee jobin + config-file + kontit. + Provider vastaanottaa, ei tulkitse branchia. + + ci.yml ──with: config-file + kontti-imaget──► ci-engine.yml + +2. PROVIDER SISÄINEN + Engine lukee config-file-polun → rakentaa steppigraafin → ajaa stepit. + Mitä pipelinea ajetaan johtuu conf-tiedostosta, ei branch-tiedosta providerissa. + +3. PROVIDER → GITEA (commit-status) + Jokainen steppi raportoi tilansa commitille (Gitea REST API). + Uniikki key per vaihe (periaate 1). + +4. GITEA → CONSUMER (feedback loop) + Consumerin commitille kertyy statuksia jokaisesta stepistä. + Kehittäjä näkee suoraan commitilta: mitä ajettiin, menikö läpi. + url-kenttä linkittää raportteihin / buildiin. + +5. PROVIDER → MinIO → GITEA (raportit) + Steppi generoi raportin → julkaisu MinIO:hon + → deterministinen URL → URL liitetään commit-statusviestiin + (periaate 5). + +6. CROSS-REPO KETJU + Mikropalvelu-commit (root) → dispatch → toinen repo → status takaisin root-committiin. + root-build kulkee inputs-parametrina koko ketjun läpi (periaate 7). +``` + +--- + +## Silmukka + +``` +Consumer pushaa koodia + │ + ├── feature-branch → ci.yml valitsee feature-jobin + │ with: config-file: ci-feature.yaml + kontit + │ + └── main-branch → ci.yml valitsee main-jobin + with: config-file: ci-main.yaml + kontit + │ + ▼ uses: +┌──────────────────────────────────────────────┐ +│ ci-engine.yml (provider, branch-agnostinen) │ +│ Lukee config-file → steppigraafi → stepit │ +│ │ +│ config-file → steppigraafi → stepit ajetaan │ +│ │ │ │ │ +│ └────────────┴────────────┘ │ +│ │ │ +│ ▼ │ +│ jokainen steppi → status commitille │ +│ │ │ +│ (raportit) → MinIO → url statusiin │ +└──────────────────┬───────────────────────────┘ + │ POST /api/v1/repos/.../statuses/{sha} + ▼ +┌──────────────────────────────────────────────┐ +│ Gitea commit │ +│ ├── ci/start ✓ │ +│ ├── ci/unit-test ✓ (url → raportti) │ +│ ├── ci/code-coverage ✓ │ +│ └── ci/end ✓ │ +└──────────────────────────────────────────────┘ +``` + +### Cross-repo ketju (periaate 7) + +``` +Mikropalvelu (root-commit abc123) + │ + ├── build + deploy ──dispatch──► Helm-repo (commit def456) + │ │ + │ ├── status → oma commit + │ └── status → root abc123 + │ + └── test-flow ──dispatch──► Testi-repo (commit ghi789) + │ + ├── status → oma commit + ├── status → root abc123 + └── status → Helm def456 + +root-build (abc123) kulkee workflow_dispatch inputs -parametrina koko ketjun läpi. +``` + +--- + +## Omistajuus + +| Mitä | Omistaja | Miten liikkuu | +|------|----------|---------------| +| Branch-päätös | Consumer | `ci.yml` `if:` | +| Mitä pipelinea ajetaan | Consumer | Valittu pipeline-as-conf -tiedosto | +| Pipeline-runko (steppigraafi) | Provider | Engine rakentaa confista | +| Kontit (maven, dind…) | Consumer | `with:` → provider `inputs` | +| Projektidata (ecosystem, test-flow…) | Consumer | pipeline-as-conf repossa | +| Moottori + jaettu suorituskoodi | Provider | `ci-engine.yml` + provider-logiikka | +| Tokenit, salasanat | Gitea org | secrets — `secrets: inherit` | +| Org-laajuiset infra-endpointit | Gitea org | variables (valinnainen poikkeus) | + +--- + +## Consumer-sopimus + +`ci.yml` on **mahdollisimman lyhyt**: + +- Kaksi jobia (feature / main) tai vastaava `if:`-jaottelu +- Sama engine, eri `config-file` per job +- Kontit consumerin `with:`:ssa — provider ei sisällä image-versioita +- `secrets: inherit` + +```yaml +# Esimerkki — kovakoodattu provider-polku (provider main-haarassa, tag @v1) + +jobs: + feature: + if: gitea.ref != 'refs/heads/main' + uses: org/gitea-ci-library/.gitea/workflows/ci-engine.yml@v1 + secrets: inherit + with: + config-file: ci-feature.yaml + maven-image: ... + + main: + if: gitea.ref == 'refs/heads/main' + uses: org/gitea-ci-library/.gitea/workflows/ci-engine.yml@v1 + secrets: inherit + with: + config-file: ci-main.yaml + maven-image: ... + dind-image: ... +``` + +--- + +## Provider-sopimus + +- Vain `workflow_call` — ei omaa triggeriä +- Pakollinen input: `config-file` (polku repossa) +- Valinnaiset: kontti-imaget consumerilta +- Ei branch-ehtoja provider-tiedostossa — consumer on jo päättänyt mitä ajetaan conf-valinnalla +- Lukee consumer-datan (lähdekoodi + config-file) ja tuo provider-artefaktit runnerille +- Lukee conf → suorittaa → raportoi + +### Mitä engine EI tiedä + +- Onko kyseessä feature vai main — consumer on jo valinnut conf-tiedoston +- Mitä komentoja ajetaan — ne tulevat conf-tiedostosta +- Mitä kontteja on käytössä — consumer kertoo `with:`:ssa + +Engine on branch-agnostinen suorittaja. Consumer omistaa päätökset ja datan. + +--- + +## Esimerkki: gitea-ci-library (dogfood) + +Tämä projekti on sekä provider että consumer. Roolit pysyvät samoina kuin ulkoisella mikropalvelulla — **sama `uses:`-viittaus** (`@v1`), ei suhteellista polkua. Consumer-sopimuksen esimerkki pätee sellaisenaan. + +| Rooli | Tiedosto | Mitä tekee | +|-------|----------|------------| +| Consumer | `ci.yml` | Tietää branchin, valitsee jobin ja conf-tiedoston, syöttää kontit | +| Consumer | `ci-feature.yaml`, `ci-main.yaml` | Pipeline-as-conf — projektin data per pipeline-tyyppi | +| Provider | `ci-engine.yml` | Pipeline-määrittely; lukee config-file:n, rakentaa steppigraafin, ajaa, raportoi | +| Provider | jaettu suorituskoodi | Status, raportit, dispatch — toteutus avoin | + +--- + +## Mitä tämä EI ota kantaa + +- Moottorin toteutuskieli ja -muoto (bash, TypeScript, Python, Go — erillinen päätös) +- Nykyiset skriptitiedostot ja niiden rajapinnat (mahdollinen redesign) +- Raporttien julkaisumekanismi (miten MinIO:hon — toteutus avoin) +- Provider-artefaktien toimitus runnerille (checkout, bundle, inline — toteutus myöhemmin) +- Main-skeletonin yksityiskohdat (build, deploy, test-flow) +- Conf-tiedoston skeema / kentät (→ `config-model.md`) +- Cross-repon workflow-tiedostojen sisältö diff --git a/tmp/git-pages.yaml b/tmp/git-pages.yaml new file mode 100644 index 0000000..688d8f7 --- /dev/null +++ b/tmp/git-pages.yaml @@ -0,0 +1,259 @@ +# DEPRECATED — käytä Helm chartia: git-pages/ +# helm upgrade --install git-pages ./git-pages -n git-pages -f dev-values.yaml +# +# git-pages — k3s homelab (Codeberg git-pages) +# +# Yksi apex index-site (pages.helm-dev.../.index). Sisältö Gitea-poluissa: +# {owner}/{repo}/reports/{sha8}/index.html +# +# Julkaisu (CI → Traefik → git-pages): +# PATCH https://pages.helm-dev.keskikuja.site/ Authorization: Basic publish: +# Traefik basicAuth middleware — token K8s-secretissä (htpasswd) +# git-pages PAGES_INSECURE=1 takana — ei forge/DNS/Gitea write +# +# Secret (kerran, ennen ensimmäistä publishia): +# export GIT_PAGES_PUBLISH_TOKEN="$(openssl rand -base64 32)" +# kubectl -n git-pages create secret generic git-pages-publish-auth \ +# --from-literal=users="$(docker run --rm httpd:2-alpine htpasswd -nb publish "$GIT_PAGES_PUBLISH_TOKEN")" +# # Sama arvo → Gitea Actions secret GIT_PAGES_PUBLISH_TOKEN +# +# Lukeminen: GET/HEAD julkinen (OIDC myöhemmin Traefikissä) +# +# URL-esimerkki: +# https://pages.helm-dev.keskikuja.site/niko/gitea-ci-library/reports/abc12345/index.html +# +# Image: codeberg.org/git-pages/git-pages:0.9.1 +# CI: GIT_PAGES_PUBLISH_URL=https://pages.helm-dev.keskikuja.site +# GIT_PAGES_PUBLISH_TOKEN → Gitea Actions secret + +--- +apiVersion: v1 +kind: Namespace +metadata: + name: git-pages + labels: + app.kubernetes.io/name: git-pages + app.kubernetes.io/component: pages-server + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: git-pages-config + namespace: git-pages + labels: + app.kubernetes.io/name: git-pages +data: + config.toml: | + log-format = "text" + + [server] + pages = "tcp/:3000" + caddy = "-" + metrics = "tcp/:3002" + + [storage] + type = "fs" + + [storage.fs] + root = "/app/data" + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: git-pages-data + namespace: git-pages + labels: + app.kubernetes.io/name: git-pages +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: git-pages + namespace: git-pages + labels: + app.kubernetes.io/name: git-pages + app.kubernetes.io/component: pages-server +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: git-pages + template: + metadata: + labels: + app.kubernetes.io/name: git-pages + app.kubernetes.io/component: pages-server + spec: + securityContext: + fsGroup: 1000 + containers: + - name: git-pages + image: codeberg.org/git-pages/git-pages:0.9.1 + imagePullPolicy: IfNotPresent + command: + - git-pages + args: + - -config + - /etc/git-pages/config.toml + env: + - name: PAGES_INSECURE + value: "1" + ports: + - name: http + containerPort: 3000 + protocol: TCP + - name: metrics + containerPort: 3002 + protocol: TCP + volumeMounts: + - name: config + mountPath: /etc/git-pages + readOnly: true + - name: data + mountPath: /app/data + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 3 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 1000m + memory: 512Mi + volumes: + - name: config + configMap: + name: git-pages-config + - name: data + persistentVolumeClaim: + claimName: git-pages-data + +--- +apiVersion: v1 +kind: Service +metadata: + name: git-pages + namespace: git-pages + labels: + app.kubernetes.io/name: git-pages +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: git-pages + ports: + - name: http + port: 3000 + targetPort: http + protocol: TCP + +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: git-pages-tls + namespace: git-pages + labels: + app.kubernetes.io/name: git-pages +spec: + secretName: git-pages-tls + dnsNames: + - pages.helm-dev.keskikuja.site + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + +--- +# PATCH/PUT vaatii BasicAuth (publish-token). Ilman tokenia → 401. +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: git-pages-publish-auth + namespace: git-pages + labels: + app.kubernetes.io/name: git-pages +spec: + basicAuth: + secret: git-pages-publish-auth + +--- +# Julkinen luku: GET/HEAD. Julkaisu: PATCH/PUT + basicAuth (erillinen reitti). +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: git-pages + namespace: git-pages + labels: + app.kubernetes.io/name: git-pages +spec: + entryPoints: + - websecure + routes: + - match: >- + Host(`pages.helm-dev.keskikuja.site`) && + (Method(`PATCH`) || Method(`PUT`)) + kind: Rule + middlewares: + - name: git-pages-publish-auth + services: + - name: git-pages + port: 3000 + - match: Host(`pages.helm-dev.keskikuja.site`) && (Method(`GET`) || Method(`HEAD`)) + kind: Rule + services: + - name: git-pages + port: 3000 + tls: + secretName: git-pages-tls + +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: https-redirect + namespace: git-pages + labels: + app.kubernetes.io/name: git-pages +spec: + redirectScheme: + scheme: https + permanent: true + +--- +# HTTP → HTTPS. Jätä /.well-known/acme-challenge/ pois — cert-manager HTTP-01 (web :80). +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: git-pages-http + namespace: git-pages + labels: + app.kubernetes.io/name: git-pages +spec: + entryPoints: + - web + routes: + - match: >- + Host(`pages.helm-dev.keskikuja.site`) && + !PathPrefix(`/.well-known/acme-challenge/`) + kind: Rule + middlewares: + - name: https-redirect + services: + - name: git-pages + port: 3000 diff --git a/tmp/gitea-pages.yaml b/tmp/gitea-pages.yaml new file mode 100644 index 0000000..89cf3de --- /dev/null +++ b/tmp/gitea-pages.yaml @@ -0,0 +1,162 @@ +# DEPRECATED — älä käytä. deadnews/gitea-pages vetää pages-branchin Giteasta (väärä suunta). +# Käytä sen sijaan: tmp/git-pages.yaml (Codeberg git-pages, CI pushaa HTML:n). +# +# Gitea Pages — k3s homelab (standardimalli, kuten vikunja) +# +# Sovellus: HTTP :8000 — ei omaa ingressiä, ei ACME:ä, ei TLS:ää podissa. +# Ulospäin: cert-manager Certificate → Traefik IngressRoute (websecure). +# +# Image: ghcr.io/deadnews/gitea-pages — vetää tiedostot Gitea API:sta. +# Data flow: CI git push → Gitea (branch "pages") → pages-server lukee API:lla. +# +# URL: https://pages.helm-dev.keskikuja.site/{owner}/{repo}/reports/{sha8}/cucumber/... +# +# Secret = Gitea PAT (read repository). +# POC: Secret inline alla (älä commitoi oikeaa tokenia). +# Tuotanto: kubectl-snippet, Secret pois manifestista — PR:llä repoon. + +--- +apiVersion: v1 +kind: Namespace +metadata: + name: gitea-pages + labels: + app.kubernetes.io/name: gitea-pages + app.kubernetes.io/component: pages-server + +# Tuotanto — Secret kubectl:lla (PAT Giteasta): +# +# NS=gitea-pages +# export GITEA_PAGES_TOKEN='gitea_pat_...' +# kubectl create secret generic gitea-pages-secrets \ +# --from-literal=gitea-api-token="$GITEA_PAGES_TOKEN" \ +# -n $NS +# +# kubectl apply -f tmp/gitea-pages.yaml # ilman Secret-resurssia +--- +apiVersion: v1 +kind: Secret +metadata: + name: gitea-pages-secrets + namespace: gitea-pages + labels: + app.kubernetes.io/name: gitea-pages +type: Opaque +stringData: + # POC: Gitea PAT read repository — täytä paikallisesti, älä commitoi + gitea-api-token: "" + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gitea-pages + namespace: gitea-pages + labels: + app.kubernetes.io/name: gitea-pages + app.kubernetes.io/component: pages-server +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: gitea-pages + template: + metadata: + labels: + app.kubernetes.io/name: gitea-pages + app.kubernetes.io/component: pages-server + spec: + containers: + - name: gitea-pages + image: ghcr.io/deadnews/gitea-pages:v1.0.1 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8000 + protocol: TCP + env: + - name: GITEA_PAGES_SERVER + value: "https://gitea.app.keskikuja.site" + - name: GITEA_PAGES_BRANCH + value: "pages" + - name: GITEA_PAGES_ADDR + value: ":8000" + - name: GITEA_PAGES_TOKEN + valueFrom: + secretKeyRef: + name: gitea-pages-secrets + key: gitea-api-token + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 3 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi + +--- +apiVersion: v1 +kind: Service +metadata: + name: gitea-pages + namespace: gitea-pages + labels: + app.kubernetes.io/name: gitea-pages +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: gitea-pages + ports: + - name: http + port: 8000 + targetPort: http + protocol: TCP + +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: gitea-pages-tls + namespace: gitea-pages + labels: + app.kubernetes.io/name: gitea-pages +spec: + secretName: gitea-pages-tls + commonName: pages.helm-dev.keskikuja.site + dnsNames: + - pages.helm-dev.keskikuja.site + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: gitea-pages + namespace: gitea-pages + labels: + app.kubernetes.io/name: gitea-pages +spec: + entryPoints: + - websecure + routes: + - match: Host(`pages.helm-dev.keskikuja.site`) + kind: Rule + services: + - name: gitea-pages + port: 8000 + tls: + secretName: gitea-pages-tls