Klaster OKD 4.19 (OpenShift) – Projekt healthlog w standardzie Helm oraz Kustomize

Założenia i funkcjonalności projektu healthlog

  • przedstawienie różnic pomiędzy standardową metodą instalacji i konfiguracji projektu OpenShift a instalacją projektu i konfiguracją aplikacji z wykorzystaniem Helm Chart oraz OpenShift Kustomize
  • maksymalnie prosta aplikacja gui w jednym pliku korzystająca z wielu zmiennych środowiskowych
  • obsługa Liveness i Readiness Probe
  • wykorzystanie zewnętrznej bazy danych PostgreSQL

Praktyczne wykorzystanie Helm Chart oraz Kustomize overlay

  • Helm Chart wygeneruje Deployment/Service/Route/ConfigMap/Secret/probes (to co wcześniej robiłeś komendami oc). Helm = “szablony + wartości (values) + release’y” (parametryzacja, wersjonowanie)
  • Kustomize overlay będzie trzymał różnice środowisk (np. namespace, host route, wartości env, liczba replik, resources, itd.) i będzie uruchamiał “helm template” jako generator manifestów. Kustomize = “nakładki/overlaye” (np. prod/dev) bez dotykania bazowych manifestów.

Poniższy opis tworzenia 3 projektów healthlog-prod oraaz healthlog-helm-prod i healthlog-helm-dev ma pokazać różnice pomiędzy standardowym tworzeniem projektów OpenShift a projektów z wykorzystaniem Helm Charts i Kustomize. W dużym skrócie:

  • Helm “renderuje”
  • Kustomize “nakłada”

Później dopiero można automatyzować render w pipeline.

Helm jest świetny do szablonów i values. Kustomize jest świetny do:

  • środowisk (prod/dev/test)
  • drobnych patchy (np. zmiana repliki, limity, host route)
  • dokładania dodatkowych zasobów specyficznych dla klastra

W wersji “Helm + Kustomize” robimy typowy pattern:

  • Helm generuje “bazę” manifestów (helm template)
  • Kustomize nakłada overlay (namespace, patch, wartości)

W OKD 4.19 Kustomize jest dostępny jako oc apply -k … (jeśli masz włączone w kliencie — zwykle tak).

Różnice pomiędzy standardową ręczną metodą konfiguracji projektu OpenShift a projektami z użyciem Helm Charts i OpenShfit Kustomize

W projekcie healthlog-prod standardowo proces instalacji i konfiguracji wygląda tak:

  • build obrazu przez oc new-build + oc start-build –from-dir=…
  • deploy przez oc new-app
  • config przez oc apply CM/Secret
  • env przez oc set env … –from=…
  • networking przez oc expose + oc create route edge
  • probes przez oc set probe

W projektach healthlog-helm-prod i healthlog-helm-dev będzie wyglądać to tak:

  • build obrazu nadal możesz robić tak samo ale deployment nie będzie “klikany”/“set env”-owany ręcznie

zamiast tego:

  • instalujesz/reinstalujesz release Helma (albo generujesz manifesty z Helma i aplikujesz)
  • config/probes/route są w YAML jako kod

Znika:

  • oc apply -f cm… (bo CM jest w Helm)
  • oc apply -f secret… (bo Secret jest w Helm)
  • oc set env … (bo envFrom jest w Deployment template)
  • oc expose … (bo Service jest w Helm)
  • oc create route … (bo Route jest w Helm)
  • oc set probe … (bo probes są w Helm)

Zostaje:

  • oc new-project …
  • oc new-build …
  • oc start-build …

(i zamiast reszty) 1 komenda render/deploy manifestów

Porównanie sposobu poprawiania konfiguracji projektu i aplikacji standardowego projektu OpenShift z projektami z użyciem Helm Charts i OpenShfit Kustomize

W healthlog-prod standardowo

oc project healthlog-prod
oc apply -f /repo/pakiety/healthlog/1.0.0/healthlog-gui/cm-healthlog-gui.yaml
oc rollout restart deployment/healthlog-gui -n healthlog-prod

# protip: sprawdzenie różnic w pliku i cm w projekcie bez zaczytywania zmian
oc diff -f /repo/pakiety/healthlog/1.0.0/healthlog-gui/cm-healthlog-gui.yaml

W healthlog-helm-prod / healthlog-helm-dev (Helm Charts + Kustomize) będzie

#np. zmieniasz wartości baz danych w pliku  
/repo/pakiety/healthlog-helm/1.0.0/healthlog-gui/kustomize/overlays/prod/values-prod.yaml

# robisz:
helm template healthlog-gui ./helm -f kustomize/overlays/prod/values-prod.yaml | oc apply -n healthlog-helm-prod -f -

Porównanie struktury katalogów projektów: healthlog-prod oraz healthlog-helm-prod

	cm-healthlog-gui.yaml
	Dockerfile
	index.php
	secret-healthlog-gui.yaml
  Dockerfile
  index.php
  helm/
      Chart.yaml
      values.yaml
      templates/
        deployment.yaml
        service.yaml
        route.yaml
        configmap.yaml
        secret.yaml
  kustomize/
    base/
      kustomization.yaml
    overlays/
     dev/
        kustomization.yaml
        values-dev.yaml
      prod/
        kustomization.yaml
        values-prod.yaml

Dwa overlaye (dev/prod) + dwa projekty OpenShift (healthlog-helm-dev, healthlog-helm-prod) i możliwość wdrożenia z jednego katalogu. Cel: jedna baza Helma + dwa overlaye Kustomize + dwa namespace’y. To jest gitops-friendly, ale też proste do ręcznego odpalania.

Co robi Helm ? Helm bierze:

  • helm/Chart.yaml (metadane)
  • helm/values.yaml + nadpisania z -f values-*.yaml
  • wszystkie pliki YAML w helm/templates/

i renderuje je do jednego strumienia YAML.

