diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 05bb66d..42ae2c9 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -12,7 +12,7 @@ permissions: jobs: docker_build_test: runs-on: ubuntu-24.04 - name: Docker_build + name: Docker_build and Helm_build steps: - uses: actions/checkout@v4 @@ -25,4 +25,49 @@ jobs: run: | echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin docker push ghcr.io/gabrielpalmar/hivebox:$(cat version.txt) - docker push ghcr.io/gabrielpalmar/hivebox:latest \ No newline at end of file + docker push ghcr.io/gabrielpalmar/hivebox:latest + + # Get the SHA256 digest + DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/gabrielpalmar/hivebox:$(cat version.txt) | cut -d'@' -f2) + if [ -z "$DIGEST" ]; then + echo "Failed to capture Docker digest" + exit 1 + fi + echo "Image digest: $DIGEST" + echo "DOCKER_DIGEST=$DIGEST" >> $GITHUB_ENV + + - name: Install Helm + run: | + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 + chmod 700 get_helm.sh + ./get_helm.sh + if ! command -v helm version &> /dev/null; then + echo "Helm could not be installed" + exit 1 + fi + + - name: Build Helm Chart + run: | + VERSION=$(cat version.txt) + sed -i "s/^version:.*/version: $VERSION/" ./helm-chart/Chart.yaml + sed -i "s/^appVersion:.*/appVersion: \"$VERSION\"/" ./helm-chart/Chart.yaml + sed -i "s|hivebox: ghcr.io/gabrielpalmar/hivebox:.*|hivebox: ghcr.io/gabrielpalmar/hivebox:$VERSION@$DOCKER_DIGEST|" ./helm-chart/values.yaml + helm package ./helm-chart + + - name: Push Helm Chart to GHCR + run: | + CHART_FILE="hivebox-$(cat version.txt).tgz" + if [ ! -f "$CHART_FILE" ]; then + echo "Helm chart $CHART_FILE not found" + exit 1 + fi + echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin + helm push $CHART_FILE oci://ghcr.io/gabrielpalmar/hivebox-helm-charts + + - name: Add job summary + run: | + VERSION=$(cat version.txt) + echo "## Build Summary" >> $GITHUB_STEP_SUMMARY + echo "- Docker image: ghcr.io/gabrielpalmar/hivebox:$VERSION" >> $GITHUB_STEP_SUMMARY + echo "- Image digest: $DOCKER_DIGEST" >> $GITHUB_STEP_SUMMARY + echo "- Helm chart: hivebox-$VERSION" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/app/opensense.py b/app/opensense.py index f88c2a9..d034ca1 100644 --- a/app/opensense.py +++ b/app/opensense.py @@ -51,7 +51,7 @@ def get_temperature(): print('Getting data from OpenSenseMap API...') - response = requests.get("https://api.opensensemap.org/boxes", params=params, timeout=300) + response = requests.get("https://api.opensensemap.org/boxes", params=params, timeout=480) print('Data retrieved successfully!') _sensor_stats["total_sensors"] = sum( diff --git a/app/readiness.py b/app/readiness.py index a21807c..d638608 100644 --- a/app/readiness.py +++ b/app/readiness.py @@ -1,4 +1,5 @@ '''Module to check the readiness of the stored information''' +import json import requests import redis from app.opensense import get_temperature @@ -41,16 +42,10 @@ def reachable_boxes(): return 400 return 200 - except requests.exceptions.RequestException as e: - # Handle network-related errors from the API call - print(f"Network error checking reachable boxes: {e}") - return 200 - except redis.RedisError as e: - # Handle Redis-related errors - print(f"Redis error checking reachable boxes: {e}") + except (json.JSONDecodeError, requests.exceptions.RequestException, redis.RedisError) as e: + print(f"Error checking reachable boxes: {e}") return 200 except (ValueError, TypeError, KeyError) as e: - # Handle data parsing errors print(f"Data error checking reachable boxes: {e}") return 400 diff --git a/app/storage.py b/app/storage.py index db8f5aa..d93d60f 100644 --- a/app/storage.py +++ b/app/storage.py @@ -54,8 +54,8 @@ def store_temperature_data(): content_type='text/plain' ) - return (f"Temperature data successfully uploaded as " - f"{destination_file} to bucket {bucket_name}") + return (f'Temperature data successfully uploaded as ' + f'{destination_file} to bucket {bucket_name}\n') except (S3Error, InvalidResponseError) as exc: error_msg = f"MinIO S3 error occurred: {exc}" diff --git a/helm-chart/Chart.yaml b/helm-chart/Chart.yaml new file mode 100644 index 0000000..16c32d4 --- /dev/null +++ b/helm-chart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: hivebox +description: A Helm chart for HiveBox application +type: application +version: 0.1.0 +appVersion: "0.7.0" diff --git a/helm-chart/NOTES.txt b/helm-chart/NOTES.txt new file mode 100644 index 0000000..9bcfc68 --- /dev/null +++ b/helm-chart/NOTES.txt @@ -0,0 +1,13 @@ +# filepath: helm-chart/NOTES.txt +1. Get the application URL by running these commands: +{{- if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services hivebox-service) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "ClusterIP" .Values.service.type }} + kubectl port-forward --namespace {{ .Release.Namespace }} svc/hivebox-service 8080:80 + echo "Visit http://127.0.0.1:8080 to use your application" +{{- end }} + +2. Check the application status: + kubectl get pods --namespace {{ .Release.Namespace }} -l "app=hivebox" \ No newline at end of file diff --git a/helm-chart/templates/_helpers.tpl b/helm-chart/templates/_helpers.tpl new file mode 100644 index 0000000..2a8d0a5 --- /dev/null +++ b/helm-chart/templates/_helpers.tpl @@ -0,0 +1,22 @@ +{{/* Pod-level securityContext */}} +{{- define "common.podSecurityContext" -}} +{{- with .Values.podSecurityContext }} +{{ toYaml . }} +{{- end }} +{{- end }} + +{{/* Container-level securityContext */}} +{{- define "common.containerSecurityContext" -}} +{{- with .Values.containerSecurityContext }} +{{ toYaml . }} +{{- end }} +{{- end }} + +{{/* Resources per workload (hivebox, redis, minio, cronjob) */}} +{{- define "common.resources" -}} +{{- $vals := .Values -}} +{{- $name := .name -}} +{{- with (index $vals.resources $name) }} +{{ toYaml . }} +{{- end }} +{{- end }} diff --git a/helm-chart/templates/cronjob.yaml b/helm-chart/templates/cronjob.yaml new file mode 100644 index 0000000..b54f5e5 --- /dev/null +++ b/helm-chart/templates/cronjob.yaml @@ -0,0 +1,49 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: temperature-storage-cronjob + labels: + app: hivebox-cronjob +spec: + schedule: "*/5 * * * *" + concurrencyPolicy: Forbid + jobTemplate: + spec: + template: + spec: + restartPolicy: OnFailure + securityContext: + {{- include "common.podSecurityContext" . | nindent 12 }} + initContainers: + - name: wait-for-start + image: {{ .Values.images.cronjob }} + command: ["/bin/sh", "-c"] + args: + - | + set -eu + while true; do + if curl -sSf -m 3 http://hivebox-service/version >/dev/null; then + echo "Hivebox service is up!" + exit 0 + else + echo "Waiting for Hivebox service to be available..." + sleep 5 + fi + done + containers: + - name: temperature-storage + image: {{ .Values.images.cronjob }} + command: ["curl"] + args: + - "-f" + - "-s" + - "-S" + - "--max-time" + - "60" + - "http://hivebox-service/store" + securityContext: + {{- include "common.containerSecurityContext" . | nindent 16 }} + resources: + {{- include "common.resources" (dict "Values" .Values "name" "cronjob") | nindent 16 }} + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 diff --git a/helm-chart/templates/deployment.yaml b/helm-chart/templates/deployment.yaml new file mode 100644 index 0000000..22c9c78 --- /dev/null +++ b/helm-chart/templates/deployment.yaml @@ -0,0 +1,138 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hivebox + labels: + app: hivebox +spec: + replicas: {{ .Values.replicas.hivebox }} + selector: + matchLabels: + app: hivebox + template: + metadata: + labels: + app: hivebox + spec: + securityContext: + {{- include "common.podSecurityContext" . | nindent 8 }} + containers: + - name: hivebox + image: {{ .Values.images.hivebox }} + ports: + - containerPort: 5000 + env: + - name: REDIS_HOST + value: {{ .Values.services.redis | quote }} + - name: MINIO_HOST + value: {{ .Values.services.minio | quote }} + securityContext: + {{- include "common.containerSecurityContext" . | nindent 12 }} + resources: + {{- include "common.resources" (dict "Values" .Values "name" "hivebox") | nindent 12 }} + startupProbe: + httpGet: + path: /readyz + port: 5000 + periodSeconds: 10 + failureThreshold: 1 + timeoutSeconds: 480 + initialDelaySeconds: 5 + readinessProbe: + httpGet: + path: /readyz + port: 5000 + periodSeconds: 60 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /version + port: 5000 + periodSeconds: 60 + timeoutSeconds: 3 + failureThreshold: 3 + volumeMounts: + - name: tmp-volume + mountPath: /tmp + volumes: + - name: tmp-volume + emptyDir: {} + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + labels: + app: redis +spec: + replicas: {{ .Values.replicas.redis }} + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + securityContext: + {{- include "common.podSecurityContext" . | nindent 10 }} + containers: + - name: valkey + image: {{ .Values.images.redis }} + ports: + - containerPort: 6379 + command: ["valkey-server"] + args: ["--save", "", "--appendonly", "no"] + securityContext: + {{- include "common.containerSecurityContext" . | nindent 12 }} + resources: + {{- include "common.resources" (dict "Values" .Values "name" "redis") | nindent 12 }} + volumeMounts: + - name: valkey-data + mountPath: /data + volumes: + - name: valkey-data + emptyDir: {} + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: minio + labels: + app: minio +spec: + replicas: {{ .Values.replicas.minio }} + selector: + matchLabels: + app: minio + template: + metadata: + labels: + app: minio + spec: + securityContext: + {{- include "common.podSecurityContext" . | nindent 10 }} + containers: + - name: minio + image: {{ .Values.images.minio }} + ports: + - containerPort: 9000 + command: ["minio", "server", "/data"] + env: + - name: MINIO_ROOT_USER + value: {{ .Values.minio.accessKey | quote }} + - name: MINIO_ROOT_PASSWORD + value: {{ .Values.minio.secretKey | quote }} + securityContext: + {{- include "common.containerSecurityContext" . | nindent 12 }} + resources: + {{- include "common.resources" (dict "Values" .Values "name" "minio") | nindent 12 }} + volumeMounts: + - name: minio-data + mountPath: /data + volumes: + - name: minio-data + emptyDir: {} diff --git a/helm-chart/templates/ingress.yaml b/helm-chart/templates/ingress.yaml new file mode 100644 index 0000000..7978576 --- /dev/null +++ b/helm-chart/templates/ingress.yaml @@ -0,0 +1,21 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: hivebox-ingress + annotations: + {{- if .Values.ingress.annotations }} + {{- toYaml .Values.ingress.annotations | nindent 4 }} + {{- end }} +spec: + ingressClassName: {{ .Values.ingress.className | default "nginx" }} + rules: + - host: {{ .Values.ingress.host | quote }} + http: + paths: + - path: {{ .Values.ingress.path | default "/" }} + pathType: Prefix + backend: + service: + name: {{ .Values.ingress.serviceName | default "hivebox-service" }} + port: + number: {{ .Values.ingress.servicePort | default 80 }} \ No newline at end of file diff --git a/helm-chart/templates/service.yaml b/helm-chart/templates/service.yaml new file mode 100644 index 0000000..c17f307 --- /dev/null +++ b/helm-chart/templates/service.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: Service +metadata: + name: hivebox-service + labels: + app: hivebox +spec: + selector: + app: hivebox + ports: + - port: 80 + targetPort: 5000 + protocol: TCP + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: redis-service +spec: + selector: + app: redis + ports: + - port: 6379 + targetPort: 6379 +--- +apiVersion: v1 +kind: Service +metadata: + name: minio-service +spec: + selector: + app: minio + ports: + - port: 9000 + targetPort: 9000 \ No newline at end of file diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml new file mode 100644 index 0000000..4f15594 --- /dev/null +++ b/helm-chart/values.yaml @@ -0,0 +1,64 @@ +# Global, reused everywhere +podSecurityContext: + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + +containerSecurityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsGroup: 1000 + runAsUser: 1000 + capabilities: + drop: ["ALL"] + +images: + hivebox: ghcr.io/gabrielpalmar/hivebox:latest@sha256:c731999c3fd9b757e2fd816e3c9dcf645dba56647d8a921cb567ece3cf378dc3 + redis: valkey/valkey:8-alpine3.22@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4 + minio: minio/minio:RELEASE.2025-07-23T15-54-02Z@sha256:d249d1fb6966de4d8ad26c04754b545205ff15a62e4fd19ebd0f26fa5baacbc0 + cronjob: curlimages/curl:8.15.0@sha256:4026b29997dc7c823b51c164b71e2b51e0fd95cce4601f78202c513d97da2922 + +replicas: + hivebox: 2 + redis: 1 + minio: 1 + +resources: + hivebox: + limits: { memory: "512Mi", cpu: "500m" } + requests: { memory: "256Mi", cpu: "250m" } + redis: + limits: { memory: "256Mi", cpu: "250m" } + requests: { memory: "128Mi", cpu: "100m" } + minio: + limits: { memory: "256Mi", cpu: "250m" } + requests: { memory: "128Mi", cpu: "100m" } + cronjob: + limits: { memory: "32Mi", cpu: "50m" } + requests: { memory: "16Mi", cpu: "10m" } + +services: + redis: redis-service + minio: minio-service + +service: + type: ClusterIP + port: 80 + targetPort: 5000 + +minio: + accessKey: minioadmin + secretKey: minioadmin + +ingress: + enabled: true + host: hivebox.local + path: / + className: nginx + serviceName: hivebox-service + servicePort: 80 + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/ssl-redirect: "false" + nginx.ingress.kubernetes.io/proxy-body-size: "10m" diff --git a/k8s/cronjob.yml b/k8s/cronjob.yml deleted file mode 100644 index 51e4b3c..0000000 --- a/k8s/cronjob.yml +++ /dev/null @@ -1,44 +0,0 @@ -apiVersion: batch/v1 -kind: CronJob -metadata: - name: temperature-storage-cronjob - labels: - app: hivebox-cronjob -spec: - schedule: "*/5 * * * *" - jobTemplate: - spec: - template: - spec: - restartPolicy: OnFailure - securityContext: - runAsNonRoot: true - runAsUser: 65534 # nobody user - seccompProfile: - type: RuntimeDefault - containers: - - name: temperature-storage - image: curlimages/curl:8.15.0@sha256:4026b29997dc7c823b51c164b71e2b51e0fd95cce4601f78202c513d97da2922 - command: ["curl"] - args: - - "-f" # Fail on HTTP errors - - "-s" # Silent mode - - "-S" # Show errors - - "--max-time" - - "60" - - "http://hivebox-service/store" - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: - - ALL - resources: - limits: - memory: "32Mi" - cpu: "50m" - requests: - memory: "16Mi" - cpu: "10m" - successfulJobsHistoryLimit: 3 - failedJobsHistoryLimit: 1 diff --git a/k8s/deployment.yml b/k8s/deployment.yml deleted file mode 100644 index 82fd257..0000000 --- a/k8s/deployment.yml +++ /dev/null @@ -1,166 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: hivebox - labels: - app: hivebox -spec: - replicas: 2 - selector: - matchLabels: - app: hivebox - template: - metadata: - labels: - app: hivebox - spec: - securityContext: - fsGroup: 1000 - seccompProfile: - type: RuntimeDefault - containers: - - name: hivebox - image: ghcr.io/gabrielpalmar/hivebox:latest@sha256:4e385cbb108b94a13b9faa884ebc1150119fcf6d4963a75450ed13b344a3c9c5 - ports: - - containerPort: 5000 - env: - - name: REDIS_HOST - value: "redis-service" - - name: MINIO_HOST - value: "minio-service" - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - runAsNonRoot: true - runAsGroup: 1000 - runAsUser: 1000 - capabilities: - drop: - - ALL - resources: - limits: - memory: "512Mi" - cpu: "500m" - requests: - memory: "256Mi" - cpu: "250m" - livenessProbe: - httpGet: - path: /version - port: 5000 - initialDelaySeconds: 60 - periodSeconds: 120 - timeoutSeconds: 10 - failureThreshold: 3 - readinessProbe: - httpGet: - path: /readyz - port: 5000 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 300 - failureThreshold: 3 - volumeMounts: - - name: tmp-volume - mountPath: /tmp - volumes: - - name: tmp-volume - emptyDir: {} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: redis - labels: - app: redis -spec: - replicas: 1 - selector: - matchLabels: - app: redis - template: - metadata: - labels: - app: redis - spec: - securityContext: - fsGroup: 1000 - seccompProfile: - type: RuntimeDefault - containers: - - name: valkey - image: valkey/valkey:8-alpine3.22@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4 - ports: - - containerPort: 6379 - command: ["valkey-server"] - args: ["--save", "", "--appendonly", "no"] - securityContext: - allowPrivilegeEscalation: false - runAsNonRoot: true - runAsGroup: 1000 - runAsUser: 1000 - capabilities: - drop: - - ALL - resources: - limits: - memory: "256Mi" - cpu: "250m" - requests: - memory: "128Mi" - cpu: "100m" - volumeMounts: - - name: valkey-data - mountPath: /data - volumes: - - name: valkey-data - emptyDir: {} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: minio - labels: - app: minio -spec: - replicas: 1 - selector: - matchLabels: - app: minio - template: - metadata: - labels: - app: minio - spec: - securityContext: - fsGroup: 1000 - seccompProfile: - type: RuntimeDefault - containers: - - name: minio - image: minio/minio:RELEASE.2025-07-23T15-54-02Z@sha256:d249d1fb6966de4d8ad26c04754b545205ff15a62e4fd19ebd0f26fa5baacbc0 - ports: - - containerPort: 9000 - command: ["minio"] - args: ["server", "/data"] - securityContext: - allowPrivilegeEscalation: false - runAsNonRoot: true - runAsGroup: 1000 - runAsUser: 1000 - capabilities: - drop: - - ALL - resources: - limits: - memory: "256Mi" - cpu: "250m" - requests: - memory: "128Mi" - cpu: "100m" - volumeMounts: - - name: minio-data - mountPath: /data - volumes: - - name: minio-data - emptyDir: {} diff --git a/k8s/ingress.yml b/k8s/ingress.yml deleted file mode 100644 index c3accda..0000000 --- a/k8s/ingress.yml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: hivebox-ingress - annotations: - nginx.ingress.kubernetes.io/rewrite-target: / - nginx.ingress.kubernetes.io/ssl-redirect: "false" - nginx.ingress.kubernetes.io/force-ssl-redirect: "false" -spec: - ingressClassName: nginx - rules: - - host: hivebox.local - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: hivebox-service - port: - number: 80 ---- -apiVersion: v1 -kind: Service -metadata: - name: redis-service -spec: - selector: - app: redis - ports: - - port: 6379 - targetPort: 6379 ---- -apiVersion: v1 -kind: Service -metadata: - name: minio-service -spec: - selector: - app: minio - ports: - - port: 9000 - targetPort: 9000 diff --git a/k8s/service.yml b/k8s/service.yml deleted file mode 100644 index 5633d33..0000000 --- a/k8s/service.yml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: hivebox-service - labels: - app: hivebox -spec: - selector: - app: hivebox - ports: - - port: 80 - targetPort: 5000 - protocol: TCP - type: ClusterIP