Klaster OKD 4.19 (OpenShift) – CD/CI przy użyciu Argo CD, GitLab oraz Jenkins

Powiązane z tematem wpisy

Założenie wstepne:

Monorepo

    • Najprostsza mentalnie ścieżka: jeden projekt w GitLab, jedno miejsce “prawdy”.
    • Szybkie startowanie: mniej konfiguracji, mniej credentiali, mniej repoURL w Argo.
    • Łatwiejsze debugowanie: commit zawiera i zmianę kodu, i zmianę deploy (widać w jednym diffie).
    • Mniej elementów do popsucia w homelabie: mniej webhooków, mniej tokenów, mniej projektów w GitLab.

    Wzorzec A (polecany): Jeden kod, różne wersje per środowisko (promocja release’ów)

    To jest najczystszy i najczęściej stosowany model.

    Jak to działa

    Jest jeden katalog app/ (jeden kod).

    Dev dostaje najnowsze buildy (np. z każdej zmiany).

    Bis dostaje buildy “kandydujące” (np. tylko z MR do main albo z tagów rc-*).

    Prod dostaje tylko release (np. tag v1.2.3) i ręczny sync Argo.

    Różnice między środowiskami?

    Nie przez różne katalogi, tylko przez:

    inny image pullspec/digest ustawiany w values-dev.yaml / values-bis.yaml / values-prod.yaml

    ewentualnie feature flags w ConfigMap (to w praktyce jest “inny kod aktywny” bez forka)

    Zalety

    Zero duplikacji kodu.

    Najłatwiejsza promocja i rollback.

    W pełni zgodne z GitOps i dobrymi praktykami.

    Kiedy to spełnia Twoje “inne wersje”?

    Jeśli przez “inne” masz na myśli:

    dev ma wersję 1.0.0-SNAPSHOT i częste zmiany

    bis ma 1.0.0-RC1

    prod ma 1.0.0

    Najważniejsza zmiana: pipeline w Jenkins

    dev overlay na main

    bis overlay na tag rc-*

    prod overlay na tag v*

    Procedura aktualizacji kodu aplikacji krok po kroku

    Zmieniam kod i chcę to przetestować na DEV

    nano /repo/git/hellodevops/app/Main.java
    cd /repo/git/hellodevops
    git add app/Main.java
    git commit -m "feat: zmiana X"
    git push origin main

    Co się dzieje dalej automatycznie:

    • Jenkins odpala build z tego commita
    • Jenkins aktualizuje values-dev.yaml (pullspec + APP_VERSION = dev-)
    • ArgoCD-dev robi auto-sync i wdraża na hellodevops-dev

    Czyli: DEV = zwykłe push na main.

    Ten stan jest OK, chcę go wrzucić na BIS

    Robisz tag na tym konkretnym commicie, który chcesz promować.

    cd /repo/git/hellodevops
    git checkout main
    git pull
    git log --oneline -1   # upewnij się, że to ten commit
    git tag rc-1.0.0-1
    git push origin rc-1.0.0-1

    Co się dzieje dalej automatycznie:

    • Jenkins odpala pipeline dla tagu rc-*
    • buduje obraz (z tego samego kodu)
    • aktualizuje values-bis.yaml (pullspec + APP_VERSION = rc-1.0.0-1)
    • ArgoCD-bis robi auto-sync i wdraża na hellodevops-bis

    Czyli: BIS = tag rc-* na wybranym commicie, bez nowego commita.

    BIS jest OK, wypuszczam to na PROD

    Robisz tag release v* na tym samym commicie, który przeszedł BIS.

    Jeśli po testach BIS nie wprowadzałeś już zmian w kodzie, to po prostu:

    Co się dzieje:

    • Jenkins odpala pipeline dla tagu v*
    • buduje obraz
    • aktualizuje values-prod.yaml (pullspec + APP_VERSION = 1.0.0)
    • ArgoCD-prod wykryje zmianę i pokaże OutOfSync
    • (bo prod ma manual sync)
    • w ArgoCD klikasz Sync ręcznie

    Czyli: PROD = tag v* + ręczny Sync w ArgoCD.

    git checkout main
    git pull
    git log --oneline -1   # albo checkout konkretnego commita, który był w RC
    git tag v1.0.0
    git push origin v1.0.0
    # potem ręczny Sync w ArgoCD-prod

    W praktyce: tak jest najbezpieczniej

    DEV: ciągłe testy

    BIS: candidate

    PROD: release

    Jedno “centrum budowania” = mniej chaosu i mniej duplikacji

    CI (budowanie obrazów) – robisz w jednym miejscu, CD (wdrażanie na dev/bis/prod) – robisz w trzech miejscach

    BuildConfig + ImageStream to infrastruktura CI. Gdybyś miał to w 3 namespace’ach, to dostajesz:

    3 × BuildConfig do utrzymania (te same definicje)

    3 × ImageStreamy

    3 × uprawnienia i polityki

    3 × miejsca, gdzie coś może się rozjechać

    A przecież obraz aplikacji ma być ten sam artefakt, który potem tylko promujesz na kolejne środowiska.

    W Twojej konfiguracji Helm używa image.pullspec (digest), np.:

    image-registry.openshift-image-registry.svc:5000/hellodevops-dev/hellodevops@sha256:…

    To jest konkretna, niezmienna wersja obrazu.

    Teraz promocja wygląda tak:

    Dev ma pullspec = sha256:AAA (najświeższy)

    Bis ma pullspec = sha256:BBB (RC)

    Prod ma pullspec = sha256:CCC (release)

    To są różne obrazy, ale wszystkie są “zbudowane” w tym samym systemie CI.

    Dev

    buildy często, szybko

    BuildConfig w dev ma sens jako “CI workspace”

    dev i tak jest “brudny” z definicji

    Bis

    staging/RC, ma być stabilniej

    nie chcesz tam budować 100 razy dziennie

    chcesz wdrażać już zbudowane RC

    Prod

    ma być najczystszy

    nie budujesz, tylko wdrażasz sprawdzony artefakt

    ręczny Sync w Argo dodatkowo to podkreśla

    Musisz dać uprawnienia do pull z innych namespace’ów

    W OpenShift to się robi poprzez przypisanie roli system:image-puller na hellodevops-dev dla serviceaccountów z bis i prod.

    oc policy add-role-to-group system:image-puller system:serviceaccounts:hellodevops-bis -n hellodevops-dev
    oc policy add-role-to-group system:image-puller system:serviceaccounts:hellodevops-prod -n hellodevops-dev

    Przygotowanie dostępu przez SSH na bastionie do GitLaba

    # Wygeneruj klucz SSH (na bastionie)
    sudo -u bastuser -H bash -lc '
    mkdir -p ~/.ssh && chmod 700 ~/.ssh
    ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_gitlab_okdlab -C "bastion->gitlab.okdlab.local" -N ""
    '
    # Dodaj host key GitLaba do known_hosts (żeby nie pytał interaktywnie)
    sudo -u bastuser -H bash -lc '
    ssh-keyscan -H gitlab.okdlab.local >> ~/.ssh/known_hosts
    chmod 600 ~/.ssh/known_hosts
    '
    
    # Dodaj wpis w ~/.ssh/config (wygoda + pewność, że używa właściwego klucza)
    sudo -u bastuser -H bash -lc '
    cat >> ~/.ssh/config <

    Logujemy się do GitLaba użytkownikiem, który będzie aktualizował repo. User settings -> SSH Keys wklejamy zawartość klucza publicznego

    # weryfikacja z bastionu
    sudo -u bastuser -H bash -lc 'ssh -T [email protected]'

    Przygotowanie repozytorium hellodevops w GitLab.

    Zakładamy nowy pusty projekt o nazwie hellodevops w GitLab.

    • Odznaczamy Initialize repository with README
    • Wybieramy grupę devops w Project URL

    # sklonujpuste  repo do /repo/git/hellodevops
    sudo mkdir -p /repo/git
    sudo chown -R bastuser:bastuser /repo/git
    
    sudo -u bastuser -H bash -lc '
    cd /repo/git
    git clone [email protected]:dev-ops/hellodevops.git
    '
    # Utworzenie struktury katalogów
    sudo -u bastuser -H bash -lc '
    cd /repo/git/hellodevops
    
    mkdir -p app
    mkdir -p gitops/argocd
    mkdir -p gitops/helm/templates
    mkdir -p gitops/kustomize/base
    mkdir -p gitops/kustomize/overlays/dev
    mkdir -p gitops/kustomize/overlays/bis
    mkdir -p gitops/kustomize/overlays/prod
    '
    
    # Minimalne pliki (na razie “szkielet”)
    sudo -u bastuser -H bash -lc '
    cd /repo/git/hellodevops
    
    cat > README.md < .gitignore <

    Struktura projektu hellodevops

    .
    ├── app
    │   ├── Dockerfile
    │   └── Main.java
    ├── .git
    │   ├── config
    │   ├── description
    │   ├── HEAD
    │   ├── hooks
    │   │   ├── applypatch-msg.sample
    │   │   ├── commit-msg.sample
    │   │   ├── fsmonitor-watchman.sample
    │   │   ├── post-update.sample
    │   │   ├── pre-applypatch.sample
    │   │   ├── pre-commit.sample
    │   │   ├── pre-merge-commit.sample
    │   │   ├── prepare-commit-msg.sample
    │   │   ├── pre-push.sample
    │   │   ├── pre-rebase.sample
    │   │   ├── pre-receive.sample
    │   │   ├── push-to-checkout.sample
    │   │   ├── sendemail-validate.sample
    │   │   └── update.sample
    │   ├── info
    │   │   └── exclude
    │   ├── objects
    │   │   ├── info
    │   │   └── pack
    │   └── refs
    │       ├── heads
    │       └── tags
    ├── .gitignore
    ├── gitops
    │   ├── argocd
    │   │   ├── app-hellodevops-bis.yaml
    │   │   ├── app-hellodevops-dev.yaml
    │   │   ├── app-hellodevops-prod.yaml
    │   │   └── project-hellodevops.yaml
    │   ├── helm
    │   │   ├── Chart.yaml
    │   │   ├── templates
    │   │   │   ├── configmap.yaml
    │   │   │   ├── deployment.yaml
    │   │   │   └── secret.yaml
    │   │   └── values.yaml
    │   └── kustomize
    │       ├── base
    │       │   └── kustomization.yaml
    │       └── overlays
    │           ├── bis
    │           │   ├── kustomization.yaml
    │           │   └── values-bis.yaml
    │           ├── dev
    │           │   ├── kustomization.yaml
    │           │   └── values-dev.yaml
    │           └── prod
    │               ├── kustomization.yaml
    │               └── values-prod.yaml
    └── README.md
    
    21 directories, 38 files
    

    Pierwszy commit i push (żeby repo nie było puste)

    sudo -u bastuser -H bash -lc '
    cd /repo/git/hellodevops
    git add .
    git branch -M main
    git commit -m "chore: initial skeleton (app + gitops structure)"
    git push -u origin main
    '

    Przygotowanie plików projektu

    # hellodevops
    
    Projekt treningowy OKD/ArgoCD/GitLab/Jenkins.
    
    ## Struktura repo
    - `app/`    – kod aplikacji (1 plik Java) + Dockerfile
    - `gitops/` – GitOps: Helm + Kustomize + Argo CD (dev/bis/prod)
    
    ## Środowiska (OpenShift namespaces)
    - `hellodevops-dev`
    - `hellodevops-bis`
    - `hellodevops-prod`
    
    ## GitOps flow (skrót)
    - Jenkins odpala build w OKD (BuildConfig), OKD publikuje obraz do internal registry
    - Jenkins pobiera pullspec (digest) i aktualizuje `gitops/kustomize/overlays/*/values-*.yaml`
    - Argo CD:
      - `dev` i `bis`: auto-sync
      - `prod`: manual sync (commit jest, ale wdrożenie ręcznie w Argo)
    # Java
    *.class
    *.log
    
    # OS
    .DS_Store
    
    # IDE
    .idea/
    .vscode/
    FROM eclipse-temurin:21-jdk AS build
    WORKDIR /src
    
    # PostgreSQL JDBC driver
    ARG PGJDBC_VERSION=42.7.4
    RUN mkdir -p /deps \
     && curl -fsSL -o /deps/postgresql.jar \
        https://repo1.maven.org/maven2/org/postgresql/postgresql/${PGJDBC_VERSION}/postgresql-${PGJDBC_VERSION}.jar
    
    COPY Main.java /src/Main.java
    RUN javac -cp /deps/postgresql.jar Main.java
    
    FROM eclipse-temurin:21-jre
    WORKDIR /app
    
    COPY --from=build /src/Main.class /app/Main.class
    COPY --from=build /deps/postgresql.jar /app/postgresql.jar
    
    USER 1001
    CMD ["java", "-cp", "/app:/app/postgresql.jar", "Main"]
    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.sql.PreparedStatement;
    import java.sql.SQLException;
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    public class Main {
        private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
        private static String envRaw(String key) {
            return System.getenv(key);
        }
    
        private static String env(String key, String def) {
            String v = envRaw(key);
            return (v == null || v.isBlank()) ? def : v;
        }
    
        private static boolean isPresent(String key) {
            String v = envRaw(key);
            return v != null && !v.isBlank();
        }
    
        private static int parseIntSafe(String s, int def) {
            try { return Integer.parseInt(s); } catch (Exception e) { return def; }
        }
    
        private static String now() {
            return LocalDateTime.now().format(FMT);
        }
    
        private static void log(String level, String msg) {
            System.out.println(level + " " + msg);
        }
    
        private static void logInfo(String msg)  { log("[INFO]", msg); }
        private static void logWarn(String msg)  { log("[WARN]", msg); }
        private static void logOk(String msg)    { log("[OK]", msg); }
        private static void logErr(String msg)   { log("[ERROR]", msg); }
    
        private static String buildJdbcUrl(String host, String port, String db) {
            // connectTimeout/socketTimeout: sekundy (pgjdbc)
            return "jdbc:postgresql://" + host + ":" + port + "/" + db
                    + "?connectTimeout=5&socketTimeout=5&tcpKeepAlive=true";
        }
    
        private static boolean insertEntry(String jdbcUrl, String user, String pass, String entryName, int entryDelay, String dbNameForMsg) {
            String sql = "INSERT INTO entries (entry_name, entry_delay) VALUES (?, ?)";
            try (Connection conn = DriverManager.getConnection(jdbcUrl, user, pass);
                 PreparedStatement ps = conn.prepareStatement(sql)) {
    
                ps.setString(1, entryName);
                ps.setInt(2, entryDelay);
    
                int rows = ps.executeUpdate();
                if (rows == 1) {
                    logOk("Poprawnie dodano 1 nowy rekord do tabeli entries w bazie " + dbNameForMsg
                            + " o treści entry_name = " + entryName + ", entry_delay = " + entryDelay);
                    return true;
                } else {
                    logErr("Nie udało dodać się nowego rekordu do tabeli entries w bazie " + dbNameForMsg
                            + " o treści entry_name = " + entryName + ", entry_delay = " + entryDelay
                            + " (rows=" + rows + ")");
                    return false;
                }
            } catch (SQLException e) {
                logErr("Nie udało dodać się nowego rekordu do tabeli entries w bazie " + dbNameForMsg
                        + " o treści entry_name = " + entryName + ", entry_delay = " + entryDelay
                        + " (SQLState=" + e.getSQLState() + ", message=" + e.getMessage() + ")");
                return false;
            } catch (Exception e) {
                logErr("Nie udało dodać się nowego rekordu do tabeli entries w bazie " + dbNameForMsg
                        + " o treści entry_name = " + entryName + ", entry_delay = " + entryDelay
                        + " (message=" + e.getMessage() + ")");
                return false;
            }
        }
    
        public static void main(String[] args) throws Exception {
            // Wymagane do trybu [INFO]
            String[] required = new String[] {
                    "APP_VERSION", "ENVIRONMENT", "USER_NAME", "DELAY",
                    "DBA_SERVER", "DBA_NAME", "DBA_PORT", "DBA_USER", "DBA_PASSWORD"
            };
    
            Map presentMap = new LinkedHashMap<>();
            boolean allPresent = true;
            for (String k : required) {
                boolean p = isPresent(k);
                presentMap.put(k, p);
                allPresent = allPresent && p;
            }
    
            // Czytamy z defaultami (żeby app nie wybuchała)
            String appVersion  = env("APP_VERSION", "0.0.0");
            String environment = env("ENVIRONMENT", "unknown");
            String userName    = env("USER_NAME", "unknown");
            int delaySeconds   = parseIntSafe(env("DELAY", "5"), 5);
    
            String dbaServer   = env("DBA_SERVER", "127.0.0.1");
            String dbaName     = env("DBA_NAME", "postgres");
            String dbaPort     = env("DBA_PORT", "5432");
            String dbaUser     = env("DBA_USER", "postgres");
            String dbaPassword = env("DBA_PASSWORD", "");
    
            // Nagłówki diagnostyczne (bez hasła!)
            System.out.println("=== hellodevops ===");
            System.out.println("APP_VERSION=" + appVersion);
            System.out.println("ENVIRONMENT=" + environment);
            System.out.println("USER_NAME=" + userName);
            System.out.println("DELAY=" + delaySeconds);
            System.out.println("DBA_SERVER=" + dbaServer);
            System.out.println("DBA_NAME=" + dbaName);
            System.out.println("DBA_PORT=" + dbaPort);
            System.out.println("DBA_USER=" + dbaUser);
            System.out.println("DBA_PASSWORD=" + (dbaPassword.isBlank() ? "" : ""));
            System.out.println("===================");
    
            if (!allPresent) {
                StringBuilder sb = new StringBuilder("Brak wymaganych zmiennych środowiskowych: ");
                boolean first = true;
                for (Map.Entry e : presentMap.entrySet()) {
                    if (!e.getValue()) {
                        if (!first) sb.append(", ");
                        sb.append(e.getKey());
                        first = false;
                    }
                }
                logWarn(sb.toString());
            }
    
            String jdbcUrl = buildJdbcUrl(dbaServer, dbaPort, dbaName);
    
            while (true) {
                String line = "Witaj " + userName + ", dziś jest " + now() + ", czekam następne " + delaySeconds + " sekund";
    
                if (allPresent) {
                    logInfo(line);
                    // INSERT tylko gdy INFO
                    insertEntry(jdbcUrl, dbaUser, dbaPassword, userName, delaySeconds, dbaName);
                } else {
                    logWarn(line);
                }
    
                Thread.sleep(delaySeconds * 1000L);
            }
        }
    }

    GitOps: Helm

    apiVersion: v2
    name: hellodevops
    description: Minimal hellodevops app (ConfigMap + Secret + Deployment)
    type: application
    version: 0.1.0
    appVersion: "1.0.0"

    (bazowe — będą nadpisywane overlayami)

    nameOverride: "hellodevops"
    
    image:
      # Preferujemy pullspec (digest) ustawiany przez Jenkins
      pullspec: ""
    
    config:
      APP_VERSION: "1.0.0"
      ENVIRONMENT: "prod"
      USER_NAME: "Zbychu"
      DELAY: "5"
    
    db:
      DBA_SERVER: "192.168.40.25"
      DBA_NAME: "dba_hellodevops_prod"
      DBA_PORT: "5432"
      DBA_USER: "usr_hellodevops_prod"
      DBA_PASSWORD: "CHANGE_ME"
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: cm-hellodevops
      labels:
        app.kubernetes.io/name: {{ .Values.nameOverride | quote }}
    data:
      APP_VERSION: {{ .Values.config.APP_VERSION | quote }}
      ENVIRONMENT: {{ .Values.config.ENVIRONMENT | quote }}
      USER_NAME: {{ .Values.config.USER_NAME | quote }}
      DELAY: {{ .Values.config.DELAY | quote }}
    
      DBA_SERVER: {{ .Values.db.DBA_SERVER | quote }}
      DBA_NAME: {{ .Values.db.DBA_NAME | quote }}
      DBA_PORT: {{ .Values.db.DBA_PORT | quote }}
      DBA_USER: {{ .Values.db.DBA_USER | quote }}
    apiVersion: v1
    kind: Secret
    metadata:
      name: secret-hellodevops-db
    type: Opaque
    stringData:
      DBA_PASSWORD: {{ .Values.db.DBA_PASSWORD | quote }}
    {{- $img := "" -}}
    {{- if .Values.image.pullspec -}}
    {{- $img = .Values.image.pullspec -}}
    {{- else -}}
    {{- $img = "image-not-set" -}}
    {{- end -}}
    
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: {{ .Values.nameOverride | quote }}
      labels:
        app.kubernetes.io/name: {{ .Values.nameOverride | quote }}
    spec:
      replicas: 1
      selector:
        matchLabels:
          app.kubernetes.io/name: {{ .Values.nameOverride | quote }}
      template:
        metadata:
          labels:
            app.kubernetes.io/name: {{ .Values.nameOverride | quote }}
        spec:
          containers:
            - name: {{ .Values.nameOverride | quote }}
              image: {{ $img | quote }}
              imagePullPolicy: Always
              envFrom:
                - configMapRef:
                    name: cm-hellodevops
                - secretRef:
                    name: secret-hellodevops-db
              resources:
                requests:
                  cpu: "10m"
                  memory: "64Mi"
                limits:
                  cpu: "200m"
                  memory: "256Mi"

    GitOps: Kustomize

    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    
    helmCharts:
      - name: hellodevops
        releaseName: hellodevops
        version: 0.1.0
        chartPath: ../../helm
        valuesFile: ../../helm/values.yaml
    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    
    resources:
      - ../../base
    
    helmCharts:
      - name: hellodevops
        releaseName: hellodevops
        version: 0.1.0
        chartPath: ../../../helm
        valuesFile: values-dev.yaml
    nameOverride: "hellodevops"
    
    image:
      pullspec: ""  # ustawiane przez Jenkins
    
    config:
      APP_VERSION: "dev"   # Jenkins nadpisze np. dev-
      ENVIRONMENT: "dev"
      USER_NAME: "Zbychu-Dev"
      DELAY: "5"
    
    db:
      DBA_SERVER: "192.168.40.25"
      DBA_NAME: "dba_hellodevops_dev"
      DBA_PORT: "5432"
      DBA_USER: "usr_hellodevops_dev"
      DBA_PASSWORD: "AZXCmntDEV26"
    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    
    resources:
      - ../../base
    
    helmCharts:
      - name: hellodevops
        releaseName: hellodevops
        version: 0.1.0
        chartPath: ../../../helm
        valuesFile: values-bis.yaml
    nameOverride: "hellodevops"
    
    image:
      pullspec: ""  # ustawiane przez Jenkins
    
    config:
      APP_VERSION: "bis"   # Jenkins nadpisze np. rc-1.0.0-1
      ENVIRONMENT: "bis"
      USER_NAME: "Zbychu-Bis"
      DELAY: "5"
    
    db:
      DBA_SERVER: "192.168.40.25"
      DBA_NAME: "dba_hellodevops_bis"
      DBA_PORT: "5432"
      DBA_USER: "usr_hellodevops_bis"
      DBA_PASSWORD: "AZXCmntBIS26"
    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    
    resources:
      - ../../base
    
    helmCharts:
      - name: hellodevops
        releaseName: hellodevops
        version: 0.1.0
        chartPath: ../../../helm
        valuesFile: values-prod.yaml
    nameOverride: "hellodevops"
    
    image:
      pullspec: ""  # ustawiane przez Jenkins
    
    config:
      APP_VERSION: "prod"  # Jenkins nadpisze np. 1.0.0
      ENVIRONMENT: "prod"
      USER_NAME: "Zbychu-Prod"
      DELAY: "5"
    
    db:
      DBA_SERVER: "192.168.40.25"
      DBA_NAME: "dba_hellodevops_prod"
      DBA_PORT: "5432"
      DBA_USER: "usr_hellodevops_prod"
      DBA_PASSWORD: "AZXCmntPROD26"

    GitOps: Argo CD

    apiVersion: argoproj.io/v1alpha1
    kind: AppProject
    metadata:
      name: hellodevops
      namespace: argocd
    spec:
      description: hellodevops project (dev/bis/prod)
      sourceRepos:
        - '*'
      destinations:
        - namespace: hellodevops-dev
          server: https://kubernetes.default.svc
        - namespace: hellodevops-bis
          server: https://kubernetes.default.svc
        - namespace: hellodevops-prod
          server: https://kubernetes.default.svc
      clusterResourceWhitelist:
        - group: ''
          kind: Namespace
      namespaceResourceWhitelist:
        - group: '*'
          kind: '*'
    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: hellodevops-dev
      namespace: argocd
    spec:
      project: hellodevops
      source:
        repoURL: 'ssh://[email protected]/dev-ops/hellodevops.git'
        targetRevision: main
        path: gitops/kustomize/overlays/dev
      destination:
        server: 'https://kubernetes.default.svc'
        namespace: hellodevops-dev
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true
    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: hellodevops-bis
      namespace: argocd
    spec:
      project: hellodevops
      source:
        repoURL: 'ssh://[email protected]/dev-ops/hellodevops.git'
        targetRevision: main
        path: gitops/kustomize/overlays/bis
      destination:
        server: 'https://kubernetes.default.svc'
        namespace: hellodevops-bis
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true
    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: hellodevops-prod
      namespace: argocd
    spec:
      project: hellodevops
      source:
        repoURL: 'ssh://[email protected]/dev-ops/hellodevops.git'
        targetRevision: main
        path: gitops/kustomize/overlays/prod
      destination:
        server: 'https://kubernetes.default.svc'
        namespace: hellodevops-prod
      syncPolicy:
        syncOptions:
          - CreateNamespace=true

    Wypchnięcie wszystkiego do GitLaba

    # Dodaj i zrób commit
    cd /repo/git/hellodevops
    
    # Sprawdź, co dokładnie zmieniasz
    git status
    git diff --stat
    
    git add .
    git commit -m "feat: hellodevops app + gitops manifests"
    git push -u origin main
    # Szybka weryfikacja po pushu
    git log --oneline -5
    git remote -v

    Dodanie pliku build-bc.yaml do overlay dev

    apiVersion: image.openshift.io/v1
    kind: ImageStream
    metadata:
      name: hellodevops
      labels:
        app.kubernetes.io/name: hellodevops
    ---
    apiVersion: build.openshift.io/v1
    kind: BuildConfig
    metadata:
      name: hellodevops
      labels:
        app.kubernetes.io/name: hellodevops
    spec:
      runPolicy: Serial
      source:
        type: Git
        git:
          uri: "ssh://[email protected]/dev-ops/hellodevops.git"
        contextDir: "app"
      strategy:
        type: Docker
        dockerStrategy:
          dockerfilePath: Dockerfile
      output:
        to:
          kind: ImageStreamTag
          name: "hellodevops:latest"
      triggers: []

    Dopisz build-bc.yaml w kustomization.yaml overlay dev

    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    
    resources:
      - ../../base
      - build-bc.yaml
    
    helmCharts:
      - name: hellodevops
        releaseName: hellodevops
        version: 0.1.0
        chartPath: ../../../helm
        valuesFile: values-dev.yaml

    Commit i push do GitLaba

    cd /repo/git/hellodevops
    git add gitops/kustomize/overlays/dev/build-bc.yaml gitops/kustomize/overlays/dev/kustomization.yaml
    git commit -m "gitops(dev): add ImageStream + BuildConfig for in-cluster builds"
    git push origin main

    Utwórzenie namespaces (projektów -dev,-bis , -prod)

    # Jeśli chcesz, żeby CreateNamespace=true faktycznie działało, Argo musi mieć clusterowe uprawnienia do tworzenia namespaces/projects.
    oc adm policy add-cluster-role-to-user cluster-admin \
      system:serviceaccount:argocd:argocd-application-controller

    My jednak założymy projekty ręcznie, zeby nie dawać tak wysokich uprawniń Argo CD na klastrze.

    # utwórz namespaces (projekty)
    oc new-project hellodevops-dev
    oc new-project hellodevops-bis
    oc new-project hellodevops-prod
    
    # Nadaj rolę admin w każdym namespace dla service account ArgoCD:
    oc adm policy add-role-to-user admin \
      system:serviceaccount:argocd:argocd-application-controller \
      -n hellodevops-dev
    
    oc adm policy add-role-to-user admin \
      system:serviceaccount:argocd:argocd-application-controller \
      -n hellodevops-bis
    
    oc adm policy add-role-to-user admin \
      system:serviceaccount:argocd:argocd-application-controller \
      -n hellodevops-prod
    
    # weryfikacja: czy teraz can-i zmieniło się na YES
    oc -n hellodevops-dev auth can-i create buildconfigs \
      --as system:serviceaccount:argocd:argocd-application-controller
    
    oc -n hellodevops-dev auth can-i create imagestreams \
      --as system:serviceaccount:argocd:argocd-application-controller

    Dodanie manifestów do projektu argocd

    oc apply -n argocd -f /repo/git/hellodevops/gitops/argocd/project-hellodevops.yaml
    oc apply -n argocd -f /repo/git/hellodevops/gitops/argocd/app-hellodevops-dev.yaml
    oc apply -n argocd -f /repo/git/hellodevops/gitops/argocd/app-hellodevops-bis.yaml
    oc apply -n argocd -f /repo/git/hellodevops/gitops/argocd/app-hellodevops-prod.yaml

    Utworzenie klucza SSH dla ArgoCD do GitLaba
    Jeśli po utworzeniu manifestów przez oc apply -n argocd … mamy podobne jak poniżej błędy to brak jest klucza SSH dla ArgoCD di Gitlaba.

    oc -n argocd describe application hellodevops-dev | sed -n '/Conditions:/,/Events:/p'
    
      Conditions:
        Last Transition Time:  2026-02-22T12:27:24Z
        Message:               Failed to load target state: failed to generate manifest for source 1 of 1: rpc error: code = Unknown desc = failed to list refs: error creating SSH agent: "SSH agent requested but SSH_AUTH_SOCK not-specified"
        Type:                  ComparisonError
      Controller Namespace:    argocd
      Health:
        Last Transition Time:  2026-02-22T12:27:24Z
        Status:                Healthy
      Reconciled At:           2026-02-22T12:27:24Z
      Sync:
        Compared To:
          Destination:
            Namespace:  hellodevops-dev
            Server:     https://kubernetes.default.svc
          Source:
            Path:             gitops/kustomize/overlays/dev
            Repo URL:         ssh://[email protected]/dev-ops/hellodevops.git
            Target Revision:  main
        Status:               Unknown
    Events:
    

    Wygeneruj deploy key dla ArgoCD (na bastionie)

    # na bastionie
    ssh-keygen -t ed25519 -f /tmp/argocd_gitlab_hellodevops -C "argocd@okdlab -> hellodevops" -N ""
    
    # To tworzy dwa pliki:
    # /tmp/argocd_gitlab_hellodevops (private key) NIE UDOSTĘPNIAJ
    # /tmp/argocd_gitlab_hellodevops.pub (public key) — to dodasz w GitLab
    cat /tmp/argocd_gitlab_hellodevops.pub

    Dodaj public key do GitLaba jako Deploy Key (read-only)

    Repo -> Settings -> Repository. Wkleić zawartość /tmp/argocd_gitlab_hellodevops.pub

    Przygotuj known_hosts dla GitLaba (na bastionie)

    ArgoCD musi ufać host key GitLaba (żeby nie było promptów i błędów).

    ssh-keyscan -H gitlab.okdlab.local > /tmp/gitlab.okdlab.local_known_hosts
    cat /tmp/gitlab.okdlab.local_known_hosts

    Utwórz repo secret w ArgoCD (namespace argocd)

    oc -n argocd create secret generic repo-hellodevops \
      --from-literal=type=git \
      --from-literal=url=ssh://[email protected]/dev-ops/hellodevops.git \
      --from-file=sshPrivateKey=/tmp/argocd_gitlab_hellodevops \
      --from-file=knownHosts=/tmp/gitlab.okdlab.local_known_hosts \
      -o yaml --dry-run=client | oc apply -f -

    I teraz najważniejsze: label, który mówi ArgoCD, że to repo secret:

    oc -n argocd label secret repo-hellodevops argocd.argoproj.io/secret-type=repository --overwrite
    
    # Szybka weryfikacja
    oc -n argocd get secret repo-hellodevops -o jsonpath='{.metadata.labels.argocd\.argoproj\.io/secret-type}{"\n"}'
    # Powinno zwrócić: repository

    Wymuś refresh aplikacji (żeby przestało być “Unknown”)

    oc -n argocd annotate application hellodevops-dev --overwrite refreshTimestamp="$(date +%s)"
    
    # Po chwili sprawdź:
    oc -n argocd get applications.argoproj.io | egrep 'hellodevops|NAME'
    
    # I sprawdź jeszcze raz condition:
    oc -n argocd describe application hellodevops-dev | sed -n '/Conditions:/,/Events:/p'

    W razie błędu jak poniżej

     oc -n argocd describe application hellodevops-dev | sed -n '/Conditions:/,/Events:/p'
     
    Message:               Failed to load target state: failed to generate manifest for source 1 of 1: rpc error: code = Unknown desc = `kustomize build /gitops/kustomize/overlays/dev` failed exit status 1: Error: invalid Kustomization: json: unknown field "chartPath"
    

    poprawić definicję chartPath w helmCharts

    cd /repo/git/hellodevops
    
    # szybka zamiana sedem w tych 4 plikach
    sed -i 's/^    chartPath:/    path:/g' gitops/kustomize/base/kustomization.yaml
    sed -i 's/^    chartPath:/    path:/g' gitops/kustomize/overlays/dev/kustomization.yaml
    sed -i 's/^    chartPath:/    path:/g' gitops/kustomize/overlays/bis/kustomization.yaml
    sed -i 's/^    chartPath:/    path:/g' gitops/kustomize/overlays/prod/kustomization.yaml
    
    git diff --stat
    
    git add gitops/kustomize/base/kustomization.yaml \
            gitops/kustomize/overlays/dev/kustomization.yaml \
            gitops/kustomize/overlays/bis/kustomization.yaml \
            gitops/kustomize/overlays/prod/kustomization.yaml
    
    git commit -m "fix: kustomize helmCharts use path instead of chartPath (argocd compatible)"
    git push origin main

    Wymuś refresh i sprawdź czy błąd zniknął

    oc -n argocd annotate application hellodevops-dev --overwrite refreshTimestamp="$(date +%s)"
    oc -n argocd describe application hellodevops-dev | sed -n '/Conditions:/,/Events:/p'
    oc -n argocd run -it --rm tcpcheck \
      --image=registry.access.redhat.com/ubi9/ubi-minimal \
      --restart=Never -- sh -lc '
    echo "Trying TCP connect to repo-server svc 172.30.86.53:8081..."
    ( /dev/null 2>&1 && echo "OK: TCP port open" || echo "FAIL: connection refused/timeout"
    
    echo "Trying TCP connect to repo-server pod 10.128.2.150:8081..."
    ( /dev/null 2>&1 && echo "OK: TCP port open" || echo "FAIL: connection refused/timeout"
    '

    Oznacz hellodevops-* jako “managed” (TO jest klucz)

    oc label ns hellodevops-dev  argocd.argoproj.io/managed-by=argocd --overwrite
    oc label ns hellodevops-bis  argocd.argoproj.io/managed-by=argocd --overwrite
    oc label ns hellodevops-prod argocd.argoproj.io/managed-by=argocd --overwrite

    Sprawdź czy zasoby zaczęły się tworzyć w hellodevops-dev

    oc -n hellodevops-dev get cm,secret,deploy
    NAME                                 DATA   AGE
    configmap/cm-hellodevops             8      27s
    configmap/kube-root-ca.crt           1      103m
    configmap/openshift-service-ca.crt   1      103m
    
    NAME                              TYPE                      DATA   AGE
    secret/builder-dockercfg-jlwc6    kubernetes.io/dockercfg   1      103m
    secret/default-dockercfg-nbbqk    kubernetes.io/dockercfg   1      103m
    secret/deployer-dockercfg-x9mkg   kubernetes.io/dockercfg   1      103m
    secret/secret-hellodevops-db      Opaque                    1      27s
    
    NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/hellodevops   0/1     1            0           27s

    Dodaj BC/IS do Helm chartu (tylko dev)

    {{- if .Values.build.enabled }}
    apiVersion: image.openshift.io/v1
    kind: ImageStream
    metadata:
      name: {{ .Values.nameOverride | default "hellodevops" | quote }}
      labels:
        app.kubernetes.io/name: {{ .Values.nameOverride | default "hellodevops" | quote }}
    ---
    apiVersion: build.openshift.io/v1
    kind: BuildConfig
    metadata:
      name: {{ .Values.nameOverride | default "hellodevops" | quote }}
      labels:
        app.kubernetes.io/name: {{ .Values.nameOverride | default "hellodevops" | quote }}
    spec:
      runPolicy: Serial
      source:
        type: Git
        git:
          uri: {{ .Values.build.gitUri | quote }}
        contextDir: {{ .Values.build.contextDir | quote }}
      strategy:
        type: Docker
        dockerStrategy:
          dockerfilePath: Dockerfile
      output:
        to:
          kind: ImageStreamTag
          name: "{{ .Values.nameOverride | default "hellodevops" }}:latest"
      triggers: []
    {{- end }}

    uzupełnij

    nameOverride: "hellodevops"
    
    image:
      # Preferujemy pullspec (digest) ustawiany przez Jenkins
      pullspec: ""
    
    config:
      APP_VERSION: "1.0.0"
      ENVIRONMENT: "prod"
      USER_NAME: "Zbychu"
      DELAY: "5"
    
    db:
      DBA_SERVER: "192.168.40.25"
      DBA_NAME: "dba_hellodevops_prod"
      DBA_PORT: "5432"
      DBA_USER: "usr_hellodevops_prod"
      DBA_PASSWORD: "CHANGE_ME"
    
    build:
      enabled: false
      gitUri: "ssh://[email protected]/dev-ops/hellodevops.git"
      contextDir: "app"
    

    Włącz build tylko w DEV

    nameOverride: "hellodevops"
    
    image:
      pullspec: ""  # ustawiane przez Jenkins
    
    config:
      APP_VERSION: "dev"   # Jenkins nadpisze np. dev-
      ENVIRONMENT: "dev"
      USER_NAME: "Zbychu-Dev"
      DELAY: "5"
    
    db:
      DBA_SERVER: "192.168.40.25"
      DBA_NAME: "dba_hellodevops_dev"
      DBA_PORT: "5432"
      DBA_USER: "usr_hellodevops_dev"
      DBA_PASSWORD: "AZXCmntDEV26"
    
    build:
      enabled: true
      gitUri: "ssh://[email protected]/dev-ops/hellodevops.git"
      contextDir: "app"
    
    cd /repo/git/hellodevops
    git add gitops/charts/hellodevops/templates/buildconfig.yaml \
            gitops/charts/hellodevops/values.yaml \
            gitops/charts/hellodevops/values-dev.yaml
    git commit -m "feat(dev): add BuildConfig+ImageStream via Helm (enabled only in dev)"
    git push origin main
    
    # Potem (opcjonalnie) hard refresh:
    oc -n argocd annotate application hellodevops-dev --overwrite argocd.argoproj.io/refresh=hard
    
    # weryfikacja
    oc -n hellodevops-dev get bc,is

    Ręczny build w OKD, bez Jenkinsa

    oc -n hellodevops-dev start-build bc/hellodevops --follow --wait
    
    # w razie błędu
    build.build.openshift.io/hellodevops-1 started
    Cloning "ssh://[email protected]/dev-ops/hellodevops.git" ...
    error: Host key verification failed.
    fatal: Could not read from remote repository.
    
    Please make sure you have the correct access rights
    and the repository exists.
    
    # Dodaj known_hosts do namespace hellodevops-dev
    
    

    Przechodzimy na HTTPS + Project Access Token

    W projekcie hellodevops:

    skopiuj token

    Settings → Access Tokens (Project Access Token) albo Personal Access Token

    scope: read_repository

    oc -n hellodevops-dev create secret generic gitlab-http-creds \
      --from-literal=username=gitlab-ci-token \
      --from-literal=password='glpat-m2uRIx4dy6cgzZDvxKwcC286MQp1OjcH.01.0w05p6ggk' \
      -o yaml --dry-run=client | oc apply -f -

    Zmień BuildConfig, żeby klonował po HTTPS i używał tego secreta

    oc -n hellodevops-dev patch bc hellodevops --type merge -p '{
      "spec": {
        "source": {
          "git": {
            "uri": "https://gitlab.okdlab.local/dev-ops/hellodevops.git"
          }
        }
      }
    }'

    Podepnij secret jako sourceSecret

    oc -n hellodevops-dev patch bc hellodevops --type merge -p '{
      "spec": {
        "source": {
          "sourceSecret": { "name": "gitlab-http-creds" }
        }
      }
    }'
    # weryfikacja
    oc -n hellodevops-dev get bc hellodevops -o jsonpath='{.spec.source.git.uri}{"\n"}{.spec.source.sourceSecret.name}{"\n"}'
    # ma być 
    https://gitlab.okdlab.local/dev-ops/hellodevops.git
    gitlab-http-creds

    Ponowny build

    oc -n hellodevops-dev start-build bc/hellodevops --follow --wait
    # w razie tgo bledy wylacz weryfikacje SSSL
    Cloning "https://gitlab.okdlab.local/dev-ops/hellodevops.git" ...
    error: fatal: unable to access 'https://gitlab.okdlab.local/dev-ops/hellodevops.git/': SSL certificate problem: self-signed certificate
    error: the build hellodevops-dev/hellodevops-3 status is "Failed"

    Dodaj
    env:
    – name: GIT_SSL_NO_VERIFY
    value: „true”

    {{- if .Values.build.enabled }}
    apiVersion: image.openshift.io/v1
    kind: ImageStream
    metadata:
      name: {{ .Values.nameOverride | default "hellodevops" | quote }}
      labels:
        app.kubernetes.io/name: {{ .Values.nameOverride | default "hellodevops" | quote }}
    ---
    apiVersion: build.openshift.io/v1
    kind: BuildConfig
    metadata:
      name: {{ .Values.nameOverride | default "hellodevops" | quote }}
      labels:
        app.kubernetes.io/name: {{ .Values.nameOverride | default "hellodevops" | quote }}
    spec:
      runPolicy: Serial
      source:
        type: Git
        git:
          uri: {{ .Values.build.gitUri | quote }}
        contextDir: {{ .Values.build.contextDir | quote }}
      strategy:
        type: Docker
        dockerStrategy:
          dockerfilePath: Dockerfile
          env:
          - name: GIT_SSL_NO_VERIFY
            value: "true"
      output:
        to:
          kind: ImageStreamTag
          name: "{{ .Values.nameOverride | default "hellodevops" }}:latest"
      triggers: []
    {{- end }}
    

    Zmien ssh na https w gitUri

    nameOverride: "hellodevops"
    
    image:
      pullspec: ""  # ustawiane przez Jenkins
    
    config:
      APP_VERSION: "dev"   # Jenkins nadpisze np. dev-
      ENVIRONMENT: "dev"
      USER_NAME: "Zbychu-Dev"
      DELAY: "5"
    
    db:
      DBA_SERVER: "192.168.40.25"
      DBA_NAME: "dba_hellodevops_dev"
      DBA_PORT: "5432"
      DBA_USER: "usr_hellodevops_dev"
      DBA_PASSWORD: "AZXCmntDEV26"
    
    build:
      enabled: true
      gitUri: "https://[email protected]/dev-ops/hellodevops.git"
      contextDir: "app"
    
    cd /repo/git/hellodevops
    git add gitops/charts/hellodevops/templates/buildconfig.yaml gitops/charts/hellodevops/values-dev.yaml
    git commit -m "fix(dev): disable TLS verify for GitLab clone in BuildConfig (self-signed)"
    git push origin main
    # Wymuś refresh ArgoCD (żeby zaktualizowało BuildConfig)
    oc -n argocd annotate application hellodevops-dev --overwrite argocd.argoproj.io/refresh=hard
    
    # Sprawdź, czy BuildConfig ma już env
    oc -n hellodevops-dev get bc hellodevops -o yaml | sed -n '/dockerStrategy:/,/output:/p' | sed -n '1,80p'
    # Odpal build jeszcze raz
    oc -n hellodevops-dev start-build bc/hellodevops --follow --wait
    ...
    Writing manifest to image destination
    Successfully pushed image-registry.openshift-image-registry.svc:5000/hellodevops-dev/hellodevops@sha256:7c4dae7fe48f3d1aa4a3332a4c42ed46fb2d478b5d497933a40f2e79f5aced57
    Push successful
    
    # Pobierz pullspec z internal registry (digest)
    PULLSPEC=$(oc -n hellodevops-dev get istag hellodevops:latest -o jsonpath='{.image.dockerImageReference}')
    echo "$PULLSPEC"
    image-registry.openshift-image-registry.svc:5000/hellodevops-dev/hellodevops@sha256:7c4dae7fe48f3d1aa4a3332a4c42ed46fb2d478b5d497933a40f2e79f5aced57
    
    # Wstaw pullspec do values-dev.yaml i push
    nano /repo/git/hellodevops/gitops/charts/hellodevops/values-dev.yaml
    ...
    image:
      pullspec: "WKLEJ_TU_PULLSPEC"
    nameOverride: "hellodevops"
    
    image:
      pullspec: "image-registry.openshift-image-registry.svc:5000/hellodevops-dev/hellodevops@sha256:7c4dae7fe48f3d1aa4a3332a4c42ed46fb2d478b5d497933a40f2e79f5aced57"
    
    config:
      APP_VERSION: "dev"   # Jenkins nadpisze np. dev-
      ENVIRONMENT: "dev"
      USER_NAME: "Zbychu-Dev"
      DELAY: "5"
    
    db:
      DBA_SERVER: "192.168.40.25"
      DBA_NAME: "dba_hellodevops_dev"
      DBA_PORT: "5432"
      DBA_USER: "usr_hellodevops_dev"
      DBA_PASSWORD: "AZXCmntDEV26"
    
    build:
      enabled: true
      gitUri: "https://[email protected]/dev-ops/hellodevops.git"
      contextDir: "app"
    
    cd /repo/git/hellodevops
    git add gitops/charts/hellodevops/values-dev.yaml
    git commit -m "gitops(dev): set image pullspec after successful build"
    git push origin main

    Poczekaj aż ArgoCD zsyncuje i sprawdź pody

    # Zrestartuj Deployment
    oc -n argocd annotate application hellodevops-dev --overwrite argocd.argoproj.io/refresh=hard
    oc -n hellodevops-dev rollout restart deploy/hellodevops
    oc -n hellodevops-dev rollout status deploy/hellodevops
    
    # Sprawdź pod i logi
    oc -n hellodevops-dev get pods
    oc -n hellodevops-dev describe pod -l app.kubernetes.io/name=hellodevops | sed -n '/Events:/,$p'
    # Jak będzie Running, to logi:
    oc -n hellodevops-dev logs deploy/hellodevops --tail=80

    Dodatkowo (ważne na przyszłość) – bis/prod i pull z dev registry

    oc policy add-role-to-group system:image-puller system:serviceaccounts:hellodevops-bis -n hellodevops-dev
    oc policy add-role-to-group system:image-puller system:serviceaccounts:hellodevops-prod -n hellodevops-dev
    # jesli pod loguje na konsoli to znaczy ze jes tOK
    oc get pods
    NAME                         READY   STATUS      RESTARTS   AGE
    hellodevops-4-build          0/1     Completed   0          12m
    hellodevops-f7865565-q6vzm   1/1     Running     0          2m48s