Czyli jeśli w helm/templates/ leży:

  • deployment.yaml → powstaje Deployment
  • service.yaml → powstaje Service
  • route.yaml → powstaje Route
  • kustomization.yaml → powstaje “manifest” kind: Kustomization

Co robi oc apply ?

oc apply próbuje zastosować każdy dokument YAML, jaki mu podasz.

Przygotowanie 1 bazy danych pod projekt healthlog-prod

Poniższy opis zakłada, że został zainstalowany i uruchomiony serwer bazy danych PostgreSQL na bazie wpisu na blogu https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-na-proxmox-9-opis-postgresql-baza-danych-dla-aplikacji-openshift/

sudo -iu postgres psql
-- podstawowe komendy do poruszania się po bazach PostgreSQL
\l
\c nazwa bazy
\dt
\d nazwa_tabeli
\d+ nazwa_tabeli

-- 1) Tworzymy rolę/użytkownika
CREATE ROLE usr_healthlog
  WITH LOGIN
  PASSWORD 'Heal26thlog37!';

-- 2) Tworzymy bazę danych i ustawiamy właściciela
CREATE DATABASE dba_healthlog
  OWNER usr_healthlog
  ENCODING 'UTF8'
  TEMPLATE template0;

-- (opcjonalnie) Odbierz domyślne uprawnienia PUBLIC
REVOKE ALL ON DATABASE dba_healthlog FROM PUBLIC;

-- Pozwól userowi się łączyć
GRANT CONNECT, TEMPORARY ON DATABASE dba_healthlog TO usr_healthlog;

-- W psql przełącz bazę:
\c dba_healthlog

CREATE TABLE IF NOT EXISTS healthlogs (
  id          BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  weight      NUMERIC(6,2) NOT NULL,
  CONSTRAINT healthlogs_weight_check CHECK (weight > 0)
);

-- Uprawnienia do sekwencji (identity)
GRANT USAGE ON SCHEMA public TO usr_healthlog;

GRANT SELECT, INSERT, UPDATE, DELETE
ON healthlogs
TO usr_healthlog;

-- kluczowe dla IDENTITY:
GRANT USAGE, SELECT, UPDATE
ON ALL SEQUENCES IN SCHEMA public
TO usr_healthlog;

Przygotowanie 2 baz danych pod projekty healthlog-helm-prod oraz healthlog-helm-dev

Poniższy opis zakłada, że został zainstalowany i uruchomiony serwer bazy danych PostgreSQL na bazie wpisu na blogu https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-na-proxmox-9-opis-postgresql-baza-danych-dla-aplikacji-openshift/

sudo -iu postgres psql
-- podstawowe komendy do poruszania się po bazach PostgreSQL
\l
\c nazwa bazy
\dt
\d nazwa_tabeli
\d+ nazwa_tabeli

-- 1) Tworzymy rolę/użytkownika
CREATE ROLE usr_healthlog_prod
  WITH LOGIN
  PASSWORD 'Heal26thlog37!PROD';
  
CREATE ROLE usr_healthlog_dev
  WITH LOGIN
  PASSWORD 'Heal26thlog37!DEV';  

-- w razie reset hasła
ALTER ROLE usr_healthlog_dev WITH PASSWORD 'NOWE_HASLO';

-- 2) Tworzymy bazę danych i ustawiamy właściciela
CREATE DATABASE dba_healthlog_prod
  OWNER usr_healthlog_prod
  ENCODING 'UTF8'
  TEMPLATE template0;
  
  CREATE DATABASE dba_healthlog_dev
  OWNER usr_healthlog_dev
  ENCODING 'UTF8'
  TEMPLATE template0;

-- (opcjonalnie) Odbierz domyślne uprawnienia PUBLIC
REVOKE ALL ON DATABASE dba_healthlog_prod FROM PUBLIC;
REVOKE ALL ON DATABASE dba_healthlog_dev FROM PUBLIC;

-- Pozwól userowi się łączyć
GRANT CONNECT, TEMPORARY ON DATABASE dba_healthlog_prod TO usr_healthlog_prod;
GRANT CONNECT, TEMPORARY ON DATABASE dba_healthlog_dev TO usr_healthlog_dev;

-- W psql przełącz bazę:
\c dba_healthlog_prod

CREATE TABLE IF NOT EXISTS healthlogs (
  id          BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  weight      NUMERIC(6,2) NOT NULL,
  CONSTRAINT healthlogs_weight_check CHECK (weight > 0)
);

-- Uprawnienia do sekwencji (identity)
GRANT USAGE ON SCHEMA public TO usr_healthlog_prod;

GRANT SELECT, INSERT, UPDATE, DELETE
ON healthlogs
TO usr_healthlog_prod;

-- kluczowe dla IDENTITY:
GRANT USAGE, SELECT, UPDATE
ON ALL SEQUENCES IN SCHEMA public
TO usr_healthlog_prod;

\c dba_healthlog_dev

CREATE TABLE IF NOT EXISTS healthlogs (
  id          BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  weight      NUMERIC(6,2) NOT NULL,
  CONSTRAINT healthlogs_weight_check CHECK (weight > 0)
);

-- Uprawnienia do sekwencji (identity)
GRANT USAGE ON SCHEMA public TO usr_healthlog_dev;

GRANT SELECT, INSERT, UPDATE, DELETE
ON healthlogs
TO usr_healthlog_dev;

-- kluczowe dla IDENTITY:
GRANT USAGE, SELECT, UPDATE
ON ALL SEQUENCES IN SCHEMA public
TO usr_healthlog_dev;

Ustawienie dostępu tylko z sieci 192.168.40.0/24 dla konkretnej bazy / użytkownika

host    dba_healthlog     usr_healthlog     192.168.40.0/24     scram-sha-256

Lub całkowicie dla całej sieci 192.168.40.0/24

