From ebeaa2154f0b5b62f15bbd176ab83dcb33601134 Mon Sep 17 00:00:00 2001 From: Moritz Graf Date: Sun, 16 Nov 2025 12:17:57 +0100 Subject: [PATCH] Adding fabsi n8n --- k8s/README.md | 10 ++ k8s/n8n-fabi/n8n-fabi.secret.yml | 110 ++++++++++++++++++++ k8s/n8n/garmin-mcp.yaml | 59 +++++------ k8s/n8n/garth-wrapper-script_configmap.yaml | 85 --------------- 4 files changed, 145 insertions(+), 119 deletions(-) create mode 100644 k8s/n8n-fabi/n8n-fabi.secret.yml delete mode 100644 k8s/n8n/garth-wrapper-script_configmap.yaml diff --git a/k8s/README.md b/k8s/README.md index b92dcb6..a75bd02 100644 --- a/k8s/README.md +++ b/k8s/README.md @@ -728,3 +728,13 @@ uvx garth login #take output, put in in garth_tkomen.txt kubectl create secret generic garth-token-secret --from-file=GARTH_TOKEN=./garth_token.txt -n n8n ``` + +## n8n-fabi + +```sh +kubectl create ns n8n-fabi +helm upgrade --cleanup-on-fail --install fabi-n8n \ +oci://8gears.container-registry.com/library/n8n \ +--namespace n8n-fabi --values n8n-fabi/n8n-fabi.secret.yml --version 1.0.15 +``` + diff --git a/k8s/n8n-fabi/n8n-fabi.secret.yml b/k8s/n8n-fabi/n8n-fabi.secret.yml new file mode 100644 index 0000000..25ec5c2 --- /dev/null +++ b/k8s/n8n-fabi/n8n-fabi.secret.yml @@ -0,0 +1,110 @@ +#small deployment with nodeport for local testing or small deployments +image: + repository: n8nio/n8n + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "stable" + + +main: + config: + generic: + timezone: Europe/Berlin + n8n: + editor_base_url: https://n8n-fabi.moritzgraf.de + webhook_url: https://n8n-fabi.moritzgraf.de + extra: + node_modules: + - axios + node: + function_allow_builtin: '*' + function_allow_external: '*' + db: + type: postgresdb + postgresdb: + host: db-rw + user: n8n +# password: password is read from cnpg db-app secretKeyRef +# Moritz: Assuming the db-app secret is created by cnpg operator + pool: + size: 10 + ssl: + enabled: true + reject_Unauthorized: true + ca_file: "/home/ssl/certs/postgresql/ca.crt" + secret: + n8n: + encryption_key: "ephikoaloVeev7xaiz5sheig9ieZaNgeihaCaiTh5ahqua5Aelanu8eicooy" + extraEnv: + DB_POSTGRESDB_PASSWORD: + valueFrom: + secretKeyRef: + name: db-app + key: password + # Mount the CNPG CA Cert into N8N container + extraVolumeMounts: + - name: db-ca-cert + mountPath: /home/ssl/certs/postgresql + readOnly: true + + extraVolumes: + - name: db-ca-cert + secret: + secretName: db-ca + items: + - key: ca.crt + path: ca.crt + resources: + limits: + memory: 2048Mi + requests: + memory: 512Mi + service: + type: NodePort + port: 5678 + + +ingress: + # Enable ingress for home assistant + enabled: true + className: "nginx" + annotations: + kubernetes.io/ingress.class: "nginx" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + kubernetes.io/tls-acme: "true" + nginx.ingress.kubernetes.io/proxy-body-size: "0" + nginx.ingress.kubernetes.io/proxy-buffering: "off" + nginx.ingress.kubernetes.io/proxy-request-buffering: "off" + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + hosts: + - host: n8n-fabi.moritzgraf.de + paths: + - / + tls: + - hosts: + - "n8n-fabi.moritzgraf.de" + secretName: n8n-fabi-moritzgraf-de + +# cnpg DB cluster request +extraManifests: + - apiVersion: postgresql.cnpg.io/v1 + kind: Cluster + metadata: + name: db + spec: + instances: 1 + bootstrap: + initdb: + database: n8n + owner: n8n + postgresql: + parameters: + shared_buffers: "64MB" + resources: + requests: + memory: "512Mi" + limits: + memory: "512Mi" + storage: + size: 1Gi \ No newline at end of file diff --git a/k8s/n8n/garmin-mcp.yaml b/k8s/n8n/garmin-mcp.yaml index 1dead2d..9ad0e5e 100644 --- a/k8s/n8n/garmin-mcp.yaml +++ b/k8s/n8n/garmin-mcp.yaml @@ -34,48 +34,39 @@ spec: - name: garth-mcp-server # Use a Python image version >= 3.13 as requested. image: python:3.13-slim - workingDir: /app + resources: + requests: + memory: "64Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" # This command now installs dependencies and directly executes the mounted script. command: ["/bin/sh", "-c"] - args: - - | - set -e - echo "----> Installing Python dependencies..." - pip install flask garth garth-mcp-server - echo "----> Dependencies installed." - echo "----> Starting Flask server from mounted script." - exec python /app/wrapper.py + args: [ + "pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir uv mcp-proxy && \ + echo '--- Setup complete, starting server ---' && \ + mcp-proxy --host=0.0.0.0 --port=8080 --pass-environment uvx garth-mcp-server" + ] ports: - - containerPort: 5000 + - containerPort: 8080 name: http - # Mount the wrapper.py script from the ConfigMap into the container. - volumeMounts: - - name: wrapper-script-volume - mountPath: /app/wrapper.py - subPath: wrapper.py # Inject the Garmin token securely from the Kubernetes Secret. envFrom: - secretRef: name: garth-token-secret - # Health probes for Kubernetes to manage the pod's lifecycle. - livenessProbe: - httpGet: - path: /healthz - port: 5000 - initialDelaySeconds: 15 - periodSeconds: 20 - readinessProbe: - httpGet: - path: /healthz - port: 5000 - # Increased delay to allow for dependency installation. - initialDelaySeconds: 60 - periodSeconds: 10 - # Define the volume that will be populated by the ConfigMap. - volumes: - - name: wrapper-script-volume - configMap: - name: garth-wrapper-script + # # Health probes for Kubernetes to manage the pod's lifecycle. + # livenessProbe: + # tcpSocket: + # port: 8080 + # initialDelaySeconds: 15 + # periodSeconds: 20 + # readinessProbe: + # tcpSocket: + # port: 8080 + # initialDelaySeconds: 60 + # periodSeconds: 10 --- # --- 3. Service to expose the Deployment --- # This creates a stable internal endpoint for the server. diff --git a/k8s/n8n/garth-wrapper-script_configmap.yaml b/k8s/n8n/garth-wrapper-script_configmap.yaml deleted file mode 100644 index 741d5e6..0000000 --- a/k8s/n8n/garth-wrapper-script_configmap.yaml +++ /dev/null @@ -1,85 +0,0 @@ -# configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: garth-wrapper-script - namespace: n8n -data: - wrapper.py: | - # wrapper.py - import subprocess - import logging - from flask import Flask, request, Response - - # Configure basic logging - logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - - # Initialize the Flask application - app = Flask(__name__) - - # Define the main endpoint that n8n will call. It only accepts POST requests. - @app.route('/mcp', methods=['POST']) - def mcp_wrapper(): - """ - This function receives a POST request from n8n, executes the garth-mcp-server - as a subprocess, pipes the request body to the subprocess's stdin, - captures its stdout, and returns it as the HTTP response. - """ - # Retrieve the raw binary data from the incoming request body. - request_data = request.get_data() - logging.info(f"Received request with {len(request_data)} bytes of data.") - - # Define the command to execute the garth-mcp-server. - # 'uvx' is the recommended runner for this package. - command = ['uvx', 'garth-mcp-server'] - - try: - # Execute the command as a subprocess. - # - input: The data to be sent to the subprocess's stdin. - # - capture_output=True: Captures stdout and stderr. - # - check=True: Raises a CalledProcessError if the command returns a non-zero exit code. - # - timeout: Sets a 60-second timeout to prevent hanging processes. - result = subprocess.run( - command, - input=request_data, - capture_output=True, - check=True, - timeout=60 - ) - - # If the command was successful, log the success and return the captured stdout. - # The content type is set to text/plain to ensure proper handling by clients. - logging.info(f"Subprocess executed successfully. Returning {len(result.stdout)} bytes of stdout.") - return Response(result.stdout, mimetype='text/plain', status=200) - - except subprocess.CalledProcessError as e: - # If the subprocess returns a non-zero exit code, it indicates an error. - # Log the error, including the captured stderr for debugging. - error_message = e.stderr.decode('utf-8', errors='ignore') - logging.error(f"Subprocess failed with exit code {e.returncode}. Stderr: {error_message}") - return Response(f"Error executing garth-mcp-server: {error_message}", mimetype='text/plain', status=500) - - except subprocess.TimeoutExpired: - # If the subprocess takes longer than the specified timeout. - logging.error("Subprocess timed out after 60 seconds.") - return Response("Error: garth-mcp-server process timed out.", mimetype='text/plain', status=504) - - except Exception as e: - # Catch any other unexpected exceptions. - logging.error(f"An unexpected error occurred: {str(e)}") - return Response(f"An unexpected server error occurred: {str(e)}", mimetype='text/plain', status=500) - - # Define a simple health check endpoint for Kubernetes liveness and readiness probes. - @app.route('/healthz', methods=['GET']) - def health_check(): - """ - A simple endpoint that returns a 200 OK response, indicating the - Flask server is running and responsive. - """ - return Response("OK", status=200) - - # Main execution block to run the Flask development server. - # host='0.0.0.0' makes the server accessible from outside the container. - # port=5000 is the port the server will listen on. - if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000) \ No newline at end of file