Browse your Git repositories
80dc39fd2c3c8ddccdce31c6f8b19e2b71de968c|luna-dj|luna@linkse.eu|Tue Jan 20 13:34:17 2026 +0100|Add Origindiff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yamlnew file mode 100644index 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/arm64diff --git a/Dockerfile b/Dockerfilenew file mode 100644index 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.mdnew file mode 100644index 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.yamlnew file mode 100644index 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.comdiff --git a/actalis-hetzner.yaml b/actalis-hetzner.yamlnew file mode 100644index 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: tokendiff --git a/app.py b/app.pynew file mode 100644index 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.shnew file mode 100755index 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"+fidiff --git a/build-in-cluster.sh b/build-in-cluster.shnew file mode 100755index 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.shnew file mode 100755index 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.gzdiff --git a/build-job.yaml b/build-job.yamlnew file mode 100644index 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.jsondiff --git a/certificate.yaml b/certificate.yamlnew file mode 100644index 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.comdiff --git a/create-registry-secret.sh b/create-registry-secret.shnew file mode 100755index 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+fidiff --git a/kubernetes.yaml b/kubernetes.yamlnew file mode 100644index 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: 80diff --git a/letsencrypt-http01.yaml b/letsencrypt-http01.yamlnew file mode 100644index 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: traefikdiff --git a/letsencrypt-issuer.yaml b/letsencrypt-issuer.yamlnew file mode 100644index 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.comdiff --git a/push-to-harbor.sh b/push-to-harbor.shnew file mode 100755index 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+fidiff --git a/requirements.txt b/requirements.txtnew file mode 100644index 0000000..53ea104--- /dev/null+++ b/requirements.txt@@ -0,0 +1,2 @@+Flask==3.0.0+Werkzeug==3.0.1diff --git a/templates/index.html b/templates/index.htmlnew file mode 100644index 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.yamlnew file mode 100644index 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