# TYPE  DATABASE        USER            ADDRESS                 METHOD

# "local" is for Unix domain socket connections only
local   all             all                                     peer
# IPv4 local connections:
host    all             all             127.0.0.1/32            ident
# IPv6 local connections:
host    all             all             ::1/128                 ident
# Allow replication connections from localhost, by a user with the
# replication privilege.
local   replication     all                                     peer
host    replication     all             127.0.0.1/32            ident
host    replication     all             ::1/128                 ident

# usr_editor może łączyć się do dba_users z dowolnego IPv4
#host    dba_users   usr_editor   192.168.40.0/24   scram-sha-256

# LAN klastra
host    all             all             192.168.40.0/24         scram-sha-256
host    all             all             192.168.8.0/24         scram-sha-256
host    all             all             192.168.10.0/24         scram-sha-256
# Restart PostgreSQL po zmianach
sudo systemctl restart postgresql
sudo systemctl status postgresql --no-page

Weryfikacja z innego hosta możliwości połączenia się z bazą dba_healthlog przez użytkownika usr_healthlog.

psql -h 192.168.40.25 -U usr_healthlog -d dba_healthlog

dba_healthlog=> \dt
             List of tables
 Schema |    Name    | Type  |  Owner
--------+------------+-------+----------
 public | healthlogs | table | postgres
(1 row)

Przygotowanie wspólnego Dockerfile pod projekty 3 healthlog-prod oraz healthlog-helm-prod/dev

FROM registry.access.redhat.com/ubi9/php-83:latest
USER 0
# Zainstaluj sterowniki PostgreSQL do PHP (pdo_pgsql jest w php-pgsql)
RUN set -eux; \
  if command -v microdnf >/dev/null 2>&1; then \
    microdnf -y install php-pgsql && microdnf clean all; \
  elif command -v dnf >/dev/null 2>&1; then \
    dnf -y install php-pgsql && dnf clean all; \
  elif command -v yum >/dev/null 2>&1; then \
    yum -y install php-pgsql && yum clean all; \
  else \
    echo "ERROR: No package manager found (microdnf/dnf/yum)"; exit 1; \
  fi
WORKDIR /var/www/html
COPY index.php /var/www/html/index.php
RUN chgrp -R 0 /var/www/html && chmod -R g+rwX /var/www/html
EXPOSE 8080
USER 1001
CMD ["php", "-S", "0.0.0.0:8080", "-t", "/var/www/html"]

Uruchomienie projektu healthlog-prod

Poniżej opisany jest standardowy, ręczny sposób instalacji i konfiguracji projektu OpenShift oraz aplikacji wewnątrz tego projektu.

apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-healthlog-gui
  namespace: healthlog-prod
data:
  HL_APP_VERSION: "1.0.0"
  HL_DBA_SERVER: "192.168.40.25"
  HL_DBA_PORT: "5432"
  HL_DBA_USER: "usr_healthlog"
  HL_DBA_NAME: "dba_healthlog"
  HL_GUI_CONFIG_SHOW_VERSION: "1"
  HL_GUI_SHOW_ENVIRONMENT_VARIABLES: "1"
apiVersion: v1
kind: Secret
metadata:
  name: secret-healthlog-gui
  namespace: healthlog-prod
type: Opaque
stringData:
  HL_DBA_PASSWORD: "Heal26thlog37!"
# Utworzenie projektu
oc new-project healthlog-prod

# BuildConfig (binary build z Dockerfile)
oc new-build --name=healthlog-gui --binary --strategy=docker -n healthlog-prod

# Start builda z katalogu (Dockerfile + index.php)
oc start-build healthlog-gui --from-dir=. -n healthlog-prod --follow

# Utworzenie aplikacji z ImageStream po buildzie
oc new-app -n healthlog-prod --image-stream=healthlog-gui --name=healthlog-gui

# Dodanie ConfigMap i Secret
oc apply -n healthlog-prod -f cm-healthlog-gui.yaml
oc apply -n healthlog-prod -f secret-healthlog-gui.yaml

# Wstrzyknięcie zmiennych środowiskowych do Deploymentu
oc set env -n healthlog-prod deployment/healthlog-gui --from=configmap/cm-healthlog-gui
oc set env -n healthlog-prod deployment/healthlog-gui --from=secret/secret-healthlog-gui

# To tworzy Service (zwykle svc/healthlog-gui), który:  
# - wybiera Pody po selektorach (labelach) z Deploymentu
# - wystawia stabilny adres w klastrze (ClusterIP)
# - mapuje porty: port: 8080 (na Service) targetPort: 8080 (na kontener w Podzie)
# - Bez Service Route nie ma do czego wskazywać.
oc expose -n healthlog-prod deployment/healthlog-gui --port=8080 --target-port=8080

# To tworzy Route (HTTP) bez TLS edge (zwykły http) wskazuje na svc/healthlog-gui na porcie 8080  to jest szybki skrót „zrób mi route do tego service”
# ten krok można pominąć w tym przykładzie
oc expose -n healthlog-prod service/healthlog-gui --name=healthlog-gui --port=8080

# To tworzy route edge TLS 
# - router robi terminację TLS (HTTPS do routera)
# - potem router leci do service zwykle HTTP (w środku klastra)
oc create route edge healthlog-gui --service=healthlog-gui --port=8080 -n healthlog-prod

# Dodanie Readiness probe
oc set probe deployment/healthlog-gui -n healthlog-prod \
  --readiness \
  --get-url=http://:8080/health/ready \
  --initial-delay-seconds=10 \
  --timeout-seconds=3 \
  --period-seconds=10 \
  --failure-threshold=3

# Dodanie Liveness probe
oc set probe deployment/healthlog-gui -n healthlog-prod \
  --liveness \
  --get-url=http://:8080/health/live \
  --initial-delay-seconds=20 \
  --timeout-seconds=3 \
  --period-seconds=20 \
  --failure-threshold=3

