diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
index ce7b173..0000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-name: ci-nullplatform
-env:
- NULLPLATFORM_API_KEY: ${{ secrets.NULLPLATFORM_API_KEY }}
-on:
- push:
- branches:
- - main
-permissions:
- id-token: write
- contents: read
- packages: read
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - name: Install nullplatform cli
- run: curl https://cli.nullplatform.com/install.sh | sh
- - name: Checkout code
- uses: actions/checkout@v4
- - name: Start nullplatform CI
- run: np build start
- - name: Build asset
- run: docker build -t main .
- - name: Push asset
- run: np asset push --type docker-image --source main
- - name: End nullplatform CI
- if: ${{ always() }}
- run: np build update --status ${{ contains(fromJSON('["failure", "cancelled"]'), job.status) && 'failed' || 'successful' }}
\ No newline at end of file
diff --git a/.github/workflows/conventional-commit.yml b/.github/workflows/conventional-commit.yml
new file mode 100644
index 0000000..92952e1
--- /dev/null
+++ b/.github/workflows/conventional-commit.yml
@@ -0,0 +1,10 @@
+name: conventional-commit
+
+on:
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ conventional-commit:
+ uses: nullplatform/actions-nullplatform/.github/workflows/conventional-commit.yml@main
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..8a65e73
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,15 @@
+name: release
+
+on:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ release:
+ uses: nullplatform/actions-nullplatform/.github/workflows/release.yml@main
+ secrets: inherit
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 2c788e0..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,5 +0,0 @@
-FROM hashicorp/http-echo:1.0.0
-
-CMD ["-text={\"status\":\"ok\",\"msg\":\"Hola mundo\"}", "-listen=:8080", "-status-code=200"]
-
-
diff --git a/README.md b/README.md
index 4e481b6..bf95254 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,222 @@
-
-
-
-
-
-
- Nullplatform "Any Technology" Template
-
-
-
-This is a minimalistic sample on how you can create an application on arbitrary technology.
-In particular, we're spinning up an image that contains an echo server.
-You can check *Echo Server* documentation [here](https://ealenn.github.io/Echo-Server/).
-
-## How do I modify this template to build my own application?
-
-1. Change the Dockerfile to run the application / binary that you are building
-2. Deploy your application in nullplatform
+# services-postgresql-k8s
+
+Nullplatform service definition for managed **PostgreSQL on Kubernetes**. Deploys and manages production-ready PostgreSQL instances via the Bitnami Helm chart, integrated with the nullplatform platform lifecycle (create, update, delete, actions, links).
+
+## Overview
+
+This repository packages the full lifecycle of a PostgreSQL database as a nullplatform **dependency service**. When registered, the platform can spin up isolated PostgreSQL instances per project, manage database users, and execute schema/data queries — all from the nullplatform dashboard or CLI.
+
+| Property | Value |
+|----------------|------------------------|
+| Service name | `Postgres DB` |
+| Slug | `postgres-db` |
+| Type | Dependency |
+| Provider | Kubernetes (K8S) |
+| Category | Database / Relational |
+| Helm chart | `bitnami/postgresql` |
+
+## Architecture
+
+```
+nullplatform agent
+ │
+ ▼
+handle-service-agent ──► np service-action exec
+ │
+ ├── service/
+ │ ├── create-postgres-db # helm upgrade --install
+ │ ├── update-postgres-db # re-render values + helm upgrade
+ │ ├── delete-postgres-db # helm uninstall + cleanup
+ │ ├── run-ddl-query # CREATE / ALTER / DROP / TRUNCATE
+ │ └── run-dml-query # SELECT / INSERT / UPDATE / DELETE
+ │
+ └── link/
+ ├── create-database-user # CREATE USER + set permissions
+ ├── update-database-user # GRANT / REVOKE permissions
+ └── delete-database-user # DROP USER
+```
+
+Each script reads its input from environment variables injected by the np agent (`ACTION_PARAMETERS_*`, `ACTION_SERVICE_ATTRIBUTES_*`, `NP_ACTION_CONTEXT`) and writes results back via `np service action update --results`.
+
+## Features
+
+- **Helm-managed lifecycle** — PostgreSQL installed/upgraded/removed via the Bitnami chart.
+- **Templated values** — `values.yaml.tpl` rendered at runtime with `gomplate` using project-specific parameters.
+- **PII security context** — when `pii: true`, the pod runs as non-root with `runAsUser: 1001`.
+- **Credential management** — admin passwords auto-generated and stored in Kubernetes secrets; never hard-coded.
+- **Database user links** — create per-application users with granular `read / write / admin` permissions.
+- **DDL & DML actions** — execute schema migrations or data queries directly through the platform, with query-type validation.
+
+## Service Attributes
+
+| Attribute | Type | Required | Exported | Description |
+|--------------------|---------|----------|----------|--------------------------------------------------|
+| `usage_type` | enum | Yes | No | `transactions`, `cache`, or `configurations` |
+| `pii` | boolean | Yes | No | Enables security context for PII workloads |
+| `hostname` | string | No | Yes | ClusterIP assigned after creation (read-only) |
+| `port` | number | No | Yes | PostgreSQL port — always `5432` (read-only) |
+| `dbname` | string | No | Yes | Database name derived from `usage_type` (read-only)|
+
+## Available Actions
+
+### `run-ddl-query`
+
+Executes DDL statements (`CREATE`, `ALTER`, `DROP`, `TRUNCATE`) against the database using an in-cluster PostgreSQL client pod. Non-DDL queries are rejected with an error.
+
+```sql
+CREATE TABLE orders (
+ id SERIAL PRIMARY KEY,
+ user_id INT NOT NULL,
+ total NUMERIC(10,2),
+ created_at TIMESTAMP DEFAULT NOW()
+);
+```
+
+### `run-dml-query`
+
+Executes DML statements (`SELECT`, `INSERT`, `UPDATE`, `DELETE`) against the database.
+
+```sql
+INSERT INTO orders (user_id, total) VALUES (42, 99.99);
+```
+
+## Available Links
+
+### `database-user`
+
+Creates an isolated database user for an application to consume. Returns `username` and `password` as exported (secret) environment variables.
+
+**Permission options** (`permisions` object):
+
+| Field | Type | Default | Description |
+|---------|---------|---------|--------------------------|
+| `read` | boolean | `true` | `SELECT` on all tables |
+| `write` | boolean | `false` | `INSERT / UPDATE / DELETE` |
+| `admin` | boolean | `false` | DDL / schema management |
+
+## Installation
+
+Register this service definition in your nullplatform account using the provided OpenTofu module under `tofu-module/`:
+
+```hcl
+module "postgres_db" {
+ source = "path/to/services-postgresql-k8s/tofu-module"
+
+ nrn = var.nrn
+ np_api_key = var.np_api_key
+ tags_selectors = var.tags_selectors
+}
+```
+
+**Variables:**
+
+| Variable | Type | Sensitive | Description |
+|-----------------|---------------|-----------|--------------------------------------------------|
+| `nrn` | `string` | No | Nullplatform Resource Name |
+| `np_api_key` | `string` | Yes | API key for authenticating with Nullplatform |
+| `tags_selectors`| `map(string)` | No | Tags used to select channels and agents |
+
+**Outputs:**
+
+| Output | Description |
+|-----------------------------------------|----------------------------------------------|
+| `service_specification_slug_postgres_db`| Slug of the Postgres DB service specification|
+| `service_specification_id_postgres_db` | ID of the Postgres DB service specification |
+
+The module internally uses:
+- [`nullplatform/tofu-modules//nullplatform/service_definition@v1.52.3`](https://github.com/nullplatform/tofu-modules) — registers the service spec.
+- [`nullplatform/tofu-modules//nullplatform/service_definition_agent_association@v1.43.0`](https://github.com/nullplatform/tofu-modules) — wires the agent command to the service.
+
+Requires the `nullplatform` Terraform provider `~> 0.0.75`.
+
+## Default Helm Values
+
+```yaml
+primary:
+ persistence:
+ enabled: true
+ size: 10Gi
+ resources:
+ limits:
+ cpu: 500m
+ memory: 512Mi
+ requests:
+ cpu: 100m
+ memory: 256Mi
+
+service:
+ type: ClusterIP
+ ports:
+ postgresql: 5432
+
+metrics:
+ enabled: false
+```
+
+When `pii: true`, the following security context is applied to the primary pod:
+
+```yaml
+securityContext:
+ enabled: true
+ runAsNonRoot: true
+ runAsUser: 1001
+ fsGroup: 1001
+```
+
+## Directory Structure
+
+```
+.
+├── postgres/k8s/
+│ ├── handle-service-agent # Entry point — delegates to np service-action exec
+│ ├── entrypoint/entrypoint # Alias entry point
+│ ├── specs/ # Nullplatform service contract templates
+│ │ ├── service-spec.json.tpl
+│ │ ├── actions/
+│ │ │ ├── run-ddl-query.json.tpl
+│ │ │ └── run-dml-query.json.tpl
+│ │ └── links/
+│ │ └── database-user.json.tpl
+│ └── postgres-db/
+│ ├── ensure_psql.sh # Installs psql client if missing
+│ ├── run_query_in_pod.sh # Runs SQL via a temporary K8s pod
+│ ├── service/
+│ │ ├── create-postgres-db
+│ │ ├── update-postgres-db
+│ │ ├── delete-postgres-db
+│ │ ├── run-ddl-query
+│ │ ├── run-dml-query
+│ │ ├── handle-helm.sh # Helm repo setup + chart install/upgrade
+│ │ ├── project.sh # Resolves project name from context
+│ │ ├── ensure_helm_deps.sh # Ensures gomplate + helm are available
+│ │ └── values.yaml.tpl # Helm values template
+│ └── link/
+│ ├── create-database-user
+│ ├── update-database-user
+│ └── delete-database-user
+└── tofu-module/ # OpenTofu module to register the service in nullplatform
+ ├── main.tf # service_definition + agent_association resources
+ ├── variables.tf # nrn, np_api_key, tags_selectors
+ ├── outputs.tf # service slug and ID outputs
+ └── provider.tf # nullplatform provider ~> 0.0.75
+```
+
+## Troubleshooting
+
+| Symptom | Likely cause | Resolution |
+|---------|-------------|------------|
+| `Failed to get PostgreSQL service IP` | Pod not scheduling | Check node resources and PVC availability |
+| `Permission denied` on query | Wrong user permissions | Update the `database-user` link permissions |
+| `Only DDL queries are allowed` error | Wrong action used | Use `run-dml-query` for SELECT/INSERT/UPDATE/DELETE |
+| Helm release pending | Previous install failed | `helm rollback` or delete the release and retry |
+
+```bash
+# Check pod status
+kubectl get pods -n postgres-db
+
+# View logs
+kubectl logs -n postgres-db -l app.kubernetes.io/name=postgresql
+
+# Check Helm release
+helm status -postgres -n postgres-db
+```
\ No newline at end of file
diff --git a/postgres/k8s/entrypoint/entrypoint b/postgres/k8s/entrypoint/entrypoint
new file mode 100755
index 0000000..3deab82
--- /dev/null
+++ b/postgres/k8s/entrypoint/entrypoint
@@ -0,0 +1,4 @@
+#!/bin/bash
+set -e
+SCRIPT_DIR="$(dirname "$(realpath "$0")")"
+exec "$SCRIPT_DIR/../handle-service-agent" "$@"
diff --git a/postgres/k8s/handle-service-agent b/postgres/k8s/handle-service-agent
new file mode 100755
index 0000000..6b011a9
--- /dev/null
+++ b/postgres/k8s/handle-service-agent
@@ -0,0 +1,14 @@
+#!/bin/bash -x
+
+WORKING_DIRECTORY="$(dirname "$(realpath "$0")")"
+cd "$WORKING_DIRECTORY" || exit 1
+
+# Bridge: np-agent exposes NP_API_KEY, but np CLI expects NULLPLATFORM_API_KEY
+if [ -n "${NP_API_KEY:-}" ] && [ -z "${NULLPLATFORM_API_KEY:-}" ]; then
+ export NULLPLATFORM_API_KEY="$NP_API_KEY"
+fi
+
+echo "Starting handle agent service"
+
+np service-action exec --live-output --live-report --debug
+exit $?
\ No newline at end of file
diff --git a/postgres/k8s/postgres-db/ensure_psql.sh b/postgres/k8s/postgres-db/ensure_psql.sh
new file mode 100755
index 0000000..e5fd879
--- /dev/null
+++ b/postgres/k8s/postgres-db/ensure_psql.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+CLI=$(which uuidgen)
+if [[ "$CLI" == "" ]]; then
+ apk add uuidgen
+fi
\ No newline at end of file
diff --git a/postgres/k8s/postgres-db/link/create-database-user b/postgres/k8s/postgres-db/link/create-database-user
new file mode 100755
index 0000000..7655b83
--- /dev/null
+++ b/postgres/k8s/postgres-db/link/create-database-user
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+set -e
+export WORKING_DIRECTORY_ORIGINAL="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd $WORKING_DIRECTORY_ORIGINAL
+../ensure_psql.sh
+
+# Get service connection details
+SERVICE_HOSTNAME=$ACTION_SERVICE_ATTRIBUTES_HOSTNAME
+SERVICE_PORT=$ACTION_SERVICE_ATTRIBUTES_PORT
+SERVICE_DBNAME=$ACTION_SERVICE_ATTRIBUTES_DBNAME
+SECRET_NAME=$ACTION_SERVICE_ATTRIBUTES_K_8_S_SECRET_NAME
+
+# Get link parameters
+export DB_USERNAME="usr$(uuidgen | tr -d "-")"
+USER_TYPE=$ACTION_PARAMETERS_USER_TYPE
+GENERATED_PASSWORD=pwd"$(uuidgen)"
+
+# Get admin credentials from K8s secret
+ADMIN_PASSWORD=$(kubectl get secret $SECRET_NAME -n postgres-db -o json | jq -r '.data["postgres-password"]' | base64 -d)
+
+# Prepare user creation query
+USER_CREATION_QUERY="
+-- Create user
+CREATE USER "$DB_USERNAME" WITH PASSWORD '$GENERATED_PASSWORD';
+"
+
+# Execute user creation using PostgreSQL client pod
+../run_query_in_pod.sh "$SERVICE_HOSTNAME" "$SERVICE_PORT" "$SERVICE_DBNAME" "postgres" "$ADMIN_PASSWORD" "$USER_CREATION_QUERY" "ddl"
+
+# After create we run edit to set permissions
+./update-database-user
+# Update link results with connection details
+JSON_RESPONSE=$(echo $NP_ACTION_CONTEXT | jq ".notification.parameters + {
+ username: \"$DB_USERNAME\",
+ password: \"$GENERATED_PASSWORD\"
+}")
+
+np link action update --results "$JSON_RESPONSE"
\ No newline at end of file
diff --git a/postgres/k8s/postgres-db/link/delete-database-user b/postgres/k8s/postgres-db/link/delete-database-user
new file mode 100755
index 0000000..80b9d86
--- /dev/null
+++ b/postgres/k8s/postgres-db/link/delete-database-user
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+set -e
+export WORKING_DIRECTORY_ORIGINAL="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd $WORKING_DIRECTORY_ORIGINAL
+
+# Get service connection details
+SERVICE_HOSTNAME=$ACTION_SERVICE_ATTRIBUTES_HOSTNAME
+SERVICE_PORT=$ACTION_SERVICE_ATTRIBUTES_PORT
+SERVICE_DBNAME=$ACTION_SERVICE_ATTRIBUTES_DBNAME
+SECRET_NAME=$ACTION_SERVICE_ATTRIBUTES_K_8_S_SECRET_NAME
+
+# Get link parameters
+USERNAME=$ACTION_LINK_ATTRIBUTES_USERNAME
+
+# Get admin credentials from K8s secret
+ADMIN_PASSWORD=$(kubectl get secret $SECRET_NAME -n postgres-db -o json | jq -r '.data["postgres-password"]' | base64 -d)
+
+# Connect to PostgreSQL and delete user
+QUERY=$(cat << EOF
+-- Revoke all privileges first
+REVOKE ALL PRIVILEGES ON DATABASE "$SERVICE_DBNAME" FROM "$USERNAME";
+REVOKE ALL PRIVILEGES ON SCHEMA public FROM "$USERNAME";
+REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "$USERNAME";
+REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM "$USERNAME";
+ALTER USER "$USERNAME" WITH NOSUPERUSER;
+
+-- Reassign or drop owned objects
+REASSIGN OWNED BY "$USERNAME" TO postgres;
+DROP OWNED BY "$USERNAME";
+
+-- Now drop the user
+DROP USER IF EXISTS "$USERNAME";
+EOF
+)
+
+../run_query_in_pod.sh "$SERVICE_HOSTNAME" "$SERVICE_PORT" "$SERVICE_DBNAME" "postgres" "$ADMIN_PASSWORD" "$QUERY" "ddl"
+
+
+echo "User $USERNAME deleted successfully"
\ No newline at end of file
diff --git a/postgres/k8s/postgres-db/link/update-database-user b/postgres/k8s/postgres-db/link/update-database-user
new file mode 100755
index 0000000..0d549da
--- /dev/null
+++ b/postgres/k8s/postgres-db/link/update-database-user
@@ -0,0 +1,70 @@
+#!/bin/bash
+
+set -e
+export WORKING_DIRECTORY_ORIGINAL="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd $WORKING_DIRECTORY_ORIGINAL
+../ensure_psql.sh
+
+# Get service connection details
+SERVICE_HOSTNAME=$ACTION_SERVICE_ATTRIBUTES_HOSTNAME
+SERVICE_PORT=$ACTION_SERVICE_ATTRIBUTES_PORT
+SERVICE_DBNAME=$ACTION_SERVICE_ATTRIBUTES_DBNAME
+SECRET_NAME=$ACTION_SERVICE_ATTRIBUTES_K_8_S_SECRET_NAME
+
+if [[ $DB_USERNAME == "" ]]; then
+ DB_USERNAME=$ACTION_LINK_ATTRIBUTES_USERNAME
+fi
+
+
+# Get admin credentials from K8s secret
+ADMIN_PASSWORD=$(kubectl get secret $SECRET_NAME -n postgres-db -o json | jq -r '.data["postgres-password"]' | base64 -d)
+
+# Prepare user update query
+USER_UPDATE_QUERY="
+
+-- Revoke all existing privileges
+REVOKE ALL PRIVILEGES ON DATABASE "$SERVICE_DBNAME" FROM "$DB_USERNAME";
+REVOKE ALL PRIVILEGES ON SCHEMA public FROM "$DB_USERNAME";
+REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "$DB_USERNAME";
+REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM "$DB_USERNAME";
+ALTER USER "$DB_USERNAME" WITH NOSUPERUSER;
+"
+
+if [[ "$ACTION_PARAMETERS_PERMISIONS_READ" == "true" ]]; then
+ USER_UPDATE_QUERY+="
+GRANT CONNECT ON DATABASE \"$SERVICE_DBNAME\" TO \"$DB_USERNAME\";
+GRANT USAGE ON SCHEMA public TO \"$DB_USERNAME\";
+GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"$DB_USERNAME\";
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO \"$DB_USERNAME\";
+"
+fi
+if [[ "$ACTION_PARAMETERS_PERMISIONS_WRITE" == "true" ]]; then
+ USER_UPDATE_QUERY+="
+GRANT CONNECT ON DATABASE \"$SERVICE_DBNAME\" TO \"$DB_USERNAME\";
+GRANT USAGE, CREATE ON SCHEMA public TO \"$DB_USERNAME\";
+GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"$DB_USERNAME\";
+GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO \"$DB_USERNAME\";
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO \"$DB_USERNAME\";
+ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO \"$DB_USERNAME\";
+"
+fi
+
+if [[ "$ACTION_PARAMETERS_PERMISIONS_ADMIN" == "true" ]]; then
+ USER_UPDATE_QUERY+="
+ALTER USER \"$DB_USERNAME\" WITH SUPERUSER;
+"
+fi
+
+# Execute user update using PostgreSQL client pod
+../run_query_in_pod.sh "$SERVICE_HOSTNAME" "$SERVICE_PORT" "$SERVICE_DBNAME" "postgres" "$ADMIN_PASSWORD" "$USER_UPDATE_QUERY" "ddl"
+
+# Update link results with connection details
+JSON_RESPONSE=$(echo $NP_ACTION_CONTEXT | jq ".notification.parameters + {
+ permisions: {
+ read: $ACTION_PARAMETERS_PERMISIONS_READ,
+ write: $ACTION_PARAMETERS_PERMISIONS_WRITE,
+ admin: $ACTION_PARAMETERS_PERMISIONS_ADMIN
+ }
+}")
+
+np link action update --results "$JSON_RESPONSE"
\ No newline at end of file
diff --git a/postgres/k8s/postgres-db/run_query_in_pod.sh b/postgres/k8s/postgres-db/run_query_in_pod.sh
new file mode 100755
index 0000000..0328c3e
--- /dev/null
+++ b/postgres/k8s/postgres-db/run_query_in_pod.sh
@@ -0,0 +1,148 @@
+#!/bin/bash
+
+# Function to run PostgreSQL queries using Bitnami PostgreSQL client pod
+# Usage: run_query_in_pod [query_type]
+# query_type: "ddl" or "dml" (default: "dml")
+
+set -e
+HOSTNAME=$1
+PORT=$2
+DBNAME=$3
+USERNAME=$4
+PASSWORD=$5
+QUERY=$6
+QUERY_TYPE=${7:-"dml"}
+QUERY_OUTPUT_FILE=$8
+if [ -z "$HOSTNAME" ] || [ -z "$PORT" ] || [ -z "$DBNAME" ] || [ -z "$USERNAME" ] || [ -z "$PASSWORD" ] || [ -z "$QUERY" ]; then
+ echo "Usage: run_query_in_pod [query_type]"
+ exit 1
+fi
+
+# Generate unique names
+TIMESTAMP=$(date +%s)
+RANDOM_ID=$(uuidgen | tr '[:upper:]' '[:lower:]' | tr -d '-' | cut -c1-8)
+POD_NAME="psql-client-${TIMESTAMP}-${RANDOM_ID}"
+SECRET_NAME="psql-client-secret-${TIMESTAMP}-${RANDOM_ID}"
+CONFIGMAP_NAME="psql-client-query-${TIMESTAMP}-${RANDOM_ID}"
+
+# Clean up function (only deletes secret and configmap, leaves pod for debugging on failure)
+cleanup() {
+ kubectl delete pod $POD_NAME -n postgres-db --ignore-not-found=true 2>/dev/null || true
+ kubectl delete secret $SECRET_NAME -n postgres-db --ignore-not-found=true 2>/dev/null || true
+ kubectl delete configmap $CONFIGMAP_NAME -n postgres-db --ignore-not-found=true 2>/dev/null || true
+ rm -f /tmp/${POD_NAME}-pod.yaml 2>/dev/null || true
+ rm -f /tmp/${POD_NAME}-query.sql 2>/dev/null || true
+}
+trap cleanup EXIT
+
+# Create temporary SQL file
+SQL_FILE="/tmp/${POD_NAME}-query.sql"
+echo "$QUERY" > "$SQL_FILE"
+
+# Create ConfigMap with the SQL query
+echo "Creating ConfigMap with SQL query..."
+kubectl create configmap $CONFIGMAP_NAME -n postgres-db --from-file=query.sql="$SQL_FILE"
+
+# Create secret with database credentials
+echo "Creating Secret with database credentials..."
+kubectl create secret generic $SECRET_NAME -n postgres-db \
+ --from-literal=hostname="$HOSTNAME" \
+ --from-literal=port="$PORT" \
+ --from-literal=dbname="$DBNAME" \
+ --from-literal=username="$USERNAME" \
+ --from-literal=password="$PASSWORD"
+
+# Prepare psql command flags based on query type
+if [ "$QUERY_TYPE" = "dml" ]; then
+ PSQL_FLAGS="-t -A -F','"
+else
+ PSQL_FLAGS=""
+fi
+
+# Create pod YAML manifest
+cat > /tmp/${POD_NAME}-pod.yaml << EOF
+apiVersion: v1
+kind: Pod
+metadata:
+ name: $POD_NAME
+ namespace: postgres-db
+spec:
+ restartPolicy: Never
+ containers:
+ - name: psql-client
+ image: bitnami/postgresql:latest
+ command: ["/bin/bash", "-c"]
+ args:
+ - |
+ PGPASSWORD="\$DB_PASSWORD" psql -h "\$DB_HOSTNAME" -p "\$DB_PORT" -U "\$DB_USERNAME" -d "\$DB_DBNAME" $PSQL_FLAGS -f /sql/query.sql
+ env:
+ - name: DB_HOSTNAME
+ valueFrom:
+ secretKeyRef:
+ name: $SECRET_NAME
+ key: hostname
+ - name: DB_PORT
+ valueFrom:
+ secretKeyRef:
+ name: $SECRET_NAME
+ key: port
+ - name: DB_DBNAME
+ valueFrom:
+ secretKeyRef:
+ name: $SECRET_NAME
+ key: dbname
+ - name: DB_USERNAME
+ valueFrom:
+ secretKeyRef:
+ name: $SECRET_NAME
+ key: username
+ - name: DB_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: $SECRET_NAME
+ key: password
+ volumeMounts:
+ - name: sql-query
+ mountPath: /sql
+ readOnly: true
+ volumes:
+ - name: sql-query
+ configMap:
+ name: $CONFIGMAP_NAME
+EOF
+
+# Apply the pod manifest
+echo "Creating pod $POD_NAME..."
+kubectl apply -f /tmp/${POD_NAME}-pod.yaml
+
+POD_STATUS=$(kubectl get pod $POD_NAME -n postgres-db -o jsonpath='{.status.phase}' 2>/dev/null || echo "NotFound")
+MAX_RETRIES=30
+RETRY_COUNT=0
+while [[ "$POD_STATUS" == "Pending" || "$POD_STATUS" == "ContainerCreating" || "$POD_STATUS" == "Running" ]] && [[ $RETRY_COUNT -lt $MAX_RETRIES ]]; do
+ echo "Pod is still pending or creating, waiting..."
+ sleep 5
+ POD_STATUS=$(kubectl get pod $POD_NAME -n postgres-db -o jsonpath='{.status.phase}' 2>/dev/null || echo "NotFound")
+ ((++RETRY_COUNT)) || true
+done
+echo "Pod status: $POD_STATUS"
+
+# Show logs
+echo "Pod logs:"
+export QUERY_LOGS=$(kubectl logs $POD_NAME -n postgres-db --follow || true)
+
+# Check final status
+
+
+if [[ "$QUERY_OUTPUT_FILE" != "" ]]; then
+ echo $QUERY_LOGS > $QUERY_OUTPUT_FILE
+fi
+
+
+
+
+if [ "$POD_STATUS" = "Succeeded" ]; then
+ exit 0
+else
+ echo "Query execution failed or pod did not complete successfully."
+ exit 1;
+fi
\ No newline at end of file
diff --git a/postgres/k8s/postgres-db/service/create-postgres-db b/postgres/k8s/postgres-db/service/create-postgres-db
new file mode 100755
index 0000000..87c2ec2
--- /dev/null
+++ b/postgres/k8s/postgres-db/service/create-postgres-db
@@ -0,0 +1,46 @@
+#!/bin/bash
+set -e
+export WORKING_DIRECTORY_ORIGINAL="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd $WORKING_DIRECTORY_ORIGINAL
+
+source ./handle-helm.sh
+
+# Wait for PostgreSQL service to be ready
+SERVICE_IP=""
+MAX_RETRIES=60
+RETRY_COUNT=0
+
+echo "Waiting for PostgreSQL service to be ready..."
+while [[ -z "$SERVICE_IP" && $RETRY_COUNT -lt $MAX_RETRIES ]]; do
+ SERVICE_IP=$(kubectl get svc $PROJECT-postgres -n postgres-db -o json 2>/dev/null | jq ".spec.clusterIP" -r 2>/dev/null || true)
+ if [[ -z "$SERVICE_IP" || "$SERVICE_IP" == "null" ]]; then
+ echo "Waiting for service IP... (attempt $((RETRY_COUNT + 1))/$MAX_RETRIES)"
+ sleep 10
+ ((RETRY_COUNT++))
+ fi
+done
+
+if [[ -z "$SERVICE_IP" || "$SERVICE_IP" == "null" ]]; then
+ echo "Failed to get PostgreSQL service IP after $MAX_RETRIES attempts"
+ exit 1
+fi
+
+# Wait for PostgreSQL pod to be ready
+echo "Waiting for PostgreSQL pod to be ready..."
+kubectl wait --for=condition=ready pod -l app.kubernetes.io/instance=$PROJECT-postgres -n postgres-db --timeout=300s || true
+
+# Get database name and generate secret name
+DB_NAME="${ACTION_PARAMETERS_USAGE_TYPE:-app}_db"
+SECRET_NAME="$PROJECT-postgres-credentials"
+HELM_RELEASE_NAME="$PROJECT-postgres"
+
+# Construct response with connection details
+JSON_RESPONSE=$(echo $NP_ACTION_CONTEXT | jq "{
+ hostname: \"$SERVICE_IP\",
+ port: 5432,
+ dbname: \"$DB_NAME\",
+ helm_release_name: \"$HELM_RELEASE_NAME\",
+ k8s_secret_name: \"$SECRET_NAME\"
+}")
+
+np service action update --results "$JSON_RESPONSE"
\ No newline at end of file
diff --git a/postgres/k8s/postgres-db/service/delete-postgres-db b/postgres/k8s/postgres-db/service/delete-postgres-db
new file mode 100755
index 0000000..0d01b58
--- /dev/null
+++ b/postgres/k8s/postgres-db/service/delete-postgres-db
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+set -e
+export WORKING_DIRECTORY_ORIGINAL="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd $WORKING_DIRECTORY_ORIGINAL
+
+source ./project.sh
+source ./ensure_helm_deps.sh
+
+echo "Deleting PostgreSQL service..."
+
+# Uninstall Helm chart
+helm uninstall $PROJECT-postgres -n postgres-db || true
+
+# Delete specific persistent volume claims for this PostgreSQL instance
+echo "Deleting persistent volume claims for $PROJECT-postgres..."
+kubectl delete pvc data-$PROJECT-postgres-0 -n postgres-db --ignore-not-found=true
+
+# Get the specific PV that was bound to our PVC and delete it
+echo "Cleaning up associated persistent volume..."
+PV_NAME=$(kubectl get pv -o json | jq -r '.items[] | select(.spec.claimRef.name == "data-'$PROJECT'-postgres-0" and .spec.claimRef.namespace == "postgres-db") | .metadata.name' 2>/dev/null || true)
+if [ -n "$PV_NAME" ]; then
+ echo "Deleting persistent volume: $PV_NAME"
+ kubectl delete pv $PV_NAME --ignore-not-found=true || true
+fi
+
+# Delete the secret
+kubectl delete secret $PROJECT-postgres-credentials -n postgres-db --ignore-not-found=true
+
+echo "PostgreSQL service and volumes deleted successfully"
\ No newline at end of file
diff --git a/postgres/k8s/postgres-db/service/ensure_helm_deps.sh b/postgres/k8s/postgres-db/service/ensure_helm_deps.sh
new file mode 100644
index 0000000..b249f93
--- /dev/null
+++ b/postgres/k8s/postgres-db/service/ensure_helm_deps.sh
@@ -0,0 +1,47 @@
+#!/bin/bash
+
+# Ensure curl is available (needed for binary downloads)
+if ! command -v curl &>/dev/null; then
+ apk add --no-cache curl
+fi
+
+# Ensure openssl is installed (used for password generation)
+if ! command -v openssl &>/dev/null; then
+ apk add --no-cache openssl
+fi
+
+# Ensure jq is installed (used for JSON processing)
+if ! command -v jq &>/dev/null; then
+ apk add --no-cache jq
+fi
+
+# Ensure kubectl is installed
+if ! command -v kubectl &>/dev/null; then
+ apk add --no-cache kubectl 2>/dev/null || {
+ KUBECTL_VERSION=$(curl -fsSL https://dl.k8s.io/release/stable.txt)
+ curl -fsSL -o /usr/local/bin/kubectl \
+ "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
+ chmod +x /usr/local/bin/kubectl
+ }
+fi
+
+# Ensure helm is installed
+if ! command -v helm &>/dev/null; then
+ apk add --no-cache helm 2>/dev/null || {
+ HELM_VERSION="v3.17.3"
+ curl -fsSL -o /tmp/helm.tar.gz \
+ "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
+ tar -xzf /tmp/helm.tar.gz -C /tmp
+ mv /tmp/linux-amd64/helm /usr/local/bin/helm
+ chmod +x /usr/local/bin/helm
+ rm -rf /tmp/helm.tar.gz /tmp/linux-amd64
+ }
+fi
+
+# Ensure gomplate is installed (not available in apk repos)
+if ! command -v gomplate &>/dev/null; then
+ GOMPLATE_VERSION="v3.11.7"
+ curl -fsSL -o /usr/local/bin/gomplate \
+ "https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_linux-amd64"
+ chmod +x /usr/local/bin/gomplate
+fi
diff --git a/postgres/k8s/postgres-db/service/handle-helm.sh b/postgres/k8s/postgres-db/service/handle-helm.sh
new file mode 100755
index 0000000..740f7b4
--- /dev/null
+++ b/postgres/k8s/postgres-db/service/handle-helm.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+export WORKING_DIRECTORY_ORIGINAL="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd $WORKING_DIRECTORY_ORIGINAL
+
+source ./project.sh
+source ./ensure_helm_deps.sh
+
+# Add Bitnami Helm repo if not already added
+helm repo add bitnami https://charts.bitnami.com/bitnami || true
+helm repo update
+
+# Get parameters
+USAGE_TYPE=$ACTION_PARAMETERS_USAGE_TYPE
+PII_ENABLED=${ACTION_PARAMETERS_PII:-false}
+DB_NAME="${USAGE_TYPE:-app}_db"
+
+# Generate random password for PostgreSQL superuser
+POSTGRES_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-25)
+
+# Create Kubernetes secret for PostgreSQL credentials
+kubectl create namespace postgres-db --dry-run=client -o yaml | kubectl apply -f -
+
+# Create secret with database credentials
+kubectl create secret generic $PROJECT-postgres-credentials \
+ --from-literal=postgres-password="$POSTGRES_PASSWORD" \
+ --from-literal=username=postgres \
+ --from-literal=password="$POSTGRES_PASSWORD" \
+ --from-literal=database="$DB_NAME" \
+ -n postgres-db \
+ --dry-run=client -o yaml | kubectl apply -f -
+
+echo '{"projectName":"'"$PROJECT"'","usageType":"'"$USAGE_TYPE"'","piiEnabled":'"$PII_ENABLED"',"dbName":"'"$DB_NAME"'","postgresPassword":"'"$POSTGRES_PASSWORD"'"}' > /tmp/context-$PROJECT.json
+
+gomplate \
+ --context .=/tmp/context-$PROJECT.json \
+ -f values.yaml.tpl > /tmp/values-$PROJECT.yaml
+
+# Install using Bitnami PostgreSQL chart
+helm upgrade --install -n postgres-db $PROJECT-postgres bitnami/postgresql -f /tmp/values-$PROJECT.yaml --create-namespace > /dev/null
\ No newline at end of file
diff --git a/postgres/k8s/postgres-db/service/project.sh b/postgres/k8s/postgres-db/service/project.sh
new file mode 100755
index 0000000..8bc20a6
--- /dev/null
+++ b/postgres/k8s/postgres-db/service/project.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+export PROJECT=$ACTION_SERVICE_SLUG-$ACTION_TAGS_APPLICATION_ID
diff --git a/postgres/k8s/postgres-db/service/run-ddl-query b/postgres/k8s/postgres-db/service/run-ddl-query
new file mode 100755
index 0000000..8b3541d
--- /dev/null
+++ b/postgres/k8s/postgres-db/service/run-ddl-query
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+set -e
+export WORKING_DIRECTORY_ORIGINAL="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd $WORKING_DIRECTORY_ORIGINAL
+
+# Get service connection details
+SERVICE_HOSTNAME=$ACTION_SERVICE_ATTRIBUTES_HOSTNAME
+SERVICE_PORT=$ACTION_SERVICE_ATTRIBUTES_PORT
+SERVICE_DBNAME=$ACTION_SERVICE_ATTRIBUTES_DBNAME
+SECRET_NAME=$ACTION_SERVICE_ATTRIBUTES_K_8_S_SECRET_NAME
+
+# Re export due encoding
+eval "echo $(np service-action export-action-data --format bash --bash-prefix ACTION)"
+# Get query parameter
+QUERY="$ACTION_PARAMETERS_QUERY"
+# Validate that this is a DDL query
+if ! echo "$QUERY" | grep -iE "^\s*(CREATE|ALTER|DROP|TRUNCATE)" > /dev/null; then
+ ERROR_MSG="Only DDL queries (CREATE, ALTER, DROP, TRUNCATE) are allowed in this action. Use run-dml-query for SELECT, INSERT, UPDATE, DELETE."
+ JSON_RESPONSE='{"error": "'$ERROR_MSG'", "results": ""}'
+ np service action update --results "$JSON_RESPONSE"
+ exit 1
+fi
+
+# Get admin credentials from K8s secret
+ADMIN_PASSWORD=$(kubectl get secret $SECRET_NAME -n postgres-db -o json | jq -r '.data["postgres-password"]' | base64 -d)
+
+# Execute the DDL query using PostgreSQL client pod
+ERROR_MSG=""
+RESULT_MSG=""
+OUTPUT_FILE="/tmp/ddl_query_output-$ACTION_ID.txt"
+../run_query_in_pod.sh "$SERVICE_HOSTNAME" "$SERVICE_PORT" "$SERVICE_DBNAME" "postgres" "$ADMIN_PASSWORD" "$QUERY" "ddl" "$OUTPUT_FILE"
+ENCODED_OUTPUT=$(printf '%s' "$(cat $OUTPUT_FILE)" | jq -Rs .)
+JSON_RESPONSE="{\"results\": $ENCODED_OUTPUT}"
+np service action update --results "$JSON_RESPONSE"
\ No newline at end of file
diff --git a/postgres/k8s/postgres-db/service/run-dml-query b/postgres/k8s/postgres-db/service/run-dml-query
new file mode 100755
index 0000000..d1bedb8
--- /dev/null
+++ b/postgres/k8s/postgres-db/service/run-dml-query
@@ -0,0 +1,47 @@
+#!/bin/bash
+
+set -e
+export WORKING_DIRECTORY_ORIGINAL="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd $WORKING_DIRECTORY_ORIGINAL
+
+# Get service connection details
+SERVICE_HOSTNAME=$ACTION_SERVICE_ATTRIBUTES_HOSTNAME
+SERVICE_PORT=$ACTION_SERVICE_ATTRIBUTES_PORT
+SERVICE_DBNAME=$ACTION_SERVICE_ATTRIBUTES_DBNAME
+SECRET_NAME=$ACTION_SERVICE_ATTRIBUTES_K_8_S_SECRET_NAME
+
+# Re export due encoding
+eval "echo $(np service-action export-action-data --format bash --bash-prefix ACTION)"
+# Get query parameter
+QUERY="$ACTION_PARAMETERS_QUERY"
+
+# Validate that this is a DML query (not DDL)
+if echo "$QUERY" | grep -iE "^\s*(CREATE|ALTER|DROP|TRUNCATE)" > /dev/null; then
+ ERROR_MSG="DDL queries are not allowed in DML action. Use run-ddl-query instead."
+ JSON_RESPONSE='{"error": "'$ERROR_MSG'"}'
+ np service action update --results "$JSON_RESPONSE"
+ exit 1
+fi
+
+# validate if query is a select to wrap it as json
+if echo "$QUERY" | grep -iE "^\s*SELECT" > /dev/null; then
+ QUERY="SELECT json_agg(row_to_json(t)) FROM ($QUERY) t;"
+ export is_select_query=true
+fi
+
+# Get admin credentials from K8s secret
+ADMIN_PASSWORD=$(kubectl get secret $SECRET_NAME -n postgres-db -o json | jq -r '.data["postgres-password"]' | base64 -d)
+
+# Execute the DDL query using PostgreSQL client pod
+ERROR_MSG=""
+RESULT_MSG=""
+OUTPUT_FILE="/tmp/ddl_query_output-$ACTION_ID.txt"
+../run_query_in_pod.sh "$SERVICE_HOSTNAME" "$SERVICE_PORT" "$SERVICE_DBNAME" "postgres" "$ADMIN_PASSWORD" "$QUERY" "dml" "$OUTPUT_FILE"
+if [[ "$is_select_query" == "true" ]]; then
+ JSON=$(cat $OUTPUT_FILE | jq -r)
+ JSON_RESPONSE="{\"results\": $JSON}"
+else
+ ENCODED_OUTPUT=$(printf '%s' "$(cat $OUTPUT_FILE)" | jq -Rs .)
+ JSON_RESPONSE="{\"textresult\": $ENCODED_OUTPUT}"
+fi
+np service action update --results "$JSON_RESPONSE"
\ No newline at end of file
diff --git a/postgres/k8s/postgres-db/service/update-postgres-db b/postgres/k8s/postgres-db/service/update-postgres-db
new file mode 100755
index 0000000..fe14840
--- /dev/null
+++ b/postgres/k8s/postgres-db/service/update-postgres-db
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+set -e
+export WORKING_DIRECTORY_ORIGINAL="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd $WORKING_DIRECTORY_ORIGINAL
+
+# For updates, we just re-run the Helm deployment with new values
+source ./handle-helm.sh
+
+echo "PostgreSQL service updated successfully"
\ No newline at end of file
diff --git a/postgres/k8s/postgres-db/service/values.yaml.tpl b/postgres/k8s/postgres-db/service/values.yaml.tpl
new file mode 100755
index 0000000..d569f10
--- /dev/null
+++ b/postgres/k8s/postgres-db/service/values.yaml.tpl
@@ -0,0 +1,49 @@
+# Bitnami PostgreSQL Helm chart values
+nameOverride: "{{ .projectName }}-postgres"
+fullnameOverride: "{{ .projectName }}-postgres"
+
+# Use existing secret for authentication
+auth:
+ existingSecret: "{{ .projectName }}-postgres-credentials"
+ secretKeys:
+ adminPasswordKey: "postgres-password"
+ userPasswordKey: "password"
+ database: "{{ .dbName }}"
+ username: "postgres"
+
+# Persistence configuration
+primary:
+ persistence:
+ enabled: true
+ size: 10Gi
+
+ resources:
+ limits:
+ cpu: 500m
+ memory: 512Mi
+ requests:
+ cpu: 100m
+ memory: 256Mi
+
+{{- if .piiEnabled }}
+ securityContext:
+ enabled: true
+ runAsNonRoot: true
+ runAsUser: 1001
+ fsGroup: 1001
+{{- end }}
+
+# Service configuration
+service:
+ type: ClusterIP
+ ports:
+ postgresql: 5432
+
+# Common labels
+commonLabels:
+ usage-type: "{{ .usageType }}"
+ pii-enabled: "{{ .piiEnabled }}"
+
+# Metrics (optional)
+metrics:
+ enabled: false
\ No newline at end of file
diff --git a/postgres/k8s/specs/actions/run-ddl-query.json.tpl b/postgres/k8s/specs/actions/run-ddl-query.json.tpl
new file mode 100644
index 0000000..a5a2c78
--- /dev/null
+++ b/postgres/k8s/specs/actions/run-ddl-query.json.tpl
@@ -0,0 +1,48 @@
+{
+ "name": "Run DDL Query",
+ "slug": "run-ddl-query",
+ "type": "custom",
+ "annotations": {},
+ "retryable": false,
+ "parameters": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "query"
+ ],
+ "uiSchema": {
+ "type": "VerticalLayout",
+ "elements": [
+ {
+ "type": "Control",
+ "scope": "#/properties/query",
+ "options": {
+ "multi": true
+ }
+ }
+ ]
+ },
+ "properties": {
+ "query": {
+ "type": "string"
+ }
+ }
+ },
+ "values": {}
+ },
+ "results": {
+ "schema": {
+ "type": "object",
+ "required": [],
+ "properties": {
+ "error": {
+ "type": "string"
+ },
+ "results": {
+ "type": "string"
+ }
+ }
+ },
+ "values": {}
+ }
+}
\ No newline at end of file
diff --git a/postgres/k8s/specs/actions/run-dml-query.json.tpl b/postgres/k8s/specs/actions/run-dml-query.json.tpl
new file mode 100644
index 0000000..90225f2
--- /dev/null
+++ b/postgres/k8s/specs/actions/run-dml-query.json.tpl
@@ -0,0 +1,54 @@
+{
+ "name": "Run DML Query",
+ "slug": "run-dml-query",
+ "type": "custom",
+ "annotations": {},
+ "retryable": false,
+ "parameters": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "query"
+ ],
+ "uiSchema": {
+ "type": "VerticalLayout",
+ "elements": [
+ {
+ "type": "Control",
+ "scope": "#/properties/query",
+ "options": {
+ "multi": true
+ }
+ }
+ ]
+ },
+ "properties": {
+ "query": {
+ "type": "string"
+ }
+ }
+ },
+ "values": {}
+ },
+ "results": {
+ "schema": {
+ "type": "object",
+ "required": [],
+ "properties": {
+ "error": {
+ "type": "string"
+ },
+ "results": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ },
+ "textresult": {
+ "type": "string"
+ }
+ }
+ },
+ "values": {}
+ }
+}
\ No newline at end of file
diff --git a/postgres/k8s/specs/links/database-user.json.tpl b/postgres/k8s/specs/links/database-user.json.tpl
new file mode 100644
index 0000000..cbb8595
--- /dev/null
+++ b/postgres/k8s/specs/links/database-user.json.tpl
@@ -0,0 +1,66 @@
+{
+ "name": "database-user",
+ "slug": "database-user",
+ "visible_to": [],
+ "unique": false,
+ "dimensions": {},
+ "assignable_to": "any",
+ "use_default_actions": true,
+ "attributes": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "permisions"
+ ],
+ "properties": {
+ "password": {
+ "type": "string",
+ "export": {
+ "type": "environment_variable",
+ "secret": true
+ },
+ "secret": true,
+ "visibleOn": [
+ "read"
+ ],
+ "editableOn": []
+ },
+ "username": {
+ "type": "string",
+ "export": true,
+ "visibleOn": [
+ "read"
+ ],
+ "editableOn": []
+ },
+ "permisions": {
+ "type": "object",
+ "properties": {
+ "read": {
+ "type": "boolean",
+ "default": true,
+ "description": "User will have read permisions"
+ },
+ "admin": {
+ "type": "boolean",
+ "default": false,
+ "description": "User will have DDL permisions"
+ },
+ "write": {
+ "type": "boolean",
+ "default": false,
+ "description": "User will have write permisions"
+ }
+ }
+ }
+ }
+ },
+ "values": {}
+ },
+ "selectors": {
+ "category": "any",
+ "imported": false,
+ "provider": "any",
+ "sub_category": "any"
+ }
+}
\ No newline at end of file
diff --git a/postgres/k8s/specs/service-spec.json.tpl b/postgres/k8s/specs/service-spec.json.tpl
new file mode 100644
index 0000000..1894f50
--- /dev/null
+++ b/postgres/k8s/specs/service-spec.json.tpl
@@ -0,0 +1,90 @@
+{
+ "name": "Postgres DB",
+ "slug": "postgres-db",
+ "type": "dependency",
+ "use_default_actions": true,
+ "available_actions": [
+ "run-ddl-query",
+ "run-dml-query"
+ ],
+ "available_links": [
+ "database-user"
+ ],
+ "agent_command":{
+ "data": {
+ "cmdline": "nullplatform/services/databases/postgres/k8s/handle-service-agent",
+ "environment": {
+ "NP_ACTION_CONTEXT": "${NOTIFICATION_CONTEXT}"
+ }
+ },
+ "type": "exec"
+ },
+ "attributes": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "usage_type",
+ "pii"
+ ],
+ "properties": {
+ "pii": {
+ "type": "boolean",
+ "default": false,
+ "description": "Will you store personal user information (email, name, id, etc)?"
+ },
+ "port": {
+ "type": "number",
+ "export": true,
+ "visibleOn": [
+ "read"
+ ],
+ "editableOn": []
+ },
+ "dbname": {
+ "type": "string",
+ "export": true,
+ "visibleOn": [
+ "read"
+ ],
+ "editableOn": []
+ },
+ "hostname": {
+ "type": "string",
+ "export": true,
+ "visibleOn": [
+ "read"
+ ],
+ "editableOn": []
+ },
+ "usage_type": {
+ "enum": [
+ "transactions",
+ "cache",
+ "configurations"
+ ],
+ "type": "string",
+ "description": "What this database is used for?"
+ },
+ "k8s_secret_name": {
+ "type": "string",
+ "export": false,
+ "visibleOn": [],
+ "editableOn": []
+ },
+ "helm_release_name": {
+ "type": "string",
+ "export": false,
+ "visibleOn": [],
+ "editableOn": []
+ }
+ }
+ },
+ "values": {}
+ },
+ "selectors": {
+ "category": "Database",
+ "imported": false,
+ "provider": "K8S",
+ "sub_category": "Relational Database"
+ }
+}
diff --git a/tofu-module/main.tf b/tofu-module/main.tf
new file mode 100644
index 0000000..6796a03
--- /dev/null
+++ b/tofu-module/main.tf
@@ -0,0 +1,31 @@
+###############################################################################
+# Service Definition: Postgres DB (K8S)
+###############################################################################
+module "service_definition_postgres_db" {
+ source = "git::https://github.com/nullplatform/tofu-modules.git//nullplatform/service_definition?ref=v1.52.3"
+ nrn = var.nrn
+ git_provider = "local"
+ local_specs_path = "/.np/services/databases/postgres/k8s"
+ service_path = "databases/postgres/k8s"
+ service_name = "Postgres DB"
+ available_actions = ["run-ddl-query", "run-dml-query"]
+ available_links = ["database-user"]
+}
+###############################################################################
+# Service Agent Association: Postgres DB (K8S)
+###############################################################################
+module "service_definition_channel_association_postgres_db" {
+ source = "git::https://github.com/nullplatform/tofu-modules.git//nullplatform/service_definition_agent_association?ref=v1.43.0"
+ nrn = var.nrn
+ api_key = var.np_api_key
+ tags_selectors = var.tags_selectors
+ service_specification_slug = module.service_definition_postgres_db.service_specification_slug
+ agent_command = {
+ type = "exec"
+ data = {
+ cmdline = "nullplatform/services/databases/postgres/k8s/entrypoint/entrypoint"
+ environment = { NP_ACTION_CONTEXT = "$${NOTIFICATION_CONTEXT}" }
+ }
+ }
+ depends_on = [ module.service_definition_postgres_db ]
+}
diff --git a/tofu-module/outputs.tf b/tofu-module/outputs.tf
new file mode 100644
index 0000000..57303e9
--- /dev/null
+++ b/tofu-module/outputs.tf
@@ -0,0 +1,13 @@
+################################################################################
+# Outputs - Postgres DB Service Definition
+################################################################################
+
+output "service_specification_slug_postgres_db" {
+ description = "Slug of the Postgres DB service specification"
+ value = module.service_definition_postgres_db.service_specification_slug
+}
+
+output "service_specification_id_postgres_db" {
+ description = "ID of the Postgres DB service specification"
+ value = module.service_definition_postgres_db.service_specification_id
+}
diff --git a/tofu-module/provider.tf b/tofu-module/provider.tf
new file mode 100644
index 0000000..9831451
--- /dev/null
+++ b/tofu-module/provider.tf
@@ -0,0 +1,12 @@
+terraform {
+ required_providers {
+ nullplatform = {
+ source = "nullplatform/nullplatform"
+ version = "~> 0.0.75"
+ }
+ }
+}
+
+provider "nullplatform" {
+ api_key = var.np_api_key
+}
diff --git a/tofu-module/variables.tf b/tofu-module/variables.tf
new file mode 100644
index 0000000..873b032
--- /dev/null
+++ b/tofu-module/variables.tf
@@ -0,0 +1,40 @@
+################################################################################
+# Nullplatform Configuration
+################################################################################
+
+variable "nrn" {
+ description = "Nullplatform Resource Name - Unique identifier for Nullplatform resources"
+ type = string
+}
+
+variable "np_api_key" {
+ description = "API key for authenticating with the Nullplatform API"
+ type = string
+ sensitive = true
+}
+
+variable "tags_selectors" {
+ description = "Map of tags used to select and filter channels and agents"
+ type = map(string)
+}
+
+
+################################################################################
+# Nullplatform Configuration
+################################################################################
+
+variable "nrn" {
+ description = "Nullplatform Resource Name - Unique identifier for Nullplatform resources"
+ type = string
+}
+
+variable "np_api_key" {
+ description = "API key for authenticating with the Nullplatform API"
+ type = string
+ sensitive = true
+}
+
+variable "tags_selectors" {
+ description = "Map of tags used to select and filter channels and agents"
+ type = map(string)
+}