diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..9be2227 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,112 @@ +stages: [build, deploy] + +variables: + IMAGE_REPO: "$CI_REGISTRY_IMAGE" + TAG: "${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}" + RELEASE: "open-meteo-service-gitlab" + NAMESPACE: "sandbox-gitlab" + CHART_PATH: "./open-meteo-service" + VALUES_FILE: "./values.yaml" + +build: + stage: build + tags: [homelab] + script: + - whoami + - set -euo pipefail + - unset DOCKER_HOST DOCKER_TLS_VERIFY DOCKER_CERT_PATH || true + - export DOCKER_HOST=unix:///var/run/docker.sock + - docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" + - docker build -t "$IMAGE_REPO:$TAG" . + - docker push "$IMAGE_REPO:$TAG" + - docker tag "$IMAGE_REPO:$TAG" "$IMAGE_REPO:latest" + - docker push "$IMAGE_REPO:latest" + - echo "Built and pushed $IMAGE_REPO:$TAG and $IMAGE_REPO:latest" + +deploy: + stage: deploy + tags: [homelab] + rules: + - if: '$CI_COMMIT_MESSAGE =~ /^ci: bump image tag/' + when: never + - when: on_success + script: |- + set -euo pipefail + + # Configure kubectl for k3s cluster + export KUBECONFIG=/tmp/k3s.yaml + echo "Configured kubectl to use k3s cluster" + kubectl cluster-info || echo "Warning: Could not connect to cluster yet" + + # Update values.yaml with the new tag using sed + sed -i "s/^ tag: .*/ tag: \"${TAG}\"/" "${VALUES_FILE}" + echo "Updated ${VALUES_FILE} with image.tag=${TAG}" + + # Verify the change + grep -A 2 "^image:" "${VALUES_FILE}" + + # Configure git identity + git config user.email "gitlab-ci@dvirlabs.com" + git config user.name "GitLab CI" + + # Commit & push values bump (skip-ci to avoid loop) + git add "${VALUES_FILE}" + git commit -m "ci: bump image tag to ${TAG} [skip ci]" || echo "No changes to commit" + + # Push using GITLAB_TOKEN (Project Access Token with api scope) + if [ -n "${GITLAB_TOKEN:-}" ]; then + git remote set-url origin "https://oauth2:${GITLAB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" + git push origin "HEAD:${CI_COMMIT_REF_NAME}" && echo "Pushed values.yaml update successfully" || echo "Failed to push, continuing deployment anyway" + else + echo "GITLAB_TOKEN not set, skipping git push (values.yaml is updated locally for this deployment)" + fi + + # Create namespace if it doesn't exist + kubectl create namespace "${NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f - + + # Create or update GitLab registry secret for pulling images + echo "Creating/updating image pull secret..." + kubectl create secret docker-registry gitlab-registry \ + --docker-server="${CI_REGISTRY}" \ + --docker-username="${CI_REGISTRY_USER}" \ + --docker-password="${CI_REGISTRY_PASSWORD}" \ + --namespace="${NAMESPACE}" \ + --dry-run=client -o yaml | kubectl apply -f - + + echo "Deploying with Helm..." + helm upgrade --install "${RELEASE}" "${CHART_PATH}" \ + -f "${VALUES_FILE}" \ + -n "${NAMESPACE}" \ + --create-namespace \ + --wait --timeout 10m \ + --debug + + echo "Waiting for deployment to be ready..." + kubectl -n "${NAMESPACE}" rollout status deploy -l app.kubernetes.io/name=open-meteo-service --timeout=180s + + echo "Port-forward for tests..." + SERVICE_NAME="${RELEASE}-open-meteo-service" + kubectl -n "${NAMESPACE}" port-forward "svc/${SERVICE_NAME}" 8000:8000 >/tmp/pf.log 2>&1 & + PF_PID=$! + sleep 5 + + echo "Testing /healthz..." + curl -fsS http://localhost:8000/healthz >/dev/null + echo "OK /healthz" + + echo "Testing /coordinates..." + COORDS_RESPONSE="$(curl -fsS http://localhost:8000/coordinates)" + echo "$COORDS_RESPONSE" | head -20 + if ! echo "$COORDS_RESPONSE" | grep -q '"data"'; then + echo "ERROR: /coordinates response missing 'data' field" + kill $PF_PID || true + exit 1 + fi + echo "OK /coordinates" + + echo "Testing /metrics..." + curl -fsS http://localhost:8000/metrics | head -20 + echo "OK /metrics" + + kill $PF_PID || true + echo "All tests passed!" \ No newline at end of file diff --git a/README.md b/README.md index e852044..cbc0ca0 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,78 @@ helm install open-meteo-service ./helm \ --set grafana.persistence.enabled=true ``` +## GitLab CI Workflow + +This project uses a GitLab CI/CD pipeline that automates building, versioning, deployment, and testing on an external k3s cluster (homelab). + +### Pipeline Overview + +The pipeline consists of two stages: **build** and **deploy**. + +#### 1. Build Stage +- **Docker Image Build**: Builds the application Docker image using the project Dockerfile +- **Tagging Strategy**: Tags each image with a unique identifier combining branch and commit SHA: + - Format: `-` (e.g., `main-a1b2c3d`, `feature-api-xyz1234`) + - Uses `CI_COMMIT_REF_SLUG` (sanitized branch name) and `CI_COMMIT_SHORT_SHA` +- **Multi-Tag Push**: Pushes two tags to GitLab Container Registry: + - `$IMAGE_REPO:-` - Specific version tag + - `$IMAGE_REPO:latest` - Always points to the most recent build +- **Registry Authentication**: Uses GitLab's built-in CI variables (`CI_REGISTRY`, `CI_REGISTRY_USER`, `CI_REGISTRY_PASSWORD`) + +#### 2. Deploy Stage +- **Values File Update**: + - Uses `yq` to update `open-meteo-service/values.yaml` + - Sets `image.tag` to the newly built tag (`-`) + - Keeps `image.repository` constant in the values file + - Commits the change with message: `ci: bump image tag to [skip ci]` + - Pushes back to the repository using `CI_JOB_TOKEN` for authentication +- **Helm Deployment**: + - Deploys to namespace: `sandbox-gitlab` + - Release name: `open-meteo-service-gitlab` + - Uses the updated values.yaml file from the workspace + - Waits for deployment to be ready (5-minute timeout) +- **Automated Tests**: + - Port-forwards the service to localhost:8000 + - `/healthz` - Checks service health + - `/coordinates` - Validates JSON response contains `"data"` field + - `/metrics` - Ensures metrics endpoint is accessible + - All tests must pass for the pipeline to succeed + +### Preventing Infinite Loops + +The pipeline includes safeguards to prevent infinite pipeline triggers: +- The deploy job includes a rule: `if: '$CI_COMMIT_MESSAGE =~ /^ci: bump image tag/'` with `when: never` +- The values bump commit includes `[skip ci]` to prevent triggering a new pipeline +- The deploy job uses the already-updated values.yaml in the current workspace + +### Deployment Target + +- **Cluster**: External k3s homelab cluster (not ephemeral) +- **Runner**: Self-hosted GitLab runner tagged with `homelab` +- **Namespace**: `sandbox-gitlab` (separate from ArgoCD/Harbor setups) +- **Ingress**: Configured for `open-meteo-gitlab.dvirlabs.com` (see values.yaml) + +### Viewing Pipeline Status + +1. Navigate to **CI/CD > Pipelines** in your GitLab project +2. Click on any pipeline run to see job details +3. Expand jobs to view real-time logs +4. The deploy job shows Helm output, rollout status, and test results + +### Manual Deployment + +To deploy manually with a specific tag: +```bash +# Option 1: Deploy using updated values.yaml +helm upgrade --install open-meteo-service-gitlab ./open-meteo-service \ + -n sandbox-gitlab --create-namespace + +# Option 2: Override tag via --set +helm upgrade --install open-meteo-service-gitlab ./open-meteo-service \ + -n sandbox-gitlab --create-namespace \ + --set image.tag=main-a1b2c3d +``` + ## API Documentation ### Endpoints diff --git a/open-meteo-service/values.yaml b/open-meteo-service/values.yaml index 27de93a..47626fa 100644 --- a/open-meteo-service/values.yaml +++ b/open-meteo-service/values.yaml @@ -1,22 +1,22 @@ replicaCount: 1 image: - repository: harbor.dvirlabs.com/my-apps/open-meteo-service - tag: "1.0.2" + repository: registry.gitlab.com/dvirlabs/open-meteo-service + tag: "latest" pullPolicy: IfNotPresent imagePullSecrets: - - name: harbor-regcred + - name: gitlab-registry service: type: ClusterIP port: 8000 ingress: - enabled: true - className: "traefik" + enabled: false + className: "" hosts: - - host: open-meteo.dvirlabs.com + - host: open-meteo-gitlab.dvirlabs.com paths: - path: / pathType: Prefix @@ -53,10 +53,10 @@ prometheus: type: ClusterIP port: 9090 ingress: - enabled: true - className: "traefik" + enabled: false + className: "" hosts: - - host: open-meteo-prometheus.dvirlabs.com + - host: prometheus.local paths: - path: / pathType: Prefix @@ -80,10 +80,10 @@ grafana: adminUser: admin adminPassword: admin ingress: - enabled: true - className: "traefik" + enabled: false + className: "" hosts: - - host: open-meteo-grafana.dvirlabs.com + - host: grafana.local paths: - path: / pathType: Prefix @@ -94,4 +94,4 @@ grafana: - ReadWriteOnce size: 5Gi storageClassName: "" - resources: {} \ No newline at end of file + resources: {} diff --git a/values.yaml b/values.yaml new file mode 100644 index 0000000..d259377 --- /dev/null +++ b/values.yaml @@ -0,0 +1,98 @@ +replicaCount: 1 + +image: + repository: registry.gitlab.com/dvirlabs-group/open-meteo-service + tag: "step3-110ba605" + pullPolicy: IfNotPresent + +imagePullSecrets: +- name: gitlab-registry + +service: + type: ClusterIP + port: 8000 + +ingress: + enabled: false + className: "traefik" + hosts: + - host: open-meteo-gitlab.dvirlabs.com + paths: + - path: / + pathType: Prefix + tls: [] + +podAnnotations: + prometheus.io/scrape: "true" + prometheus.io/path: /metrics + prometheus.io/port: "8000" + +env: + cacheFile: /data/coordinates_cache.json + +persistence: + enabled: false + accessModes: + - ReadWriteOnce + size: 1Gi + storageClassName: "nfs-client" + +resources: {} + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +prometheus: + enabled: true + replicaCount: 1 + image: prom/prometheus:latest + service: + type: ClusterIP + port: 9090 + ingress: + enabled: false + className: "traefik" + hosts: + - host: open-meteo-prometheus-gitlab.dvirlabs.com + paths: + - path: / + pathType: Prefix + tls: [] + persistence: + enabled: true + accessModes: + - ReadWriteOnce + size: 5Gi + storageClassName: "nfs-client" + resources: {} + extraArgs: [] + +grafana: + enabled: true + replicaCount: 1 + image: grafana/grafana:latest + service: + type: ClusterIP + port: 3000 + adminUser: admin + adminPassword: admin + ingress: + enabled: false + className: "traefik" + hosts: + - host: open-meteo-grafana-gitlab.dvirlabs.com + paths: + - path: / + pathType: Prefix + tls: [] + persistence: + enabled: true + accessModes: + - ReadWriteOnce + size: 5Gi + storageClassName: "nfs-client" + resources: {} +