# weryfikacja Readiness / Liveness probe
oc describe deployment healthlog-gui -n healthlog-prod | egrep -n "Liveness|Readiness|http-get"

# po zmianach w cm
oc rollout restart deployment/healthlog-gui

# Sprawdzenie:
oc get all -n healthlog-prod
oc get route -n healthlog-prod

Testowanie Liveness probe

sdasdsa

Testowanie Readiness probe

asdsad

Instalacja helm 3 bez roota (do $HOME/bin) – polecane na bastionach

mkdir -p "$HOME/bin"

export HELM_INSTALL_DIR="$HOME/bin"
export USE_SUDO=false
export DESIRED_VERSION="v3.20.0"

curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# upewnij się, że PATH zawiera ~/bin (na stałe)
grep -q 'export PATH=$HOME/bin:$PATH' ~/.bashrc || echo 'export PATH=$HOME/bin:$PATH' >> ~/.bashrc
source ~/.bashrc

hash -r
helm version --short

v3.20.0+gb2e4314

Uruchomienie projektów healthlog-helm-prod / healthlog-helm-dev

Poniżej opisany jest nowoczesny sposób instalacji i konfiguracji projektu OpenShift oraz aplikacji wewnątrz tego projektu oparty na Helm Chart oraz OpenShift Kustomize.

apiVersion: v2
name: healthlog-gui
description: Simple healthlog GUI (PHP + Bootstrap) for OKD/OpenShift
type: application
version: 0.1.0
appVersion: "1.0.0"
  • version = wersja chartu (infra)
  • appVersion = wersja aplikacji (to, co pokazywałeś w GUI)

Poniżej znajduje się bazowy zestaw wartości domyślnych dla chartu. A values-dev.yaml i values-prod.yaml to nadpisania (override) dla konkretnych środowisk.

nameOverride: ""
fullnameOverride: ""

image:
  # w OpenShift typowo wskazujesz na ImageStreamTag albo registry wewnętrzny
  # na start: zakładamy imagestream "healthlog-gui:latest" w namespace
  repository: healthlog-gui
  tag: latest
  pullPolicy: IfNotPresent

service:
  port: 8080
  targetPort: 8080

route:
  enabled: true
  name: healthlog-gui
  tlsTermination: edge
  # host opcjonalny (jak nie ustawisz, OpenShift wygeneruje)
  host: ""

config:
  HL_APP_VERSION: "1.0.0"
  HL_DBA_SERVER: "192.168.40.25"
  HL_DBA_PORT: "5432"
  HL_DBA_USER: "usr_healthlog"
  HL_DBA_NAME: "dba_healthlog"
  HL_GUI_CONFIG_SHOW_VERSION: "1"
  HL_GUI_SHOW_ENVIRONMENT_VARIABLES: "1"

secret:
  HL_DBA_PASSWORD: "Heal26thlog37!"

probes:
  readiness:
    path: /health/ready
    initialDelaySeconds: 10
    timeoutSeconds: 3
    periodSeconds: 10
    failureThreshold: 3
  liveness:
    path: /health/live
    initialDelaySeconds: 20
    timeoutSeconds: 3
    periodSeconds: 20
    failureThreshold: 3

resources: {}
replicaCount: 1

Helm templates: to zastępuje ręczne oc apply, oc set env, oc set probe, oc expose, oc create route

ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-healthlog-gui
data:
{{- range $k, $v := .Values.config }}
  {{ $k }}: {{ $v | quote }}
{{- end }}

Secret

apiVersion: v1
kind: Secret
metadata:
  name: secret-healthlog-gui
type: Opaque
stringData:
{{- range $k, $v := .Values.secret }}
  {{ $k }}: {{ $v | quote }}
{{- end }}

Deployment

To jest najważniejsze — tu wchodzi:

  • obraz
  • envFrom (ConfigMap + Secret)
  • readiness + liveness (to co wcześniej oc set probe)
  • port 8080

To zastępuje:

  • oc new-app … (deployment)
  • oc set env … –from=configmap/secret
  • oc set probe …

Automatyczny rollout po zmianie CM/Secret

Teraz, gdy zmienisz ConfigMap/Secret, Deployment może nie zrestartować Podów automatycznie (env zaciąga się tylko przy starcie kontenera).
Najprościej: dodanie checksum anotacje do helm/templates/deployment.yaml.

  • checksum/config: {{ include (print $.Template.BasePath „/configmap.yaml”) . | sha256sum }}
  • checksum/secret: {{ include (print $.Template.BasePath „/secret.yaml”) . | sha256sum }}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: healthlog-gui
  labels:
    app: healthlog-gui
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: healthlog-gui
  template:
    metadata:
      labels:
        app: healthlog-gui
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
        checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
    spec:
      containers:
        - name: healthlog-gui
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: {{ .Values.service.targetPort }}
              name: web
          envFrom:
            - configMapRef:
                name: cm-healthlog-gui
            - secretRef:
                name: secret-healthlog-gui

          readinessProbe:
            httpGet:
              path: {{ .Values.probes.readiness.path }}
              port: {{ .Values.service.targetPort }}
            initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
            timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
            periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
            failureThreshold: {{ .Values.probes.readiness.failureThreshold }}

          livenessProbe:
            httpGet:
              path: {{ .Values.probes.liveness.path }}
              port: {{ .Values.service.targetPort }}
            initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
            timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
            periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
            failureThreshold: {{ .Values.probes.liveness.failureThreshold }}

          resources:
{{ toYaml .Values.resources | indent 12 }}

Service

To zastępuje:

  • oc expose deployment/… (tworzenie Service)
apiVersion: v1
kind: Service
metadata:
  name: healthlog-gui
  labels:
    app: healthlog-gui
spec:
  selector:
    app: healthlog-gui
  ports:
    - name: web
      port: {{ .Values.service.port }}
      targetPort: {{ .Values.service.targetPort }}

