diff --git a/app_python/README.md b/app_python/README.md index d6eb0fc12d..40190a648f 100644 --- a/app_python/README.md +++ b/app_python/README.md @@ -4,7 +4,7 @@ [![python-ci](https://github.com/GrayMansion/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg)](https://github.com/GrayMansion/DevOps-Core-Course/actions/workflows/python-ci.yml) -A small web service that exposes system/runtime information and a health check endpoint. +A small web service that exposes system/runtime information, metrics, and a persistent visits counter. ## Prerequisites - Python 3.11+ @@ -32,7 +32,9 @@ DEBUG=true python app.py ## API Endpoints ```bash GET / — Service and system information +GET /visits — Current visits counter value GET /health — Health check +GET /metrics — Prometheus metrics ``` ## Configuration @@ -41,6 +43,7 @@ GET /health — Health check | HOST | 0.0.0.0 | Bind address | | PORT | 5000 | Listening port | | DEBUG | False | If true, enables reload + debug logging | +| VISITS_FILE | ./data/visits | File path for persistent visits counter | ## Docker @@ -64,3 +67,22 @@ docker pull graymansion/devops-info-service:lab02 docker run --rm -p : graymansion/devops-info-service:lab02 ``` +## Local persistence test with Docker Compose + +Run from app_python directory: + +```bash +docker compose up --build -d +curl http://localhost:5000/ +curl http://localhost:5000/ +curl http://localhost:5000/visits +cat ./data/visits +docker compose restart +curl http://localhost:5000/visits +docker compose down +``` + +Expected result: +- Counter increments when / is called. +- The same counter value is kept after container restart because ./data is mounted to /app/data. + diff --git a/app_python/app.py b/app_python/app.py index 12463822c1..a6ee312c1f 100644 --- a/app_python/app.py +++ b/app_python/app.py @@ -9,6 +9,8 @@ import platform import socket import time +from pathlib import Path +from threading import Lock from datetime import datetime, timezone import uvicorn @@ -24,6 +26,7 @@ PORT = int(os.getenv("PORT", "5000")) DEBUG = os.getenv("DEBUG", "False").lower() == "true" LOG_FORMAT = os.getenv("LOG_FORMAT", "text") # "json" for structured logging +VISITS_FILE = os.getenv("VISITS_FILE", "./data/visits") # ------------------------- @@ -97,6 +100,35 @@ def format(self, record: logging.LogRecord) -> str: "System info collection duration in seconds", ) +VISITS_LOCK = Lock() + + +def read_visits() -> int: + visits_path = Path(VISITS_FILE) + try: + raw_value = visits_path.read_text(encoding="utf-8").strip() + return int(raw_value) if raw_value else 0 + except FileNotFoundError: + return 0 + except ValueError: + logger.warning("Invalid visits counter content in %s, resetting to 0", visits_path) + return 0 + + +def write_visits(value: int) -> int: + visits_path = Path(VISITS_FILE) + visits_path.parent.mkdir(parents=True, exist_ok=True) + tmp_path = visits_path.with_name(f"{visits_path.name}.tmp") + tmp_path.write_text(str(value), encoding="utf-8") + tmp_path.replace(visits_path) + return value + + +def increment_visits() -> int: + with VISITS_LOCK: + visits = read_visits() + 1 + return write_visits(visits) + def iso_utc_now() -> str: # Example desired format: 2026-01-07T14:30:00.000Z @@ -136,6 +168,14 @@ def get_system_info() -> dict: } +@app.on_event("startup") +async def initialize_visits_counter() -> None: + with VISITS_LOCK: + current_visits = read_visits() + write_visits(current_visits) + logger.info("Visits counter initialized at %d in %s", current_visits, VISITS_FILE) + + @app.exception_handler(StarletteHTTPException) async def http_exception_handler(_: Request, exc: StarletteHTTPException): if exc.status_code == 404: @@ -202,6 +242,7 @@ async def log_requests(request: Request, call_next): @app.get("/") async def index(request: Request) -> dict: ENDPOINT_CALLS.labels(endpoint="/").inc() + visits = increment_visits() uptime = get_uptime() with SYSTEM_INFO_DURATION_SECONDS.time(): system_info = get_system_info() @@ -226,14 +267,25 @@ async def index(request: Request) -> dict: "method": request.method, "path": request.url.path, }, + "stats": { + "visits": visits, + }, "endpoints": [ {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/visits", "method": "GET", "description": "Current visits count"}, {"path": "/health", "method": "GET", "description": "Health check"}, {"path": "/metrics", "method": "GET", "description": "Prometheus metrics"}, ], } +@app.get("/visits") +async def visits() -> dict: + ENDPOINT_CALLS.labels(endpoint="/visits").inc() + with VISITS_LOCK: + return {"visits": read_visits()} + + @app.get("/health") async def health() -> dict: ENDPOINT_CALLS.labels(endpoint="/health").inc() diff --git a/app_python/docker-compose.yml b/app_python/docker-compose.yml new file mode 100644 index 0000000000..abfc9d1dc2 --- /dev/null +++ b/app_python/docker-compose.yml @@ -0,0 +1,17 @@ +version: "3.9" + +services: + devops-info: + build: + context: . + dockerfile: Dockerfile + container_name: devops-info + ports: + - "5000:5000" + environment: + HOST: "0.0.0.0" + PORT: "5000" + DEBUG: "false" + VISITS_FILE: "/app/data/visits" + volumes: + - ./data:/app/data diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py index 75dd92f292..7d8d19f519 100644 --- a/app_python/tests/test_app.py +++ b/app_python/tests/test_app.py @@ -1,9 +1,41 @@ +import app as app_module +import pytest from fastapi.testclient import TestClient from app import app client = TestClient(app) + +@pytest.fixture(autouse=True) +def isolated_visits_file(tmp_path, request): + app_module.VISITS_FILE = str(tmp_path / f"{request.node.name}.visits") + + +def test_visits_counter_persists_in_file(tmp_path): + visits_file = tmp_path / "visits" + app_module.VISITS_FILE = str(visits_file) + + first = client.get("/") + second = client.get("/") + assert first.status_code == 200 + assert second.status_code == 200 + + assert first.json()["stats"]["visits"] == 1 + assert second.json()["stats"]["visits"] == 2 + assert visits_file.read_text(encoding="utf-8").strip() == "2" + + +def test_visits_endpoint_reads_current_count(tmp_path): + visits_file = tmp_path / "visits" + visits_file.write_text("7", encoding="utf-8") + app_module.VISITS_FILE = str(visits_file) + + response = client.get("/visits") + assert response.status_code == 200 + assert response.json() == {"visits": 7} + + def test_root_structure(): r = client.get("/", headers={"User-Agent": "pytest"}) assert r.status_code == 200 @@ -20,13 +52,15 @@ def test_root_structure(): assert isinstance(data["runtime"]["uptime_seconds"], int) assert data["runtime"]["timezone"] == "UTC" + assert isinstance(data["stats"]["visits"], int) assert data["request"]["method"] == "GET" assert data["request"]["path"] == "/" assert data["request"]["user_agent"] is not None paths = {e["path"] for e in data["endpoints"]} - assert "/" in paths and "/health" in paths + assert "/" in paths and "/health" in paths and "/visits" in paths + def test_health(): r = client.get("/health") @@ -36,6 +70,7 @@ def test_health(): assert isinstance(data["uptime_seconds"], int) assert "timestamp" in data + def test_404_json(): r = client.get("/nope") assert r.status_code == 404 @@ -46,6 +81,7 @@ def test_404_json(): def test_metrics_endpoint(): client.get("/") client.get("/health") + client.get("/visits") r = client.get("/metrics") assert r.status_code == 200 @@ -58,3 +94,4 @@ def test_metrics_endpoint(): assert "devops_info_endpoint_calls_total" in body assert 'endpoint="/"' in body assert 'endpoint="/health"' in body + assert 'endpoint="/visits"' in body diff --git a/k8s/LAB11.md b/k8s/LAB11.md new file mode 100644 index 0000000000..158fcc4d91 --- /dev/null +++ b/k8s/LAB11.md @@ -0,0 +1,346 @@ +# Lab 11 Report - Kubernetes Secrets and HashiCorp Vault + +## 1. Kubernetes Secrets + +### 1.1 Create Secret via kubectl + +```bash +kubectl create secret generic app-credentials \ + --from-literal=username=app-user \ + --from-literal=password='S3cr3t-P@ss' +``` + +Expected output: + +```text +secret/app-credentials created +``` + +### 1.2 View Secret YAML + +```bash +kubectl get secret app-credentials -o yaml +``` + +Example output (sanitized): + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: app-credentials +type: Opaque +data: + username: YXBwLXVzZXI= + password: UzNjcjN0LVBAccNz +``` + +### 1.3 Decode Secret Values + +```bash +echo 'YXBwLXVzZXI=' | base64 -d; echo +echo 'UzNjcjN0LVBAccNz' | base64 -d; echo +``` + +### 1.4 Encoding vs Encryption + +- Base64 is encoding, not encryption. It only transforms representation and is reversible without a key. +- Kubernetes Secret values in manifest/API responses are base64-encoded for transport format compatibility. +- Real protection requires encryption at rest, transport security, strict RBAC, and external secret systems for high-security workloads. + +### 1.5 Are Secrets encrypted at rest by default? + +- Not guaranteed by default in all clusters. +- In production, enable etcd encryption at rest using an EncryptionConfiguration on the API server. +- Also restrict access with RBAC and audit secret access. + +--- + +## 2. Helm Secret Integration + +Implemented in chart: k8s/devops-info + +### 2.1 Chart structure changes + +```text +k8s/devops-info/ + values.yaml + templates/ + _helpers.tpl + deployment.yaml + secrets.yaml + serviceaccount.yaml +``` + +### 2.2 Secret template + +Created: `templates/secrets.yaml` + +- Uses `apiVersion: v1`, `kind: Secret`, `type: Opaque` +- Uses `stringData` so values from `values.yaml` are auto-encoded by Kubernetes +- Secret name is templated using helper `devops-info.secretName` + +### 2.3 Values for secret and vault + +Added to `values.yaml`: + +- `secret.enabled`, `secret.name`, `secret.data.username`, `secret.data.password` +- `serviceAccount.create`, `serviceAccount.name` +- `vault.enabled`, `vault.role`, `vault.secretPath`, `vault.fileName`, `vault.template`, `vault.command` + +### 2.4 Secret consumption in Deployment + +`deployment.yaml` now consumes secret values with `envFrom.secretRef`: + +```yaml +envFrom: + - secretRef: + name: {{ include "devops-info.secretName" . }} +``` + +This exposes each key from the Secret as environment variables inside the container. + +### 2.5 Verify secret injection + +```bash +# Deploy/upgrade chart +helm upgrade --install devops-info k8s/devops-info + +# Check secret exists +kubectl get secret + +# Check pod env variable names (do not print actual values in report) +POD=$(kubectl get pods -l app.kubernetes.io/name=devops-info -o jsonpath='{.items[0].metadata.name}') +kubectl exec "$POD" -- sh -c 'env | grep -E "^(username|password)=" | cut -d= -f1' +``` + +Expected output should show only variable names: + +```text +username +password +``` + +Security check: + +```bash +kubectl describe pod "$POD" +``` + +- Secret values are not printed in clear text in pod describe output. +- Secret references can appear, but not actual values. + +--- + +## 3. Resource Management + +### 3.1 Configured resources + +In `values.yaml`: + +```yaml +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi +``` + +### 3.2 Requests vs Limits + +- Requests: scheduler guarantee used for bin-packing and QoS decisions. +- Limits: hard upper bound enforced by kernel cgroups. +- If memory usage exceeds limit, container can be OOM-killed. +- CPU over limit is throttled. + +### 3.3 How to choose values + +- Start from observed baseline (p50/p95 usage under normal and peak load). +- Set requests near steady-state plus safety margin. +- Set limits to protect node stability while allowing realistic burst. +- Revisit values after load tests and production telemetry. + +--- + +## 4. Vault Integration + +### 4.1 Install Vault with Helm (dev mode) + +```bash +helm repo add hashicorp https://helm.releases.hashicorp.com +helm repo update + +helm upgrade --install vault hashicorp/vault \ + --set 'server.dev.enabled=true' \ + --set 'injector.enabled=true' +``` + +Verification: + +```bash +kubectl get pods -l app.kubernetes.io/name=vault +``` + +Expected: vault server and injector pods in Running state. + +### 4.2 Configure Vault KV and secret + +```bash +kubectl exec -it vault-0 -- sh + +vault secrets enable -path=secret kv-v2 +vault kv put secret/myapp/config username='vault-user' password='vault-pass' +``` + +### 4.3 Configure Kubernetes auth, policy, and role + +Policy (`devops-info-policy.hcl`): + +```hcl +path "secret/data/myapp/config" { + capabilities = ["read"] +} +``` + +Apply policy and role: + +```bash +vault auth enable kubernetes + +vault policy write devops-info-policy devops-info-policy.hcl + +vault write auth/kubernetes/role/devops-info-role \ + bound_service_account_names=devops-info \ + bound_service_account_namespaces=default \ + policies=devops-info-policy \ + ttl=1h +``` + +### 4.4 Vault Agent Injector in chart + +Implemented via helper template and deployment annotations when `vault.enabled=true`. + +Current chart uses: + +- `vault.hashicorp.com/agent-inject: "true"` +- `vault.hashicorp.com/role: "devops-info-role"` +- `vault.hashicorp.com/agent-inject-secret-config: "secret/data/myapp/config"` +- `vault.hashicorp.com/agent-inject-template-config: | ...` + +### 4.5 Verify file-based injection + +```bash +helm upgrade --install devops-info k8s/devops-info \ + --set vault.enabled=true + +POD=$(kubectl get pods -l app.kubernetes.io/name=devops-info -o jsonpath='{.items[0].metadata.name}') + +kubectl exec "$POD" -- ls -la /vault/secrets +kubectl exec "$POD" -- cat /vault/secrets/config +``` + +Expected: + +- `/vault/secrets/config` exists. +- File contains rendered key-value content from Vault template. + +### 4.6 Sidecar injection pattern explanation + +- Mutating webhook adds Vault Agent container and shared volume to pod. +- Agent authenticates using pod service account JWT through Vault Kubernetes auth method. +- Agent retrieves secret, renders template, writes file in shared volume. +- App container reads file without embedding static credentials in image or chart values. + +--- + +## 5. Security Analysis + +### 5.1 Kubernetes Secrets vs Vault + +- Kubernetes Secret: + - Native and simple. + - Good for low-complexity internal workloads. + - Requires strict RBAC and etcd encryption for stronger security. + +- HashiCorp Vault: + - Centralized secret lifecycle and policy model. + - Supports dynamic secrets, lease/TTL, revocation, rotation workflows. + - Better fit for production-grade multi-service or compliance-heavy environments. + +### 5.2 When to use which + +- Use Kubernetes Secrets for small projects, bootstrap config, or low-risk non-critical credentials. +- Use Vault for production environments requiring auditability, rotation, short-lived credentials, and centralized governance. + +### 5.3 Production recommendations + +- Enable etcd encryption at rest. +- Apply least-privilege RBAC for Secret reads. +- Avoid committing real secrets to Git. +- Prefer external secret manager (Vault or cloud managed equivalent). +- Rotate credentials regularly and monitor access logs. + +--- + +## Bonus - Vault Agent Templates + +### B1. Template annotation implementation + +Implemented in chart using: + +```yaml +vault.hashicorp.com/agent-inject-template-config: | + {{- with secret "secret/data/myapp/config" -}} + APP_USERNAME={{ .Data.data.username }} + APP_PASSWORD={{ .Data.data.password }} + {{- end -}} +``` + +This renders multiple values into one file (`/vault/secrets/config`) in env-style format. + +### B2. Dynamic refresh mechanism + +- Vault Agent renews token/leases where applicable and re-renders templates on data updates. +- File update events can trigger in-container reload logic. +- `vault.hashicorp.com/agent-inject-command` can run a command after template re-render (for example: send SIGHUP to app). + +### B3. Named template for environment variables (DRY) + +Implemented helper in `_helpers.tpl`: + +```yaml +{{- define "devops-info.envVars" -}} +{{- range .Values.env }} +- name: {{ .name }} + value: {{ .value | quote }} +{{- end }} +{{- end -}} +``` + +Deployment uses: + +```yaml +env: + {{- include "devops-info.envVars" . | nindent 12 }} +``` + +Benefit: one reusable source of env rendering logic, easier maintenance, less duplication. + +--- + +## Final Checklist Mapping + +- [x] Kubernetes Secret created/viewed/decoded commands documented. +- [x] Base64 vs encryption explained. +- [x] Helm secret template added and wired to deployment. +- [x] Resource requests/limits documented and present. +- [x] Vault install/config/auth/role/policy steps documented. +- [x] Vault injection and sidecar pattern documented. +- [x] Bonus template annotation and named template documented. + +## Notes + +- Real cluster command output can differ by namespace, release name, and chart version. +- Keep placeholders in Git and pass real values via `--set`, `-f`, CI secrets, or Vault only. diff --git a/k8s/LAB12.md b/k8s/LAB12.md new file mode 100644 index 0000000000..76b3afcb32 --- /dev/null +++ b/k8s/LAB12.md @@ -0,0 +1,206 @@ +# Lab 12 Report - ConfigMaps and Persistent Volumes + +## 1. Application Changes + +### 1.1 Visits counter implementation + +Implemented in app code: +- app_python/app.py + +Behavior: +- Counter is stored in a file path from VISITS_FILE env var. +- Default path is ./data/visits for local runs. +- For Kubernetes, VISITS_FILE is set to /data/visits. +- Root endpoint / increments the counter and returns the current value in stats.visits. +- New endpoint /visits returns current counter value. + +Implementation notes: +- File operations are guarded with a process-level lock to reduce concurrent write races. +- Atomic file update pattern is used: write to temporary file, then replace target file. +- Startup initializes visits file if it does not exist. + +### 1.2 New endpoint documentation + +- GET /visits -> returns: + +```json +{"visits": 2} +``` + +### 1.3 Local testing evidence + +Automated tests: + +```bash +cd app_python +./venv/bin/pytest -q +``` + +Output: + +```text +...... [100%] +6 passed, 2 warnings in 0.55s +``` + +Runtime evidence (executed with TestClient): + +```text +root-call-1-visits=1 +root-call-2-visits=2 +visits-endpoint=2 +file-content=2 +``` + +### 1.4 Docker Compose for local persistence + +Added file: +- app_python/docker-compose.yml + +Volume mapping: + +```yaml +volumes: + - ./data:/app/data +``` + +Env override: + +```yaml +environment: + VISITS_FILE: "/app/data/visits" +``` + +--- + +## 2. ConfigMap Implementation + +### 2.1 File-based ConfigMap + +Added chart file: +- k8s/devops-info/files/config.json + +Added template: +- k8s/devops-info/templates/configmap-file.yaml + +Template uses Helm file loading: + +```yaml +data: + config.json: |- +{{ tpl (.Files.Get "files/config.json") . | indent 4 }} +``` + +This mounts JSON config as a file in the pod. + +### 2.2 Environment variable ConfigMap + +Added template: +- k8s/devops-info/templates/configmap-env.yaml + +Data comes from values: +- .Values.configEnv.APP_ENV +- .Values.configEnv.LOG_LEVEL +- .Values.configEnv.FEATURE_VISITS + +### 2.3 Deployment wiring + +Updated: +- k8s/devops-info/templates/deployment.yaml + +Changes: +- Config file mounted as volume at /config. +- Env vars injected via envFrom -> configMapRef. +- Rollout checksums added: + - checksum/config-file + - checksum/config-env + +Verification commands to run in cluster: + +```bash +kubectl get configmap +kubectl exec -- cat /config/config.json +kubectl exec -- printenv | grep -E 'APP_ENV|LOG_LEVEL|FEATURE_VISITS' +``` + +--- + +## 3. Persistent Volume Implementation + +### 3.1 PVC template + +Added: +- k8s/devops-info/templates/pvc.yaml + +PVC spec: +- accessModes: ReadWriteOnce +- storage request: .Values.persistence.size (default 100Mi) +- storageClassName: optional from .Values.persistence.storageClass + +### 3.2 Deployment volume mount + +Updated deployment to mount PVC: +- volume name: data-volume +- claimName: -data +- mountPath: /data (from values) + +Application path alignment: +- VISITS_FILE=/data/visits + +### 3.3 Persistence test procedure + +Run in cluster: + +```bash +kubectl get pvc +kubectl get pods -l app.kubernetes.io/name=devops-info + +# hit / several times +kubectl exec -- cat /data/visits + +kubectl delete pod +kubectl get pods -l app.kubernetes.io/name=devops-info -w + +# after new pod is running +kubectl exec -- cat /data/visits +kubectl exec -- curl -s localhost:5000/visits +``` + +Expected result: +- Counter value is the same before and after pod deletion. + +--- + +## 4. ConfigMap vs Secret + +Use ConfigMap when: +- Data is non-sensitive configuration. +- Plain application settings are needed (feature flags, log level, environment name). + +Use Secret when: +- Data is sensitive (passwords, tokens, API keys, certificates). +- You need stricter access control and secret management integration. + +Key differences: +- ConfigMap stores plain config data. +- Secret stores sensitive data (base64 encoded in manifests and should be protected with RBAC and encryption at rest). +- Operationally both can be consumed as files or environment variables. + +--- + +## 6. Changed Files Summary + +Application: +- app_python/app.py +- app_python/tests/test_app.py +- app_python/README.md +- app_python/docker-compose.yml + +Helm chart: +- k8s/devops-info/values.yaml +- k8s/devops-info/files/config.json +- k8s/devops-info/templates/_helpers.tpl +- k8s/devops-info/templates/configmap-file.yaml +- k8s/devops-info/templates/configmap-env.yaml +- k8s/devops-info/templates/pvc.yaml +- k8s/devops-info/templates/deployment.yaml diff --git a/k8s/devops-info/files/config.json b/k8s/devops-info/files/config.json new file mode 100644 index 0000000000..2193976746 --- /dev/null +++ b/k8s/devops-info/files/config.json @@ -0,0 +1,10 @@ +{ + "application": { + "name": "{{ .Values.appConfig.appName }}", + "environment": "{{ .Values.appConfig.environment }}" + }, + "features": { + "enableVisitsCounter": {{ .Values.appConfig.features.enableVisitsCounter }}, + "includeSystemInfo": {{ .Values.appConfig.features.includeSystemInfo }} + } +} diff --git a/k8s/devops-info/templates/_helpers.tpl b/k8s/devops-info/templates/_helpers.tpl index f72f561727..2b56667681 100644 --- a/k8s/devops-info/templates/_helpers.tpl +++ b/k8s/devops-info/templates/_helpers.tpl @@ -13,3 +13,53 @@ {{- define "devops-info.selectorLabels" -}} {{- include "common.selectorLabels" . -}} {{- end -}} + +{{- define "devops-info.secretName" -}} +{{- if .Values.secret.name -}} +{{- .Values.secret.name -}} +{{- else -}} +{{- printf "%s-secret" (include "devops-info.fullname" .) -}} +{{- end -}} +{{- end -}} + +{{- define "devops-info.fileConfigMapName" -}} +{{- printf "%s-config" (include "devops-info.fullname" .) -}} +{{- end -}} + +{{- define "devops-info.envConfigMapName" -}} +{{- printf "%s-env" (include "devops-info.fullname" .) -}} +{{- end -}} + +{{- define "devops-info.pvcName" -}} +{{- printf "%s-data" (include "devops-info.fullname" .) -}} +{{- end -}} + +{{- define "devops-info.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{- default (include "devops-info.fullname" .) .Values.serviceAccount.name -}} +{{- else -}} +{{- default "default" .Values.serviceAccount.name -}} +{{- end -}} +{{- end -}} + +{{- define "devops-info.envVars" -}} +{{- range .Values.env }} +- name: {{ .name }} + value: {{ .value | quote }} +{{- end }} +{{- end -}} + +{{- define "devops-info.vaultAnnotations" -}} +{{- if .Values.vault.enabled }} +vault.hashicorp.com/agent-inject: "true" +vault.hashicorp.com/role: {{ .Values.vault.role | quote }} +{{ printf "vault.hashicorp.com/agent-inject-secret-%s" .Values.vault.fileName }}: {{ .Values.vault.secretPath | quote }} +{{- if .Values.vault.template }} +{{ printf "vault.hashicorp.com/agent-inject-template-%s" .Values.vault.fileName }}: | +{{ tpl .Values.vault.template . | nindent 2 }} +{{- end }} +{{- if .Values.vault.command }} +vault.hashicorp.com/agent-inject-command: {{ .Values.vault.command | quote }} +{{- end }} +{{- end }} +{{- end -}} diff --git a/k8s/devops-info/templates/configmap-env.yaml b/k8s/devops-info/templates/configmap-env.yaml new file mode 100644 index 0000000000..2ee9d83d4e --- /dev/null +++ b/k8s/devops-info/templates/configmap-env.yaml @@ -0,0 +1,12 @@ +{{- if .Values.config.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "devops-info.envConfigMapName" . }} + labels: + {{- include "devops-info.labels" . | nindent 4 }} +data: + {{- range $key, $value := .Values.configEnv }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} diff --git a/k8s/devops-info/templates/configmap-file.yaml b/k8s/devops-info/templates/configmap-file.yaml new file mode 100644 index 0000000000..8acf24f1f2 --- /dev/null +++ b/k8s/devops-info/templates/configmap-file.yaml @@ -0,0 +1,11 @@ +{{- if .Values.config.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "devops-info.fileConfigMapName" . }} + labels: + {{- include "devops-info.labels" . | nindent 4 }} +data: + config.json: |- +{{ tpl (.Files.Get "files/config.json") . | indent 4 }} +{{- end }} diff --git a/k8s/devops-info/templates/deployment.yaml b/k8s/devops-info/templates/deployment.yaml index 1d3088f82e..1fe39ed31b 100644 --- a/k8s/devops-info/templates/deployment.yaml +++ b/k8s/devops-info/templates/deployment.yaml @@ -17,10 +17,21 @@ spec: {{- include "devops-info.selectorLabels" . | nindent 6 }} template: metadata: + annotations: + {{- if .Values.config.enabled }} + checksum/config-file: {{ include (print $.Template.BasePath "/configmap-file.yaml") . | sha256sum }} + checksum/config-env: {{ include (print $.Template.BasePath "/configmap-env.yaml") . | sha256sum }} + {{- end }} + {{- if .Values.vault.enabled }} + {{- include "devops-info.vaultAnnotations" . | nindent 8 }} + {{- end }} labels: {{- include "devops-info.selectorLabels" . | nindent 8 }} component: api spec: + serviceAccountName: {{ include "devops-info.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: {{ include "devops-info.name" . }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" @@ -30,9 +41,29 @@ spec: containerPort: {{ .Values.containerPort }} protocol: TCP env: - {{- range .Values.env }} - - name: {{ .name }} - value: {{ .value | quote }} + {{- include "devops-info.envVars" . | nindent 12 }} + {{- if .Values.secret.enabled }} + envFrom: + - secretRef: + name: {{ include "devops-info.secretName" . }} + {{- if .Values.config.enabled }} + - configMapRef: + name: {{ include "devops-info.envConfigMapName" . }} + {{- end }} + {{- else if .Values.config.enabled }} + envFrom: + - configMapRef: + name: {{ include "devops-info.envConfigMapName" . }} + {{- end }} + volumeMounts: + {{- if .Values.config.enabled }} + - name: config-volume + mountPath: {{ .Values.config.mountPath }} + readOnly: true + {{- end }} + {{- if .Values.persistence.enabled }} + - name: data-volume + mountPath: {{ .Values.persistence.mountPath }} {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }} @@ -43,4 +74,15 @@ spec: startupProbe: {{- toYaml .Values.probes.startup | nindent 12 }} securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} \ No newline at end of file + {{- toYaml .Values.securityContext | nindent 12 }} + volumes: + {{- if .Values.config.enabled }} + - name: config-volume + configMap: + name: {{ include "devops-info.fileConfigMapName" . }} + {{- end }} + {{- if .Values.persistence.enabled }} + - name: data-volume + persistentVolumeClaim: + claimName: {{ include "devops-info.pvcName" . }} + {{- end }} \ No newline at end of file diff --git a/k8s/devops-info/templates/pvc.yaml b/k8s/devops-info/templates/pvc.yaml new file mode 100644 index 0000000000..56cfe35f50 --- /dev/null +++ b/k8s/devops-info/templates/pvc.yaml @@ -0,0 +1,17 @@ +{{- if .Values.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "devops-info.pvcName" . }} + labels: + {{- include "devops-info.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.persistence.size }} + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass }} + {{- end }} +{{- end }} diff --git a/k8s/devops-info/templates/secrets.yaml b/k8s/devops-info/templates/secrets.yaml new file mode 100644 index 0000000000..898c7b8a13 --- /dev/null +++ b/k8s/devops-info/templates/secrets.yaml @@ -0,0 +1,13 @@ +{{- if .Values.secret.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "devops-info.secretName" . }} + labels: + {{- include "devops-info.labels" . | nindent 4 }} +type: Opaque +stringData: + {{- range $key, $value := .Values.secret.data }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} diff --git a/k8s/devops-info/templates/serviceaccount.yaml b/k8s/devops-info/templates/serviceaccount.yaml new file mode 100644 index 0000000000..9e91578eba --- /dev/null +++ b/k8s/devops-info/templates/serviceaccount.yaml @@ -0,0 +1,8 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "devops-info.serviceAccountName" . }} + labels: + {{- include "devops-info.labels" . | nindent 4 }} +{{- end }} diff --git a/k8s/devops-info/values.yaml b/k8s/devops-info/values.yaml index 2fbfd886c9..b32aa96678 100644 --- a/k8s/devops-info/values.yaml +++ b/k8s/devops-info/values.yaml @@ -23,6 +23,53 @@ env: value: "5000" - name: DEBUG value: "false" + - name: VISITS_FILE + value: "/data/visits" + +config: + enabled: true + mountPath: "/config" + +appConfig: + appName: "devops-info-service" + environment: "dev" + features: + enableVisitsCounter: true + includeSystemInfo: true + +configEnv: + APP_ENV: "dev" + LOG_LEVEL: "info" + FEATURE_VISITS: "true" + +persistence: + enabled: true + mountPath: "/data" + size: 100Mi + storageClass: "" + +serviceAccount: + create: true + name: "" + +secret: + enabled: true + name: "" + data: + username: "change-me-user" + password: "change-me-password" + +vault: + enabled: false + role: "devops-info-role" + secretPath: "secret/data/myapp/config" + fileName: "config" + command: "" + template: | + {{- with secret "secret/data/myapp/config" -}} + APP_USERNAME={{ .Data.data.username }} + APP_PASSWORD={{ .Data.data.password }} + {{- end -}} resources: requests: @@ -59,10 +106,14 @@ probes: securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: false + runAsNonRoot: true capabilities: drop: - ALL +podSecurityContext: + fsGroup: 1000 + hooks: enabled: true image: