🍦 Soft Serve Frontend

Browse your Git repositories

Commit 80dc39fd2c3c8ddccdce31c6f8b19e2b71de968c

80dc39fd2c3c8ddccdce31c6f8b19e2b71de968c|luna-dj|luna@linkse.eu|Tue Jan 20 13:34:17 2026 +0100|Add Origin
diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml
new file mode 100644
index 0000000..5730736
--- /dev/null
+++ b/.forgejo/workflows/build.yaml
@@ -0,0 +1,35 @@
+name: Build and Deploy Notes App
+
+on:
+ push:
+ branches: [ main, master ]
+ workflow_dispatch:
+
+jobs:
+ build:
+ name: Build ARM64 container image
+ runs-on: dind # Use dind label for host-mode execution with Docker access
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Create Docker config for authentication
+ run: |
+ mkdir -p $(pwd)/.docker
+ AUTH=$(echo -n "${{ secrets.HARBOR_USERNAME }}:${{ secrets.HARBOR_PASSWORD }}" | base64 -w 0)
+ echo "{\"auths\":{\"harbor.kratov.luiv.dev\":{\"auth\":\"${AUTH}\"}}}" > $(pwd)/.docker/config.json
+
+ - name: Build and push with Kaniko for ARM64
+ run: |
+ docker run --rm \
+ -v $(pwd)/notes-app:/workspace \
+ -v $(pwd)/.docker:/kaniko/.docker:ro \
+ martizih/kaniko:v1.26.4 \
+ --context /workspace \
+ --dockerfile /workspace/Dockerfile \
+ --destination harbor.kratov.luiv.dev/luna_priv/notes:latest \
+ --destination harbor.kratov.luiv.dev/luna_priv/notes:${{ github.sha }} \
+ --cache=true \
+ --cache-repo harbor.kratov.luiv.dev/luna_priv/notes/cache \
+ --custom-platform=linux/arm64
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..2c74af1
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,23 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# Copy requirements and install dependencies
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copy application code
+COPY app.py .
+COPY templates/ templates/
+
+# Create data directory for persistent storage
+RUN mkdir -p /data
+
+# Expose port
+EXPOSE 5000
+
+# Set environment variable
+ENV NOTES_FILE=/data/notes.json
+
+# Run the application
+CMD ["python", "app.py"]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a1be155
--- /dev/null
+++ b/README.md
@@ -0,0 +1,145 @@
+# Simple Notes App
+
+A simple notes application built with Python Flask that allows you to create, view, and delete notes.
+
+## Features
+
+- ✨ Create and delete notes
+- πŸ’Ύ Persistent storage with JSON file
+- 🎨 Beautiful gradient UI
+- πŸ”Œ REST API endpoints
+- ❀️ Health check endpoint
+- 🐳 Fully containerized
+- ☸️ Kubernetes-ready with Traefik ingress
+
+## Local Development
+
+### Prerequisites
+- Python 3.11+
+- pip
+
+### Run Locally
+
+```bash
+cd notes-app
+pip install -r requirements.txt
+python app.py
+```
+
+Visit `http://localhost:5000`
+
+## Docker
+
+### Build Image
+
+```bash
+docker build -t notes-app:latest .
+```
+
+### Run Container
+
+```bash
+docker run -d \
+ -p 5000:5000 \
+ -v $(pwd)/data:/data \
+ --name notes-app \
+ notes-app:latest
+```
+
+## Kubernetes Deployment
+
+### Prerequisites
+- Kubernetes cluster
+- Traefik ingress controller
+- cert-manager with configured issuer
+
+### Deploy
+
+1. Update the domain in `kubernetes.yaml`:
+ - Change `notes.codeuwu.com` to your domain
+
+2. Update the image registry:
+ - Change `your-registry/notes-app:latest` to your actual registry
+
+3. Apply the manifest:
+
+```bash
+kubectl apply -f kubernetes.yaml
+```
+
+### What's Included
+
+The Kubernetes manifest includes:
+- **PersistentVolumeClaim**: 1Gi storage for notes data
+- **Deployment**: Flask app with health checks
+- **Service**: ClusterIP service on port 80
+- **Ingress**: Traefik ingress with automatic TLS via cert-manager
+
+## API Endpoints
+
+- `GET /` - Web interface
+- `GET /api/notes` - Get all notes (JSON)
+- `POST /api/notes` - Create a note (JSON)
+- `POST /add` - Add note via form
+- `POST /delete/<id>` - Delete a note
+- `GET /health` - Health check
+
+## Building with Kaniko
+
+### ⚠️ Important: ARM64 Architecture
+
+Your Kubernetes cluster is running on **ARM64** architecture. You must build images for ARM64.
+
+### Option 1: Build in Forgejo CI/CD (Recommended)
+
+If your Forgejo runner is ARM64, use `.forgejo/workflows/build.yaml`:
+
+```yaml
+name: Build Notes App
+on: [push]
+
+jobs:
+ build:
+ runs-on: dind # Make sure this runner is ARM64
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Create Docker config
+ run: |
+ mkdir -p $(pwd)/.docker
+ AUTH=$(echo -n "${{ secrets.HARBOR_USERNAME }}:${{ secrets.HARBOR_PASSWORD }}" | base64 -w 0)
+ echo "{\"auths\":{\"harbor.kratov.luiv.dev\":{\"auth\":\"${AUTH}\"}}}" > $(pwd)/.docker/config.json
+
+ - name: Build and push with Kaniko for ARM64
+ run: |
+ docker run --rm \
+ -v $(pwd)/notes-app:/workspace \
+ -v $(pwd)/.docker:/kaniko/.docker:ro \
+ martizih/kaniko:v1.26.4 \
+ --context /workspace \
+ --dockerfile /workspace/Dockerfile \
+ --destination harbor.kratov.luiv.dev/luna_priv/notes:latest \
+ --cache=true \
+ --custom-platform=linux/arm64
+```
+
+### Option 2: Build on x86_64 with Buildx
+
+If you have Docker Buildx installed:
+
+```bash
+docker buildx create --use --name arm-builder
+docker buildx build \
+ --platform linux/arm64 \
+ -t harbor.kratov.luiv.dev/luna_priv/notes:latest \
+ --push \
+ notes-app/
+```
+
+### Option 3: Build Directly on an ARM64 Machine
+
+SSH into one of your Kubernetes nodes or an ARM64 machine and build there.
+
+## Storage
+
+Notes are stored in `/data/notes.json` as a JSON array. In Kubernetes, this is backed by a PersistentVolumeClaim to ensure data persists across pod restarts.
diff --git a/actalis-cloudflare.yaml b/actalis-cloudflare.yaml
new file mode 100644
index 0000000..cd90750
--- /dev/null
+++ b/actalis-cloudflare.yaml
@@ -0,0 +1,24 @@
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+ name: actalis-cloudflare
+spec:
+ acme:
+ server: https://acme-api.actalis.com/acme/directory
+ email: luna@linkse.eu
+ privateKeySecretRef:
+ name: actalis-cloudflare-key
+ externalAccountBinding:
+ keyID: J4N0hEAAc8EnGyI4kBi47n9p7p
+ keySecretRef:
+ name: actalis-eab-secret
+ key: secret
+ solvers:
+ - dns01:
+ cloudflare:
+ apiTokenSecretRef:
+ name: cloudflare-api-token
+ key: api-token
+ selector:
+ dnsZones:
+ - codeuwu.com
diff --git a/actalis-hetzner.yaml b/actalis-hetzner.yaml
new file mode 100644
index 0000000..7975115
--- /dev/null
+++ b/actalis-hetzner.yaml
@@ -0,0 +1,24 @@
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+ name: actalis-hetzner
+spec:
+ acme:
+ server: https://acme-api.actalis.com/acme/directory
+ email: luna@linkse.eu
+ privateKeySecretRef:
+ name: actalis-hetzner-key
+ externalAccountBinding:
+ keyID: J4N0hEAAc8EnGyI4kBi47n9p7p
+ keySecretRef:
+ name: actalis-eab-secret
+ key: secret
+ solvers:
+ - dns01:
+ webhook:
+ solverName: hetzner
+ groupName: acme.hetzner.com
+ config:
+ tokenSecretKeyRef:
+ name: hetzner-dns-api-token
+ key: token
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..ef5d1dc
--- /dev/null
+++ b/app.py
@@ -0,0 +1,94 @@
+from flask import Flask, render_template, request, redirect, url_for, jsonify
+import json
+import os
+from datetime import datetime
+
+app = Flask(__name__)
+
+# Store notes in a JSON file
+NOTES_FILE = os.environ.get('NOTES_FILE', '/data/notes.json')
+
+def load_notes():
+ """Load notes from JSON file"""
+ if os.path.exists(NOTES_FILE):
+ try:
+ with open(NOTES_FILE, 'r') as f:
+ return json.load(f)
+ except:
+ return []
+ return []
+
+def save_notes(notes):
+ """Save notes to JSON file"""
+ os.makedirs(os.path.dirname(NOTES_FILE), exist_ok=True)
+ with open(NOTES_FILE, 'w') as f:
+ json.dump(notes, f, indent=2)
+
+@app.route('/')
+def index():
+ """Display all notes"""
+ notes = load_notes()
+ return render_template('index.html', notes=notes)
+
+@app.route('/add', methods=['POST'])
+def add_note():
+ """Add a new note"""
+ title = request.form.get('title', '').strip()
+ content = request.form.get('content', '').strip()
+
+ if title and content:
+ notes = load_notes()
+ note = {
+ 'id': len(notes) + 1,
+ 'title': title,
+ 'content': content,
+ 'created_at': datetime.now().isoformat()
+ }
+ notes.append(note)
+ save_notes(notes)
+
+ return redirect(url_for('index'))
+
+@app.route('/delete/<int:note_id>', methods=['POST'])
+def delete_note(note_id):
+ """Delete a note by ID"""
+ notes = load_notes()
+ notes = [n for n in notes if n['id'] != note_id]
+ save_notes(notes)
+ return redirect(url_for('index'))
+
+@app.route('/api/notes', methods=['GET'])
+def api_get_notes():
+ """API endpoint to get all notes"""
+ notes = load_notes()
+ return jsonify(notes)
+
+@app.route('/api/notes', methods=['POST'])
+def api_add_note():
+ """API endpoint to add a note"""
+ data = request.get_json()
+ title = data.get('title', '').strip()
+ content = data.get('content', '').strip()
+
+ if not title or not content:
+ return jsonify({'error': 'Title and content are required'}), 400
+
+ notes = load_notes()
+ note = {
+ 'id': len(notes) + 1,
+ 'title': title,
+ 'content': content,
+ 'created_at': datetime.now().isoformat()
+ }
+ notes.append(note)
+ save_notes(notes)
+
+ return jsonify(note), 201
+
+@app.route('/health')
+def health():
+ """Health check endpoint"""
+ return jsonify({'status': 'healthy'}), 200
+
+if __name__ == '__main__':
+ app.run(host='0.0.0.0', port=5000, debug=False)
diff --git a/build-arm64.sh b/build-arm64.sh
new file mode 100755
index 0000000..6c7eeaf
--- /dev/null
+++ b/build-arm64.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+
+# Quick build script for ARM64 architecture
+# Run this on an ARM64 machine (like your Forgejo runner)
+
+set -e
+
+echo "=== Building Notes App for ARM64 ==="
+echo ""
+
+# Check architecture
+ARCH=$(uname -m)
+echo "Current architecture: $ARCH"
+
+if [ "$ARCH" != "aarch64" ] && [ "$ARCH" != "arm64" ]; then
+ echo "⚠️ WARNING: You're not on ARM64. This will build for x86_64."
+ echo " Your Kubernetes cluster needs ARM64 images!"
+ read -p "Continue anyway? (y/N) " -n 1 -r
+ echo
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ exit 1
+ fi
+fi
+
+cd "$(dirname "$0")"
+
+echo ""
+echo "Building image..."
+docker build -t harbor.kratov.luiv.dev/luna_priv/notes:latest .
+
+echo ""
+read -p "Push to Harbor? (y/N) " -n 1 -r
+echo
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+ echo "Logging into Harbor..."
+ read -p "Username: " username
+ read -s -p "Password: " password
+ echo ""
+
+ echo "$password" | docker login harbor.kratov.luiv.dev -u "$username" --password-stdin
+
+ echo ""
+ echo "Pushing image..."
+ docker push harbor.kratov.luiv.dev/luna_priv/notes:latest
+
+ echo ""
+ echo "βœ… Done! Image pushed successfully."
+ echo ""
+ echo "Now restart the deployment:"
+ echo " kubectl rollout restart deployment/notes-app -n default"
+fi
diff --git a/build-in-cluster.sh b/build-in-cluster.sh
new file mode 100755
index 0000000..f0541de
--- /dev/null
+++ b/build-in-cluster.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+
+# Script to build ARM64 image directly on a Kubernetes node
+
+echo "This script will build the ARM64 image on a Kubernetes worker node"
+echo ""
+
+# Get the first worker node IP
+WORKER_IP=$(kubectl get nodes -l '!node-role.kubernetes.io/control-plane' -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
+
+echo "Building on worker: $WORKER_IP"
+echo ""
+
+# Create a build pod
+cat <<EOF | kubectl apply -f -
+apiVersion: v1
+kind: Pod
+metadata:
+ name: notes-builder
+ namespace: default
+spec:
+ restartPolicy: Never
+ nodeSelector:
+ kubernetes.io/arch: arm64
+ containers:
+ - name: kaniko
+ image: martizih/kaniko:v1.26.4
+ args:
+ - "--dockerfile=/workspace/Dockerfile"
+ - "--context=git://github.com/YOUR_REPO/notes-app.git" # Update this
+ - "--destination=harbor.kratov.luiv.dev/luna_priv/notes:latest"
+ - "--cache=true"
+ volumeMounts:
+ - name: docker-config
+ mountPath: /kaniko/.docker/
+ volumes:
+ - name: docker-config
+ secret:
+ secretName: harbor-registry-secret
+ items:
+ - key: .dockerconfigjson
+ path: config.json
+EOF
+
+echo "Build pod created. Check status with: kubectl logs -f notes-builder"
diff --git a/build-in-k8s.sh b/build-in-k8s.sh
new file mode 100755
index 0000000..9b35372
--- /dev/null
+++ b/build-in-k8s.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+
+# Simple solution: Build using a Job that copies local files
+
+echo "Creating tar of source files..."
+cd /home/luna/hcloud-1/notes-app
+tar czf /tmp/notes-app-source.tar.gz Dockerfile requirements.txt app.py templates/
+
+echo "Creating ConfigMap from tarball..."
+kubectl create configmap notes-app-tarball \
+ --from-file=source.tar.gz=/tmp/notes-app-source.tar.gz \
+ --namespace=default \
+ --dry-run=client -o yaml | kubectl apply -f -
+
+echo "Creating build job..."
+cat <<'EOF' | kubectl apply -f -
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: build-notes-app
+ namespace: default
+spec:
+ template:
+ spec:
+ restartPolicy: Never
+ dnsPolicy: Default
+ dnsConfig:
+ nameservers:
+ - 8.8.8.8
+ - 1.1.1.1
+ nodeSelector:
+ kubernetes.io/arch: arm64
+ initContainers:
+ - name: extract-source
+ image: busybox:latest
+ command:
+ - sh
+ - -c
+ - |
+ cd /workspace
+ tar xzf /tarball/source.tar.gz
+ ls -la /workspace
+ volumeMounts:
+ - name: tarball
+ mountPath: /tarball
+ - name: workspace
+ mountPath: /workspace
+ containers:
+ - name: kaniko
+ image: gcr.io/kaniko-project/executor:latest
+ args:
+ - "--dockerfile=/workspace/Dockerfile"
+ - "--context=/workspace"
+ - "--destination=harbor.kratov.luiv.dev/luna_priv/notes:latest"
+ - "--cache=true"
+ volumeMounts:
+ - name: workspace
+ mountPath: /workspace
+ - name: docker-config
+ mountPath: /kaniko/.docker/
+ volumes:
+ - name: workspace
+ emptyDir: {}
+ - name: tarball
+ configMap:
+ name: notes-app-tarball
+ - name: docker-config
+ secret:
+ secretName: harbor-registry-secret
+ items:
+ - key: .dockerconfigjson
+ path: config.json
+EOF
+
+echo ""
+echo "Job created! Monitor with:"
+echo " kubectl get pods -l job-name=build-notes-app -w"
+echo " kubectl logs -f job/build-notes-app"
+
+# Cleanup
+rm /tmp/notes-app-source.tar.gz
diff --git a/build-job.yaml b/build-job.yaml
new file mode 100644
index 0000000..0894d43
--- /dev/null
+++ b/build-job.yaml
@@ -0,0 +1,188 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: notes-app-source
+ namespace: default
+data:
+ Dockerfile: |
+ FROM python:3.11-slim
+
+ WORKDIR /app
+
+ # Copy requirements and install dependencies
+ COPY requirements.txt .
+ RUN pip install --no-cache-dir -r requirements.txt
+
+ # Copy application code
+ COPY app.py .
+ COPY templates/ templates/
+
+ # Create data directory for persistent storage
+ RUN mkdir -p /data
+
+ # Expose port
+ EXPOSE 5000
+
+ # Set environment variable
+ ENV NOTES_FILE=/data/notes.json
+
+ # Run the application
+ CMD ["python", "app.py"]
+
+ requirements.txt: |
+ Flask==3.0.0
+ Werkzeug==3.0.1
+
+ app.py: |
+ from flask import Flask, render_template, request, redirect, url_for, jsonify
+ import json
+ import os
+ from datetime import datetime
+
+ app = Flask(__name__)
+
+ # Store notes in a JSON file
+ NOTES_FILE = os.environ.get('NOTES_FILE', '/data/notes.json')
+
+ def load_notes():
+ """Load notes from JSON file"""
+ if os.path.exists(NOTES_FILE):
+ try:
+ with open(NOTES_FILE, 'r') as f:
+ return json.load(f)
+ except:
+ return []
+ return []
+
+ def save_notes(notes):
+ """Save notes to JSON file"""
+ os.makedirs(os.path.dirname(NOTES_FILE), exist_ok=True)
+ with open(NOTES_FILE, 'w') as f:
+ json.dump(notes, f, indent=2)
+
+ @app.route('/')
+ def index():
+ """Display all notes"""
+ notes = load_notes()
+ return render_template('index.html', notes=notes)
+
+ @app.route('/add', methods=['POST'])
+ def add_note():
+ """Add a new note"""
+ title = request.form.get('title', '').strip()
+ content = request.form.get('content', '').strip()
+
+ if title and content:
+ notes = load_notes()
+ note = {
+ 'id': len(notes) + 1,
+ 'title': title,
+ 'content': content,
+ 'created_at': datetime.now().isoformat()
+ }
+ notes.append(note)
+ save_notes(notes)
+
+ return redirect(url_for('index'))
+
+ @app.route('/delete/<int:note_id>', methods=['POST'])
+ def delete_note(note_id):
+ """Delete a note by ID"""
+ notes = load_notes()
+ notes = [n for n in notes if n['id'] != note_id]
+ save_notes(notes)
+ return redirect(url_for('index'))
+
+ @app.route('/api/notes', methods=['GET'])
+ def api_get_notes():
+ """API endpoint to get all notes"""
+ notes = load_notes()
+ return jsonify(notes)
+
+ @app.route('/api/notes', methods=['POST'])
+ def api_add_note():
+ """API endpoint to add a note"""
+ data = request.get_json()
+ title = data.get('title', '').strip()
+ content = data.get('content', '').strip()
+
+ if not title or not content:
+ return jsonify({'error': 'Title and content are required'}), 400
+
+ notes = load_notes()
+ note = {
+ 'id': len(notes) + 1,
+ 'title': title,
+ 'content': content,
+ 'created_at': datetime.now().isoformat()
+ }
+ notes.append(note)
+ save_notes(notes)
+
+ return jsonify(note), 201
+
+ @app.route('/health')
+ def health():
+ """Health check endpoint"""
+ return jsonify({'status': 'healthy'}), 200
+
+ if __name__ == '__main__':
+ app.run(host='0.0.0.0', port=5000, debug=False)
+
+---
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: build-notes-app
+ namespace: default
+spec:
+ template:
+ spec:
+ restartPolicy: Never
+ dnsPolicy: Default
+ dnsConfig:
+ nameservers:
+ - 8.8.8.8
+ - 1.1.1.1
+ nodeSelector:
+ kubernetes.io/arch: arm64
+ initContainers:
+ - name: setup-source
+ image: busybox:latest
+ command:
+ - sh
+ - -c
+ - |
+ cp /source-config/* /workspace/
+ mkdir -p /workspace/templates
+ cp /source-config/index.html /workspace/templates/
+ volumeMounts:
+ - name: source
+ mountPath: /source-config
+ - name: workspace
+ mountPath: /workspace
+ containers:
+ - name: kaniko
+ image: gcr.io/kaniko-project/executor:latest
+ args:
+ - "--dockerfile=/workspace/Dockerfile"
+ - "--context=/workspace"
+ - "--destination=harbor.kratov.luiv.dev/luna_priv/notes:latest"
+ - "--cache=true"
+ volumeMounts:
+ - name: workspace
+ mountPath: /workspace
+ - name: docker-config
+ mountPath: /kaniko/.docker/
+ volumes:
+ - name: workspace
+ emptyDir: {}
+ - name: source
+ configMap:
+ name: notes-app-source
+ - name: docker-config
+ secret:
+ secretName: harbor-registry-secret
+ items:
+ - key: .dockerconfigjson
+ path: config.json
diff --git a/certificate.yaml b/certificate.yaml
new file mode 100644
index 0000000..4c2b0a2
--- /dev/null
+++ b/certificate.yaml
@@ -0,0 +1,13 @@
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+ name: notes-actalis-cert
+ namespace: default
+spec:
+ secretName: notes-actalis-cert
+ issuerRef:
+ name: actalis-hetzner
+ kind: ClusterIssuer
+ group: cert-manager.io
+ dnsNames:
+ - notes.codeuwu.com
diff --git a/create-registry-secret.sh b/create-registry-secret.sh
new file mode 100755
index 0000000..713a121
--- /dev/null
+++ b/create-registry-secret.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+# Create Kubernetes secret for Harbor registry access
+
+echo "Create Kubernetes secret for Harbor registry"
+echo ""
+read -p "Harbor Username: " username
+read -s -p "Harbor Password: " password
+echo ""
+read -p "Namespace (default: default): " namespace
+namespace=${namespace:-default}
+
+kubectl create secret docker-registry harbor-registry-secret \
+ --docker-server=harbor.kratov.luiv.dev \
+ --docker-username="$username" \
+ --docker-password="$password" \
+ --namespace="$namespace"
+
+if [ $? -eq 0 ]; then
+ echo ""
+ echo "βœ… Secret 'harbor-registry-secret' created successfully in namespace '$namespace'"
+ echo ""
+ echo "You can now deploy the app with:"
+ echo "kubectl apply -f kubernetes.yaml"
+else
+ echo "❌ Failed to create secret"
+ exit 1
+fi
diff --git a/kubernetes.yaml b/kubernetes.yaml
new file mode 100644
index 0000000..e264d6c
--- /dev/null
+++ b/kubernetes.yaml
@@ -0,0 +1,103 @@
+---
+# PersistentVolumeClaim for notes data
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: notes-app-pvc
+ namespace: default
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 1Gi
+
+---
+# Deployment for Notes App
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: notes-app
+ namespace: default
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: notes-app
+ template:
+ metadata:
+ labels:
+ app: notes-app
+ spec:
+ imagePullSecrets:
+ - name: harbor-registry-secret
+ containers:
+ - name: notes-app
+ image: harbor.kratov.luiv.dev/luna_priv/notes:latest
+ ports:
+ - containerPort: 5000
+ env:
+ - name: NOTES_FILE
+ value: /data/notes.json
+ volumeMounts:
+ - name: notes-data
+ mountPath: /data
+ livenessProbe:
+ httpGet:
+ path: /health
+ port: 5000
+ initialDelaySeconds: 10
+ periodSeconds: 30
+ readinessProbe:
+ httpGet:
+ path: /health
+ port: 5000
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ volumes:
+ - name: notes-data
+ persistentVolumeClaim:
+ claimName: notes-app-pvc
+
+---
+# Service for Notes App
+apiVersion: v1
+kind: Service
+metadata:
+ name: notes-app
+ namespace: default
+spec:
+ selector:
+ app: notes-app
+ ports:
+ - port: 80
+ targetPort: 5000
+
+---
+# Ingress with automatic HTTPS via Let's Encrypt
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: notes-app-tls
+ namespace: default
+ annotations:
+ cert-manager.io/cluster-issuer: letsencrypt-http01
+ traefik.ingress.kubernetes.io/router.tls: "true"
+ traefik.ingress.kubernetes.io/redirect-to-https: "true"
+spec:
+ ingressClassName: traefik
+ tls:
+ - hosts:
+ - notes.codeuwu.com
+ secretName: notes-actalis-cert
+ rules:
+ - host: notes.codeuwu.com # Change to your domain
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: notes-app
+ port:
+ number: 80
diff --git a/letsencrypt-http01.yaml b/letsencrypt-http01.yaml
new file mode 100644
index 0000000..d1fb3e0
--- /dev/null
+++ b/letsencrypt-http01.yaml
@@ -0,0 +1,14 @@
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+ name: letsencrypt-http01
+spec:
+ acme:
+ server: https://acme-v02.api.letsencrypt.org/directory
+ email: luna@linkse.eu
+ privateKeySecretRef:
+ name: letsencrypt-http01-key
+ solvers:
+ - http01:
+ ingress:
+ class: traefik
diff --git a/letsencrypt-issuer.yaml b/letsencrypt-issuer.yaml
new file mode 100644
index 0000000..7dfb96c
--- /dev/null
+++ b/letsencrypt-issuer.yaml
@@ -0,0 +1,25 @@
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+ name: zerossl-cloudflare
+spec:
+ acme:
+ server: https://acme.zerossl.com/v2/DV90
+ email: luna@linkse.eu
+ privateKeySecretRef:
+ name: zerossl-cloudflare-account-key
+ externalAccountBinding:
+ keyID: QYqSjUIllXvWfAgvAiCdiQ
+ keySecretRef:
+ name: zerossl-eab-secret
+ key: secret
+ keyAlgorithm: HS256
+ solvers:
+ - dns01:
+ cloudflare:
+ apiTokenSecretRef:
+ name: cloudflare-api-token
+ key: api-token
+ selector:
+ dnsZones:
+ - codeuwu.com
diff --git a/push-to-harbor.sh b/push-to-harbor.sh
new file mode 100755
index 0000000..f491230
--- /dev/null
+++ b/push-to-harbor.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+# Script to push notes app to Harbor registry
+
+echo "Harbor Registry: harbor.kratov.luiv.dev/luna_priv/notes"
+echo ""
+echo "Please login to Harbor first:"
+read -p "Harbor Username: " username
+read -s -p "Harbor Password: " password
+echo ""
+
+echo "$password" | docker login harbor.kratov.luiv.dev -u "$username" --password-stdin
+
+if [ $? -eq 0 ]; then
+ echo ""
+ echo "Pushing image to Harbor..."
+ docker push harbor.kratov.luiv.dev/luna_priv/notes:latest
+
+ if [ $? -eq 0 ]; then
+ echo ""
+ echo "βœ… Image pushed successfully!"
+ echo ""
+ echo "Now create the Kubernetes secret with:"
+ echo "kubectl create secret docker-registry harbor-registry-secret \\"
+ echo " --docker-server=harbor.kratov.luiv.dev \\"
+ echo " --docker-username=\$username \\"
+ echo " --docker-password=\$password \\"
+ echo " --namespace=default"
+ else
+ echo "❌ Failed to push image"
+ exit 1
+ fi
+else
+ echo "❌ Failed to login to Harbor"
+ exit 1
+fi
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..53ea104
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+Flask==3.0.0
+Werkzeug==3.0.1
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..165619a
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,186 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Simple Notes App</title>
+ <style>
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+ body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ min-height: 100vh;
+ padding: 20px;
+ }
+ .container {
+ max-width: 800px;
+ margin: 0 auto;
+ }
+ h1 {
+ color: white;
+ text-align: center;
+ margin-bottom: 30px;
+ font-size: 2.5em;
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
+ }
+ .add-note-form {
+ background: white;
+ padding: 25px;
+ border-radius: 10px;
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
+ margin-bottom: 30px;
+ }
+ .form-group {
+ margin-bottom: 15px;
+ }
+ label {
+ display: block;
+ margin-bottom: 5px;
+ color: #333;
+ font-weight: 600;
+ }
+ input[type="text"], textarea {
+ width: 100%;
+ padding: 12px;
+ border: 2px solid #e0e0e0;
+ border-radius: 5px;
+ font-size: 14px;
+ transition: border-color 0.3s;
+ }
+ input[type="text"]:focus, textarea:focus {
+ outline: none;
+ border-color: #667eea;
+ }
+ textarea {
+ resize: vertical;
+ min-height: 100px;
+ font-family: inherit;
+ }
+ button {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ padding: 12px 30px;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 16px;
+ font-weight: 600;
+ transition: transform 0.2s, box-shadow 0.2s;
+ }
+ button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
+ }
+ button:active {
+ transform: translateY(0);
+ }
+ .notes-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 20px;
+ }
+ .note-card {
+ background: white;
+ padding: 20px;
+ border-radius: 10px;
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
+ transition: transform 0.2s, box-shadow 0.2s;
+ }
+ .note-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 8px 15px rgba(0,0,0,0.2);
+ }
+ .note-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: start;
+ margin-bottom: 10px;
+ }
+ .note-title {
+ font-size: 1.3em;
+ font-weight: 600;
+ color: #333;
+ flex: 1;
+ }
+ .note-content {
+ color: #666;
+ line-height: 1.6;
+ margin-bottom: 15px;
+ white-space: pre-wrap;
+ }
+ .note-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-top: 15px;
+ border-top: 1px solid #e0e0e0;
+ }
+ .note-date {
+ font-size: 0.85em;
+ color: #999;
+ }
+ .delete-btn {
+ background: #f44336;
+ padding: 8px 16px;
+ font-size: 14px;
+ }
+ .delete-btn:hover {
+ background: #d32f2f;
+ }
+ .empty-state {
+ text-align: center;
+ color: white;
+ font-size: 1.2em;
+ margin-top: 50px;
+ opacity: 0.8;
+ }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <h1>πŸ“ Simple Notes App</h1>
+
+ <div class="add-note-form">
+ <h2 style="margin-bottom: 20px; color: #333;">Add New Note</h2>
+ <form method="POST" action="/add">
+ <div class="form-group">
+ <label for="title">Title</label>
+ <input type="text" id="title" name="title" required placeholder="Enter note title...">
+ </div>
+ <div class="form-group">
+ <label for="content">Content</label>
+ <textarea id="content" name="content" required placeholder="Enter note content..."></textarea>
+ </div>
+ <button type="submit">Add Note</button>
+ </form>
+ </div>
+
+ {% if notes %}
+ <div class="notes-grid">
+ {% for note in notes %}
+ <div class="note-card">
+ <div class="note-header">
+ <div class="note-title">{{ note.title }}</div>
+ </div>
+ <div class="note-content">{{ note.content }}</div>
+ <div class="note-footer">
+ <div class="note-date">{{ note.created_at[:10] }}</div>
+ <form method="POST" action="/delete/{{ note.id }}" style="display: inline;">
+ <button type="submit" class="delete-btn">Delete</button>
+ </form>
+ </div>
+ </div>
+ {% endfor %}
+ </div>
+ {% else %}
+ <div class="empty-state">
+ No notes yet. Add your first note above! ✨
+ </div>
+ {% endif %}
+ </div>
+</body>
+</html>
diff --git a/zerossl-cloudflare.yaml b/zerossl-cloudflare.yaml
new file mode 100644
index 0000000..4e0bff2
--- /dev/null
+++ b/zerossl-cloudflare.yaml
@@ -0,0 +1,24 @@
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+ name: zerossl-cloudflare
+spec:
+ acme:
+ server: https://acme.zerossl.com/v2/DV90
+ email: luna@linkse.eu
+ privateKeySecretRef:
+ name: zerossl-cloudflare-key
+ externalAccountBinding:
+ keyID: QYqSjUIllXvWfAgvAiCdiQ
+ keySecretRef:
+ name: zerossl-eab-secret
+ key: secret
+ solvers:
+ - dns01:
+ cloudflare:
+ apiTokenSecretRef:
+ name: cloudflare-api-token
+ key: api-token
+ selector:
+ dnsZones:
+ - codeuwu.com