Route

To zastępuje:

  • oc create route edge …
{{- if .Values.route.enabled }}
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: {{ .Values.route.name }}
spec:
  {{- if .Values.route.host }}
  host: {{ .Values.route.host | quote }}
  {{- end }}
  to:
    kind: Service
    name: healthlog-gui
  port:
    targetPort: web
  tls:
    termination: {{ .Values.route.tlsTermination }}
{{- end }}

Kustomize base: generator z Helma

Funkcja: miejsce na wspólne elementy. Na przyszłość (wspólne patche, labelki itd.). Na start nie musi robić nic.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources: []

Kustomize overlay prod: healthlog-helm-prod

  • Ustawia namespace healthlog-helm-prod
  • Renderuje chart Helma z values values-prod.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: healthlog-helm-prod

resources:
  - ../../base

helmGlobals:
  chartHome: ../../../

helmCharts:
  - name: helm
    releaseName: healthlog-gui
    valuesFile: values-prod.yaml

Kustomize overlay dev: healthlog-helm-dev

  • Ustawia namespace healthlog-helm-dev
  • Renderuje chart Helma z values values-dev.yaml

Wynik to komplet zasobów gotowych do oc apply

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: healthlog-helm-dev

resources:
  - ../../base

helmGlobals:
  chartHome: ../../../

helmCharts:
  - name: helm
    releaseName: healthlog-gui
    valuesFile: values-dev.yaml

Dwa różne values: dev i prod. Tu realizujesz różnice:

  • inne DB name, user, password
  • inne flagi GUI
  • opcjonalnie inny host route
  • opcjonalnie inny tag obrazu/pullPolicy
image:
  repository: image-registry.openshift-image-registry.svc:5000/healthlog-helm-prod/healthlog-gui
  tag: latest
  pullPolicy: Always

route:
  enabled: true
  name: healthlog-gui
  tlsTermination: edge
  host: "healthlog-gui-healthlog-helm-prod.apps.testcluster.okdlab.local"

config:
  HL_APP_VERSION: "1.0.0"
  HL_DBA_SERVER: "192.168.40.25"
  HL_DBA_PORT: "5432"
  HL_DBA_USER: "usr_healthlog_prod"
  HL_DBA_NAME: "dba_healthlog_prod"
  HL_GUI_CONFIG_SHOW_VERSION: "1"
  HL_GUI_SHOW_ENVIRONMENT_VARIABLES: "0"  # prod zwykle nie pokazuje envów

secret:
  HL_DBA_PASSWORD: "Heal26thlog37!PROD"
image:
  # jeśli budujesz obraz w tym samym projekcie, najlepiej używać wewnętrznego registry
  repository: image-registry.openshift-image-registry.svc:5000/healthlog-helm-dev/healthlog-gui
  tag: latest
  pullPolicy: Always

route:
  enabled: true
  name: healthlog-gui
  tlsTermination: edge
  host: "healthlog-gui-healthlog-helm-dev.apps.testcluster.okdlab.local"

config:
  HL_APP_VERSION: "1.0.0-dev"
  HL_DBA_SERVER: "192.168.40.25"
  HL_DBA_PORT: "5432"
  HL_DBA_USER: "usr_healthlog_dev"
  HL_DBA_NAME: "dba_healthlog_dev"
  HL_GUI_CONFIG_SHOW_VERSION: "1"
  HL_GUI_SHOW_ENVIRONMENT_VARIABLES: "1"

secret:
  HL_DBA_PASSWORD: "DevHeal26thlog37!DEV"

Wdrożenie i aktualizacja projektów healthlog-helm-prod lub healthlog-helm-dev

cd /repo/pakiety/healthlog-helm/1.0.0/healthlog-gui
# Jednorazowo: utworzenie projektów
oc new-project healthlog-helm-dev
oc new-project healthlog-helm-prod

# Jednorazowo: BuildConfig w obu projektachg
oc new-build --name=healthlog-gui --binary --strategy=docker -n healthlog-helm-dev
oc new-build --name=healthlog-gui --binary --strategy=docker -n healthlog-helm-prod

# Start builda
oc start-build healthlog-gui --from-dir=. -n healthlog-helm-dev --follow
oc start-build healthlog-gui --from-dir=. -n healthlog-helm-prod --follow

#  Helm render → oc apply – wariant “zawsze działa”
helm template healthlog-gui ./helm -f kustomize/overlays/dev/values-dev.yaml | oc apply -n healthlog-helm-dev -f -
helm template healthlog-gui ./helm -f kustomize/overlays/prod/values-prod.yaml | oc apply -n healthlog-helm-prod -f -

Zmiana konfiguracji aplikacji w projekcie Helm

# Aktualizacja po zmianie values (env / config).Zmieniłeś tylko values-dev.yaml albo values-prod.yaml
# np. HL_GUI_SHOW_ENVIRONMENT_VARIABLES: "1"  w /repo/pakiety/healthlog-helm/1.0.0/healthlog-gui/kustomize/overlays/prod/values-prod.yaml
helm template healthlog-gui ./helm -f kustomize/overlays/dev/values-dev.yaml | oc apply -n healthlog-helm-dev -f -
helm template healthlog-gui ./helm -f kustomize/overlays/prod/values-prod.yaml | oc apply -n healthlog-helm-prod -f -

# pokaże się komunikat i aplikacja ma zmieniona konfiguracje
secret/secret-healthlog-gui configured
configmap/cm-healthlog-gui configured
service/healthlog-gui unchanged
deployment.apps/healthlog-gui configured
route.route.openshift.io/healthlog-gui unchanged


# jeśli NIE masz checksum anotacji w Deployment:
oc rollout restart deployment/healthlog-gui -n healthlog-helm-dev
oc rollout restart deployment/healthlog-gui -n healthlog-helm-prod

