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 - -
-
- 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) +}