diff --git a/.gitea/workflows/cicd.yaml b/.gitea/workflows/cicd.yaml new file mode 100644 index 0000000..58dda21 --- /dev/null +++ b/.gitea/workflows/cicd.yaml @@ -0,0 +1,90 @@ +name: Build and Publish Docker Image + +on: + push: + branches: + - main + +jobs: + build-and-push: + name: Build and Push Docker Image + runs-on: my_custom_label + container: node:22 + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Prepare Env for Docker + run: | + whoami + which docker || true + apt update + cat /etc/os-release + apt install -y ca-certificates curl gnupg + mkdir -p /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + focal stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + apt update + DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + + - name: Get Version from version.ini + id: get_version + run: | + # Extract version value from version.ini + VERSION=$(awk -F'=[ \t]*' '/^version[ \t]*=/ {gsub(/[ \t"'\''\r]/, "", $2); print $2}' version.ini) + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Build Dockerfile + run: | + export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`" + docker build -t $BUILDIMGTAG:${{ env.VERSION }} . + docker images + + - name: Tag and Push (Harbor) + run: | + export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`" + export FINALBUILDTAG="`cat Dockerfile | tail -n1 | sed 's/^#//g'`" + docker tag $BUILDIMGTAG:${{ env.VERSION }} $FINALBUILDTAG:${{ env.VERSION }} + docker images + echo $CR_PAT | docker login harbor.freshbrewed.science -u $CR_USER --password-stdin + docker push $FINALBUILDTAG:${{ env.VERSION }} + env: # Or as an environment variable + CR_PAT: ${{ secrets.CR_PAT }} + CR_USER: ${{ secrets.CR_USER }} + + - name: Tag and Push (Dockerhub) + run: | + export BUILDIMGTAG="`cat Dockerfile | tail -n1 | sed 's/^.*\///g'`" + docker tag $BUILDIMGTAG:${{ env.VERSION }} $DHUSER/$BUILDIMGTAG:${{ env.VERSION }} + docker images + echo $DHPAT | docker login -u $DHUSER --password-stdin + docker push $DHUSER/$BUILDIMGTAG:${{ env.VERSION }} + env: # Or as an environment variable + DHPAT: ${{ secrets.DHPAT }} + DHUSER: ${{ secrets.DHUSER }} + + - name: Prepare Env for Helm OCI Push (Helm v3 reference only) + run: | + if [ ! -f /tmp/linux-amd64/helm ]; then + apt update + DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl || true + wget https://get.helm.sh/helm-v3.14.0-linux-amd64.tar.gz -O /tmp/helm.tar.gz || true + cd /tmp + tar xzvf helm.tar.gz + chmod +x linux-amd64/helm + fi + + - name: Package Helm Chart + run: | + set -x + /tmp/linux-amd64/helm package ./helm-chart + export HLMPKG=`ls -tr *.tgz | tail -n1 | tr -d '\n'` + /tmp/linux-amd64/helm registry login harbor.freshbrewed.science -u $CR_USER -p $CR_PAT + /tmp/linux-amd64/helm push ./$HLMPKG oci://harbor.freshbrewed.science/chartrepo/k8s-pulse + env: # Or as an environment variable + CR_PAT: ${{ secrets.CR_PAT }} + CR_USER: ${{ secrets.CR_USER }} + diff --git a/Dockerfile b/Dockerfile index 44be037..8bf7648 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,3 +10,4 @@ COPY . . EXPOSE 5000 CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"] +#harbor.freshbrewed.science/library/k8s-pulse diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..37309fe --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +services: + k8s-pulse: + build: + context: . + dockerfile: Dockerfile + container_name: k8s-pulse + ports: + - "5000:5000" + environment: + - KUBECONFIG=/root/.kube/config + volumes: + - ${KUBECONFIG:-~/.kube/config}:/root/.kube/config:ro + restart: unless-stopped diff --git a/helm-chart/Chart.yaml b/helm-chart/Chart.yaml index 8cf4b0a..9e6514f 100644 --- a/helm-chart/Chart.yaml +++ b/helm-chart/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 -name: K8sMonitor -description: A Helm chart for a containerized python web app. -type: Project +name: k8s-pulse +description: A Helm chart for k8s-pulse, a Kubernetes cluster resource visualizer. +type: application version: 0.1.0 appVersion: "1.0.0" diff --git a/helm-chart/INSTALL.md b/helm-chart/INSTALL.md index af2d02d..1c17077 100644 --- a/helm-chart/INSTALL.md +++ b/helm-chart/INSTALL.md @@ -1,78 +1,79 @@ -# K8sMonitor Helm Chart +# k8s-pulse Helm Chart -This Helm chart provides a standardized way to deploy the **K8sMonitor** python web application on a Kubernetes cluster. +This Helm chart provides a production-ready Kubernetes deployment for **k8s-pulse**, a Python web application that visualizes cluster metrics and resource usage. ## Prerequisites -- A running Kubernetes cluster. -- [Helm](https://helm.sh/) installed on your local machine or CI/CD runner. -- Access to a container registry where the `python-web-app` image is stored (if using a custom image). +- A running Kubernetes cluster (v1.19+ recommended). +- [Helm v3](https://helm.sh/) installed. +- (Optional) Metrics Server installed in your cluster for detailed container CPU & memory usage. -## Getting Started +## Quick Start -### 1. Prepare the Configuration -The default configuration can be found in `values.yaml`. You can override any value here before installing or pass them via `--set` or `-f `. +### 1. Install the Chart -Example of modifying ingress annotations: -```yaml -ingress: - annotations: - cert-manager.io/cluster-issuer: "letsencrypt-prod" -``` - -### 2. Install the Chart -To install the chart using the default values: +To install the chart with default release name `k8s-pulse`: ```bash -helm install k8smonitor ./helm-chart +helm install k8s-pulse ./helm-chart ``` -To install with a custom values file: +### 2. Custom Configuration + +You can override default settings by passing a custom `values.yaml` file or using `--set`: ```bash -helm install k8smonitor ./helm-chart -f custom-values.yaml +helm install k8s-pulse ./helm-chart -f custom-values.yaml ``` -### 3. Upgrade the Chart -To update your installation with newer configuration or version: +Example: Enable Ingress and change image tag: ```bash -helm upgrade k8smonitor ./helm-chart +helm install k8s-pulse ./helm-chart \ + --set image.tag="1.0.0" \ + --set ingress.enabled=true \ + --set ingress.hosts[0].host="k8s-pulse.example.com" ``` -## Configuration Options +### 3. Upgrade the Release -| Value | Description | Default | -|------|-------------|---------| -| `replicaCount` | Number of desired pods | 2 | -| `image.repository` | The Docker image to pull | python-web-app | -| `service.port` | The port the service listens on | 80 | -| `ingress.enabled` | Enable or disable Ingress | true | -| `persistence.size` | Size of the PersistentVolumeClaim | 1Gi | +```bash +helm upgrade k8s-pulse ./helm-chart +``` + +### 4. Uninstall + +```bash +helm uninstall k8s-pulse +``` + +## Configuration Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `replicaCount` | Number of application replicas | `1` | +| `image.repository` | Docker image repository | `k8s-pulse` | +| `image.tag` | Docker image tag | `latest` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `service.type` | Kubernetes service type | `ClusterIP` | +| `service.port` | Service port exposed | `80` | +| `service.targetPort` | Container application port | `5000` | +| `rbac.create` | Create ClusterRole & ClusterRoleBinding for K8s API access | `true` | +| `serviceAccount.create` | Create ServiceAccount for the deployment | `true` | +| `serviceAccount.name` | Explicit ServiceAccount name | `""` | +| `ingress.enabled` | Enable Ingress resource | `false` | +| `resources` | Pod CPU/Memory resource limits & requests | `{ limits: { cpu: 200m, memory: 256Mi }, requests: { cpu: 100m, memory: 128Mi } }` | +| `persistence.enabled` | Enable optional PersistentVolumeClaim | `false` | + +## RBAC Requirements + +`k8s-pulse` requires read access to cluster nodes, pods, and `metrics.k8s.io` custom objects. By default, `rbac.create: true` creates a `ClusterRole` and `ClusterRoleBinding` bound to the pod's `ServiceAccount`. ## Verification -Once installed, you can check the status of your deployment: +Check the deployment status: ```bash -# List all pods for the app -kubectl get pods -l app=k8smonitor - -# Check the service -kubectl get svc k8smonitor - -# Check the ingress -kubectl get ingress k8smonitor -``` - -## Troubleshooting - -If the pod is not running, check the logs: -```bash -kubectl logs -l app=k8smonitor -``` - -Check for events in the namespace: -```bash -kubectl get events --sort-by=.metadata.creationTimestamp +kubectl get pods -l app.kubernetes.io/name=k8s-pulse +kubectl get svc -l app.kubernetes.io/name=k8s-pulse ``` diff --git a/helm-chart/templates/_helpers.tpl b/helm-chart/templates/_helpers.tpl new file mode 100644 index 0000000..9cccf79 --- /dev/null +++ b/helm-chart/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "k8s-pulse.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "k8s-pulse.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "k8s-pulse.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "k8s-pulse.labels" -}} +helm.sh/chart: {{ include "k8s-pulse.chart" . }} +{{ include "k8s-pulse.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "k8s-pulse.selectorLabels" -}} +app.kubernetes.io/name: {{ include "k8s-pulse.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "k8s-pulse.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "k8s-pulse.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm-chart/templates/clusterrole.yaml b/helm-chart/templates/clusterrole.yaml new file mode 100644 index 0000000..ec3622e --- /dev/null +++ b/helm-chart/templates/clusterrole.yaml @@ -0,0 +1,15 @@ +{{- if .Values.rbac.create -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "k8s-pulse.fullname" . }} + labels: + {{- include "k8s-pulse.labels" . | nindent 4 }} +rules: + - apiGroups: [""] + resources: ["nodes", "pods"] + verbs: ["get", "list", "watch"] + - apiGroups: ["metrics.k8s.io"] + resources: ["nodes", "pods"] + verbs: ["get", "list"] +{{- end }} diff --git a/helm-chart/templates/clusterrolebinding.yaml b/helm-chart/templates/clusterrolebinding.yaml new file mode 100644 index 0000000..d1ea409 --- /dev/null +++ b/helm-chart/templates/clusterrolebinding.yaml @@ -0,0 +1,16 @@ +{{- if .Values.rbac.create -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "k8s-pulse.fullname" . }} + labels: + {{- include "k8s-pulse.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "k8s-pulse.fullname" . }} +subjects: + - kind: ServiceAccount + name: {{ include "k8s-pulse.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/helm-chart/templates/deployment.yaml b/helm-chart/templates/deployment.yaml index ebcaf46..8d8303d 100644 --- a/helm-chart/templates/deployment.yaml +++ b/helm-chart/templates/deployment.yaml @@ -1,29 +1,74 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "k8smonitor.fullname" . }} + name: {{ include "k8s-pulse.fullname" . }} labels: - {{- include "k8smonitor.selectorLabels" . | nindent 4 }} + {{- include "k8s-pulse.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: - {{- include "k8smonitor.selectorLabels" . | nindent 6 }} + {{- include "k8s-pulse.selectorLabels" . | nindent 6 }} template: metadata: labels: - {{- include "k8smonitor.selectorLabels" . | nindent 8 }} + {{- include "k8s-pulse.selectorLabels" . | nindent 8 }} + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "k8s-pulse.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: {{ .Chart.Name }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http - containerPort: {{ .Values.service.port }} + containerPort: {{ .Values.service.targetPort }} + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- if .Values.persistence.enabled }} volumeMounts: - name: data mountPath: /app/data + {{- end }} + {{- if .Values.persistence.enabled }} volumes: - name: data persistentVolumeClaim: - claimName: {{ include "k8smonitor.fullname" . }}-data + claimName: {{ include "k8s-pulse.fullname" . }}-data + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm-chart/templates/ingress.yaml b/helm-chart/templates/ingress.yaml index d9c47c8..b69ebe5 100644 --- a/helm-chart/templates/ingress.yaml +++ b/helm-chart/templates/ingress.yaml @@ -1,18 +1,45 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "k8s-pulse.fullname" . -}} +{{- $svcPort := .Values.service.port -}} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: {{ include "k8smonitor.fullname" . }} + name: {{ $fullName }} + labels: + {{- include "k8s-pulse.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} annotations: - {{- toYaml .Values.ingress.annotations | nindent 4 }} + {{- toYaml . | nindent 4 }} + {{- end }} spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} rules: - - host: {{ index .Values.ingress.hosts 0 .host | default "k8smonitor.local" }} + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} http: - path: - - path: / - pathType: ImplementationSpecific + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if .pathType }} + pathType: {{ .pathType }} + {{- end }} backend: service: - name: {{ include "k8smonitor.fullname" . }} + name: {{ $fullName }} port: - number: {{ .Values.service.port }} + number: {{ $svcPort }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm-chart/templates/pvc.yaml b/helm-chart/templates/pvc.yaml index 7ada785..264cb14 100644 --- a/helm-chart/templates/pvc.yaml +++ b/helm-chart/templates/pvc.yaml @@ -1,10 +1,17 @@ +{{- if .Values.persistence.enabled -}} apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: {{ include "k8smonitor.fullname" . }}-data + name: {{ include "k8s-pulse.fullname" . }}-data + labels: + {{- include "k8s-pulse.labels" . | nindent 4 }} spec: accessModes: - - ReadWriteOnce + - {{ .Values.persistence.accessMode | default "ReadWriteOnce" }} resources: requests: storage: {{ .Values.persistence.size }} + {{- if .Values.persistence.storageClassName }} + storageClassName: {{ .Values.persistence.storageClassName | quote }} + {{- end }} +{{- end }} diff --git a/helm-chart/templates/service.yaml b/helm-chart/templates/service.yaml index 15e983e..bad6b24 100644 --- a/helm-chart/templates/service.yaml +++ b/helm-chart/templates/service.yaml @@ -1,12 +1,19 @@ apiVersion: v1 kind: Service metadata: - name: {{ include "k8smonitor.fullname" . }} + name: {{ include "k8s-pulse.fullname" . }} + labels: + {{- include "k8s-pulse.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} spec: type: {{ .Values.service.type }} ports: - - name: http - port: {{ .Values.service.port }} - targetPort: http + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP + name: http selector: - {{- include "k8smonitor.selectorLabels" . | nindent 6 }} + {{- include "k8s-pulse.selectorLabels" . | nindent 4 }} diff --git a/helm-chart/templates/serviceaccount.yaml b/helm-chart/templates/serviceaccount.yaml new file mode 100644 index 0000000..b4cce76 --- /dev/null +++ b/helm-chart/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "k8s-pulse.serviceAccountName" . }} + labels: + {{- include "k8s-pulse.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index 3d4807b..abb035d 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -1,25 +1,68 @@ -replicaCount: 2 +# Default values for k8s-pulse. + +replicaCount: 1 image: - repository: python-web-app + repository: k8s-pulse pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. tag: "latest" +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +rbac: + create: true + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + +securityContext: {} + service: type: ClusterIP port: 80 + targetPort: 5000 + annotations: {} ingress: - enabled: true + enabled: false + className: "" + annotations: {} hosts: - - host: k8smonitor.local + - host: k8s-pulse.local paths: - path: / - pathType: ImplementationSpecific - annotations: - kubernetes.io/ingress.nginx._tls.k8smonitor.local: "cert-manager-challenge" + pathType: Prefix + tls: [] + +resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi persistence: - enabled: true + enabled: false size: 1Gi - storageClassName: standard + accessMode: ReadWriteOnce + storageClassName: "" + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/version.ini b/version.ini new file mode 100644 index 0000000..232ddc5 --- /dev/null +++ b/version.ini @@ -0,0 +1,2 @@ +[metadata] +version = 0.1