# Zmieniłeś kod (index.php) lub Dockerfile Lepszy scenariusz (polecany pod CI/CD): unikalne tagi obrazu
oc start-build healthlog-gui --from-dir=. -n healthlog-helm-dev --follow
oc start-build healthlog-gui --from-dir=. -n healthlog-helm-prod --follow
# jeśli tag jest "latest" (najczęściej potrzebne):
oc rollout restart deployment/healthlog-gui -n healthlog-helm-dev
oc rollout restart deployment/healthlog-gui -n healthlog-helm-prod

Zmiana Dockerfile lub kodu aplikacji w projekcie Helm

Jeśli używany jest tag latest czyli w values-prod.yaml jest coś takiego

image:
  repository: image-registry.openshift-image-registry.svc:5000/healthlog-helm-prod/healthlog-gui
  tag: latest
  pullPolicy: Always
cd /repo/pakiety/healthlog-helm/1.0.0/healthlog-gui
oc start-build healthlog-gui --from-dir=. -n healthlog-helm-prod --follow
# Wymuś przeładowanie Podów (bo tag się nie zmienił)
oc rollout restart deployment/healthlog-gui -n healthlog-helm-prod
oc rollout status deployment/healthlog-gui -n healthlog-helm-prod

W lepszym scenariuszu pod CI/CD są unikalne tagi obrazu. Można tak jak poniżej po zbudowaniu obrazu otagować wynikowy ImageStreamTag na np.1.0.1

oc tag -n healthlog-helm-prod healthlog-gui:latest healthlog-gui:1.0.1

# potem w pliku /repo/pakiety/healthlog-helm/1.0.0/healthlog-gui/kustomize/overlays/prod/values-prod.yaml zmieniamy tag: latest na tag: 1.0.1
image:
  repository: image-registry.openshift-image-registry.svc:5000/healthlog-helm-prod/healthlog-gui
  tag: 1.0.1
  pullPolicy: Always

# Deployment widzi nowy tag → rollout idzie automatycznie
helm template healthlog-gui ./helm -f kustomize/overlays/prod/values-prod.yaml | oc apply -n healthlog-helm-prod -f -

# Jak sprawdzić, czy pod naprawdę ma nową wersję?
POD=$(oc get pod -n healthlog-helm-prod -l app=healthlog-gui -o name | head -n1)
oc get -n healthlog-helm-prod $POD -o jsonpath='{.spec.containers[0].image}{"\n"}'
image-registry.openshift-image-registry.svc:5000/healthlog-helm-prod/healthlog-gui:1.0.1

Rozwiązywanie problemów

# w razie błedu jak ponizej nalezy się upewnić że user usr_healthlog_dev ma poprawne hasło takie jak w secret ma być w PSSQL
"error":"SQLSTATE[08006] [7] FATAL:  password authentication failed for user \"usr_healthlog_dev\""
# wtedy na maszynie database-1 
sudo -iu postgres psql
ALTER ROLE usr_healthlog_dev WITH PASSWORD 'NOWE_HASLO';

# weryfikacja aktualnego secret
oc get secret -n healthlog-helm-dev secret-healthlog-gui -o jsonpath='{.data.HL_DBA_PASSWORD}' | base64 -d; echo
DevHeal26thlog37!DEV

# jeśli / zwraca 200, ale /health/ready 503 → problem to logika ready
POD=$(oc get pod -n healthlog-helm-dev -l app=healthlog-gui -o name | head -n1)
oc rsh -n healthlog-helm-dev $POD sh -lc 'curl -sS -i http://127.0.0.1:8080/health/ready || true'
oc rsh -n healthlog-helm-dev $POD sh -lc 'curl -sS -i http://127.0.0.1:8080/health/live || true'
oc rsh -n healthlog-helm-dev $POD sh -lc 'curl -sS -i http://127.0.0.1:8080/ || true'

# sprawdź dostęp sieciowy do PostgreSQL z DEV poda
oc rsh -n healthlog-helm-dev $POD sh -lc 'timeout 3 bash -lc "</dev/tcp/192.168.40.25/5432" && echo OK || echo FAIL'

# sprawdź jakie env-y faktycznie siedzą w Podzie (DEV)
oc set env -n healthlog-helm-dev deployment/healthlog-gui --list | egrep 'HL_|POSTGRES|DBA' || true

# weryfikacja rediness probe z poda 
oc rsh -n healthlog-helm-dev $POD sh -lc 'curl -sS -i http://127.0.0.1:8080/health/ready || true'
HTTP/1.1 200 OK
Host: 127.0.0.1:8080
Date: Tue, 17 Feb 2026 15:01:35 GMT
Connection: close
X-Powered-By: PHP/8.3.29
Content-Type: application/json; charset=utf-8

Wspólny kod aplikacji index.php pod projekty healthlog-prod oraz healthlog-helm-prod

<?php
declare(strict_types=1);

// --- health endpoints (must be BEFORE any HTML output) ---
$uriPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';

if ($uriPath === '/health/live' || $uriPath === '/healthz') {
    // Liveness: odpowiadaj 200 jeśli PHP działa
    header('Content-Type: application/json; charset=utf-8');
    http_response_code(200);
    echo json_encode([
        "status" => "ok",
        "check" => "live",
        "ts" => date('c'),
    ], JSON_UNESCAPED_UNICODE);
    exit;
}

if ($uriPath === '/health/ready') {
    // Readiness: 200 tylko jeśli DB działa (i driver jest)
    header('Content-Type: application/json; charset=utf-8');

    $driverOk = in_array('pgsql', PDO::getAvailableDrivers(), true);

    if (!$driverOk) {
        http_response_code(503);
        echo json_encode([
            "status" => "fail",
            "check" => "ready",
            "reason" => "pdo_pgsql driver missing",
            "drivers" => PDO::getAvailableDrivers(),
            "ts" => date('c'),
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    try {
        // Tu korzystamy z tych samych env co reszta aplikacji
        $HL_DBA_SERVER   = getenv("HL_DBA_SERVER") ?: "127.0.0.1";
        $HL_DBA_PORT     = getenv("HL_DBA_PORT") ?: "5432";
        $HL_DBA_USER     = getenv("HL_DBA_USER") ?: "usr_healthlog";
        $HL_DBA_NAME     = getenv("HL_DBA_NAME") ?: "dba_healthlog";
        $HL_DBA_PASSWORD = getenv("HL_DBA_PASSWORD") ?: "";

        $dsn = sprintf("pgsql:host=%s;port=%s;dbname=%s;sslmode=prefer", $HL_DBA_SERVER, $HL_DBA_PORT, $HL_DBA_NAME);

        $pdo = new PDO($dsn, $HL_DBA_USER, $HL_DBA_PASSWORD, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_TIMEOUT => 2, // krótki timeout do readiness
        ]);

        // super-lekki test
        $pdo->query("SELECT 1");

        http_response_code(200);
        echo json_encode([
            "status" => "ok",
            "check" => "ready",
            "db" => "ok",
            "ts" => date('c'),
        ], JSON_UNESCAPED_UNICODE);
        exit;

    } catch (Throwable $e) {
        http_response_code(503);
        echo json_encode([
            "status" => "fail",
            "check" => "ready",
            "db" => "error",
            "error" => $e->getMessage(),
            "ts" => date('c'),
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }
}

function envv(string $key, ?string $default = null): string {
    $v = getenv($key);
    if ($v === false || $v === null || $v === '') return (string)$default;
    return $v;
}

function bool_env(string $key, string $default = "0"): bool {
    $v = trim(envv($key, $default));
    return ($v === "1" || strcasecmp($v, "true") === 0 || strcasecmp($v, "yes") === 0 || strcasecmp($v, "on") === 0);
}

function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }

$HL_APP_VERSION = envv("HL_APP_VERSION", "1.0.0");
$HL_DBA_SERVER  = envv("HL_DBA_SERVER", "127.0.0.1");
$HL_DBA_PORT    = envv("HL_DBA_PORT", "5432");
$HL_DBA_USER    = envv("HL_DBA_USER", "usr_healthlog");
$HL_DBA_NAME    = envv("HL_DBA_NAME", "dba_healthlog");
$HL_DBA_PASSWORD= envv("HL_DBA_PASSWORD", ""); // z Secret
$SHOW_VERSION   = bool_env("HL_GUI_CONFIG_SHOW_VERSION", "1");
$SHOW_ENVVARS   = bool_env("HL_GUI_SHOW_ENVIRONMENT_VARIABLES", "1");

$envList = [
  "HL_APP_VERSION" => $HL_APP_VERSION,
  "HL_DBA_SERVER" => $HL_DBA_SERVER,
  "HL_DBA_PORT" => $HL_DBA_PORT,
  "HL_DBA_USER" => $HL_DBA_USER,
  "HL_DBA_NAME" => $HL_DBA_NAME,
  "HL_GUI_CONFIG_SHOW_VERSION" => envv("HL_GUI_CONFIG_SHOW_VERSION", "1"),
  "HL_GUI_SHOW_ENVIRONMENT_VARIABLES" => envv("HL_GUI_SHOW_ENVIRONMENT_VARIABLES", "1"),
];

// --- DB connect ---
$pdo = null;
$dbError = null;

try {
    $dsn = sprintf("pgsql:host=%s;port=%s;dbname=%s;sslmode=prefer", $HL_DBA_SERVER, $HL_DBA_PORT, $HL_DBA_NAME);
    $pdo = new PDO($dsn, $HL_DBA_USER, $HL_DBA_PASSWORD, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]);
} catch (Throwable $e) {
    $dbError = $e->getMessage();
}

// --- handle POST (save weight) ---
$flashOk = null;
$flashErr = null;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $raw = trim((string)($_POST['weight'] ?? ''));
    // Akceptuj "98,2" i "98.2"
    $normalized = str_replace(' ', '', $raw);
    $normalized = str_replace(',', '.', $normalized);

    if ($normalized === '') {
        $flashErr = "Podaj wagę.";
    } elseif (!preg_match('/^\d{1,3}(\.\d{1,2})?$/', $normalized)) {
        $flashErr = "Nieprawidłowy format wagi. Przykład: 98,2 albo 98.20";
    } else {
        $weight = (float)$normalized;
        if ($weight <= 0) {
            $flashErr = "Waga musi być > 0.";
        } elseif ($pdo === null) {
            $flashErr = "Brak połączenia z bazą – nie zapisano.";
        } else {
            try {
                $stmt = $pdo->prepare("INSERT INTO healthlogs (weight) VALUES (:w)");
                // NUMERIC(6,2) -> zapisuj z 2 miejscami
                $stmt->execute([':w' => number_format($weight, 2, '.', '')]);
                $flashOk = "Zapisano wagę: " . number_format($weight, 2, ',', '') . " kg";
                // PRG: przekierowanie żeby nie dublować wpisu po odświeżeniu
                header("Location: " . strtok($_SERVER["REQUEST_URI"], '?') . "?saved=1");
                exit;
            } catch (Throwable $e) {
                $flashErr = "Błąd zapisu do bazy: " . $e->getMessage();
            }
        }
    }
}

// --- Read last 14 for table (newest first) + chart (oldest first) ---
$rowsNewest = [];
$chartLabels = [];
$chartData = [];

if ($pdo !== null) {
    try {
        $stmt = $pdo->query("
            SELECT id, created_at, weight
            FROM healthlogs
            ORDER BY id DESC
            LIMIT 14
        ");
        $rowsNewest = $stmt->fetchAll();

        $rowsOldest = array_reverse($rowsNewest);
        foreach ($rowsOldest as $r) {
            // Format label: YYYY-MM-DD HH:MM
            $dt = new DateTime((string)$r['created_at']);
            $chartLabels[] = $dt->format('Y-m-d H:i');
            $chartData[] = (float)$r['weight'];
        }
    } catch (Throwable $e) {
        $dbError = $dbError ?: $e->getMessage();
    }
}

// Helper for nice dt in table
function fmt_dt(string $ts): string {
    try {
        $dt = new DateTime($ts);
        return $dt->format('Y-m-d H:i:s');
    } catch (Throwable $e) {
        return $ts;
    }
}

?>
<!doctype html>
<html lang="pl">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>healthlog</title>
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg bg-white border-bottom">
  <div class="container">
    <span class="navbar-brand fw-semibold">healthlog</span>
    <div class="ms-auto small text-muted">
      <?php if ($pdo !== null && $dbError === null): ?>
        <span class="badge text-bg-success">DB: OK</span>
      <?php else: ?>
        <span class="badge text-bg-danger">DB: ERROR</span>
      <?php endif; ?>
    </div>
  </div>
</nav>

<main class="container py-4">
  <div class="row g-4">
    <div class="col-12 col-lg-5">
      <div class="card shadow-sm border-0">
        <div class="card-body">
          <h5 class="card-title mb-3">Dodaj wpis</h5>

          <?php if ($flashErr): ?>
            <div class="alert alert-danger mb-3"><?= h($flashErr) ?></div>
          <?php elseif (isset($_GET['saved'])): ?>
            <div class="alert alert-success mb-3">Zapisano.</div>
          <?php endif; ?>

          <?php if ($dbError): ?>
            <div class="alert alert-warning">
              <div class="fw-semibold mb-1">Uwaga: problem z bazą danych</div>
              <div class="small text-muted" style="word-break: break-word;">
                <?= h($dbError) ?>
              </div>
            </div>
          <?php endif; ?>

          <form method="post" class="row g-2 align-items-end">
            <div class="col-8">
              <label for="weight" class="form-label">Waga (kg)</label>
              <input
                type="text"
                inputmode="decimal"
                class="form-control form-control-lg"
                id="weight"
                name="weight"
                placeholder="np. 98,2"
                autocomplete="off"
                required
              >
              <div class="form-text">Akceptuje format: <code>xx,x</code> lub <code>xx.x</code> (do 2 miejsc po przecinku).</div>
            </div>
            <div class="col-4 d-grid">
              <button type="submit" class="btn btn-primary btn-lg">Zapisz</button>
            </div>
          </form>

          <hr class="my-4">

          <div class="d-flex justify-content-between align-items-center">
            <div class="fw-semibold">Ostatnie 14 wpisów</div>
            <div class="small text-muted"><?= count($rowsNewest) ?> rekordów</div>
          </div>

          <div class="mt-3">
            <canvas id="wChart" height="140"></canvas>
          </div>

          <div class="small text-muted mt-2">
            Wykres pokazuje <b>kolejność czasową</b> (od najstarszego do najnowszego w ramach 14 wpisów).
          </div>
        </div>
      </div>
    </div>

    <div class="col-12 col-lg-7">
      <div class="card shadow-sm border-0">
        <div class="card-body">
          <h5 class="card-title mb-3">Historia</h5>

          <div class="table-responsive">
            <table class="table table-striped table-hover align-middle mb-0">
              <thead class="table-dark">
                <tr>
                  <th style="width: 90px;">id</th>
                  <th>data</th>
                  <th style="width: 140px;" class="text-end">waga (kg)</th>
                </tr>
              </thead>
              <tbody>
                <?php if (empty($rowsNewest)): ?>
                  <tr>
                    <td colspan="3" class="text-muted">Brak danych. Dodaj pierwszy wpis.</td>
                  </tr>
                <?php else: ?>
                  <?php foreach ($rowsNewest as $r): ?>
                    <tr>
                      <td class="fw-semibold"><?= (int)$r['id'] ?></td>
                      <td><?= h(fmt_dt((string)$r['created_at'])) ?></td>
                      <td class="text-end"><?= h(number_format((float)$r['weight'], 2, ',', '')) ?></td>
                    </tr>
                  <?php endforeach; ?>
                <?php endif; ?>
              </tbody>
            </table>
          </div>

        </div>
      </div>
    </div>
  </div>

  <?php if ($SHOW_ENVVARS): ?>
    <div class="alert mt-4" style="background:#fff3cd; color:#000; border-color:#ffeeba;">
      <div class="fw-semibold mb-2">Zmienne środowiskowe (z ConfigMap/Secret)</div>
      <ul class="mb-0">
        <?php foreach ($envList as $k => $v): ?>
          <li><code><?= h($k) ?></code> = "<span><?= h($v) ?></span>"</li>
        <?php endforeach; ?>
      </ul>
    </div>
  <?php endif; ?>

  <?php if ($SHOW_VERSION): ?>
    <div class="text-center text-muted small mt-3">
      healthlog-gui wersja <?= h($HL_APP_VERSION) ?>
    </div>
  <?php endif; ?>
</main>

<script>
(() => {
  const labels = <?= json_encode($chartLabels, JSON_UNESCAPED_UNICODE) ?>;
  const data   = <?= json_encode($chartData, JSON_UNESCAPED_UNICODE) ?>;

  const ctx = document.getElementById('wChart');
  if (!ctx) return;

  new Chart(ctx, {
    type: 'line',
    data: {
      labels,
      datasets: [{
        label: 'Waga (kg)',
        data,
        tension: 0.25,
        pointRadius: 3,
        pointHoverRadius: 5,
        borderWidth: 2,
        fill: false
      }]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      plugins: {
        legend: { display: true }
      },
      scales: {
        x: {
          ticks: { maxRotation: 60, minRotation: 0 }
        },
        y: {
          beginAtZero: false
        }
      }
    }
  });
})();
</script>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>