From 7924c48c550a4a5ff167e64d9bd1197d94f0d964 Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Tue, 17 Mar 2026 13:17:49 -0700 Subject: [PATCH 01/15] reintroduce managed db --- charts/agentregistry/templates/_helpers.tpl | 106 ++++++-- .../agentregistry/templates/deployment.yaml | 13 + .../agentregistry/templates/postgresql.yaml | 145 +++++++++++ charts/agentregistry/templates/secrets.yaml | 14 +- .../agentregistry/tests/deployment_test.yaml | 53 +++- .../agentregistry/tests/postgresql_test.yaml | 245 ++++++++++++++++++ charts/agentregistry/tests/secrets_test.yaml | 44 +++- .../agentregistry/tests/validation_test.yaml | 60 +++-- charts/agentregistry/values.yaml | 120 +++++++-- 9 files changed, 725 insertions(+), 75 deletions(-) create mode 100644 charts/agentregistry/templates/postgresql.yaml create mode 100644 charts/agentregistry/tests/postgresql_test.yaml diff --git a/charts/agentregistry/templates/_helpers.tpl b/charts/agentregistry/templates/_helpers.tpl index c77d3f76..931ea11d 100644 --- a/charts/agentregistry/templates/_helpers.tpl +++ b/charts/agentregistry/templates/_helpers.tpl @@ -149,10 +149,11 @@ Priority: global.existingSecret → config.existingSecret → chart-managed secr {{/* Return the secret name that holds POSTGRES_PASSWORD. -Priority: global.existingSecret → database.existingSecret → chart-managed secret. +Priority: global.existingSecret → database.external.existingSecret → chart-managed secret. +When database.bundled.enabled, external.existingSecret is not meaningful; the chart-managed secret holds the bundled password. */}} {{- define "agentregistry.passwordSecretName" -}} -{{- .Values.global.existingSecret | default .Values.database.existingSecret | default (include "agentregistry.fullname" .) }} +{{- .Values.global.existingSecret | default .Values.database.external.existingSecret | default (include "agentregistry.fullname" .) }} {{- end }} {{/* ====================================================================== @@ -161,20 +162,28 @@ Priority: global.existingSecret → database.existingSecret → chart-managed se {{/* Return the PostgreSQL database URL. -If database.url is set, use it directly. -Otherwise build from individual fields, injecting POSTGRES_PASSWORD at runtime. +When database.bundled.enabled, points at the in-chart PostgreSQL service. +Otherwise: if database.external.url is set, use it directly; else build from individual external fields. */}} {{- define "agentregistry.databaseUrl" -}} -{{- if .Values.database.url }} -{{- .Values.database.url }} +{{- if .Values.database.bundled.enabled }} +{{- printf "postgres://%s:$(%s)@%s:%s/%s?sslmode=%s" + .Values.database.bundled.auth.username + "POSTGRES_PASSWORD" + (include "agentregistry.postgresql.fullname" .) + (toString .Values.database.bundled.service.port) + .Values.database.bundled.auth.database + .Values.database.bundled.sslMode }} +{{- else if .Values.database.external.url }} +{{- .Values.database.external.url }} {{- else }} {{- printf "postgres://%s:$(%s)@%s:%s/%s?sslmode=%s" - .Values.database.username + .Values.database.external.username "POSTGRES_PASSWORD" - .Values.database.host - (toString .Values.database.port) - .Values.database.database - .Values.database.sslMode }} + .Values.database.external.host + (toString .Values.database.external.port) + .Values.database.external.database + .Values.database.external.sslMode }} {{- end }} {{- end }} @@ -372,6 +381,70 @@ If .Values.affinity is set it wins entirely. Otherwise build from presets. {{- end }} {{- end }} +{{/* ====================================================================== + Bundled PostgreSQL helpers + ====================================================================== */}} + +{{/* +Full name for the bundled PostgreSQL resources. +*/}} +{{- define "agentregistry.postgresql.fullname" -}} +{{- printf "%s-postgresql" (include "agentregistry.fullname" .) | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Standard labels for bundled PostgreSQL resources. +*/}} +{{- define "agentregistry.postgresql.labels" -}} +helm.sh/chart: {{ include "agentregistry.chart" . }} +{{ include "agentregistry.postgresql.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: {{ include "agentregistry.name" . }} +{{- if .Values.commonLabels }} +{{ toYaml .Values.commonLabels }} +{{- end }} +{{- end }} + +{{/* +Selector labels for bundled PostgreSQL resources. +*/}} +{{- define "agentregistry.postgresql.selectorLabels" -}} +app.kubernetes.io/name: {{ include "agentregistry.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/component: database +{{- end }} + +{{/* +Return the bundled PostgreSQL image string. +Respects global.imageRegistry override. +*/}} +{{- define "agentregistry.postgresql.image" -}} +{{- $registry := .Values.database.bundled.image.registry -}} +{{- if .Values.global }} + {{- if .Values.global.imageRegistry }} + {{- $registry = .Values.global.imageRegistry -}} + {{- end }} +{{- end }} +{{- printf "%s/%s:%s" $registry .Values.database.bundled.image.repository .Values.database.bundled.image.tag }} +{{- end }} + +{{/* +Resolve the storageClassName for the bundled PostgreSQL PVC. +Prefers global.storageClass over database.bundled.persistence.storageClass. +*/}} +{{- define "agentregistry.storageClass" -}} +{{- $sc := .Values.database.bundled.persistence.storageClass -}} +{{- if .Values.global }} + {{- if .Values.global.storageClass }} + {{- $sc = .Values.global.storageClass -}} + {{- end }} +{{- end }} +{{- $sc }} +{{- end }} + {{/* ====================================================================== Validation ====================================================================== */}} @@ -379,6 +452,7 @@ If .Values.affinity is set it wins entirely. Otherwise build from presets. {{/* Compile hard-error validations. Any non-empty result triggers fail. Called from templates/validate.yaml so it fires during helm template/install. +When database.bundled.enabled, external database host/password validation is skipped. */}} {{- define "agentregistry.validateValues.errors" -}} {{- $errors := list }} @@ -388,11 +462,13 @@ Called from templates/validate.yaml so it fires during helm template/install. {{- else if and (not $hasExternalJwt) (not (regexMatch "^[0-9a-fA-F]+$" .Values.config.jwtPrivateKey)) }} {{- $errors = append $errors "config.jwtPrivateKey must be a valid hex string (e.g. generated with: openssl rand -hex 32)." }} {{- end }} -{{- if and (not (or .Values.global.existingSecret .Values.database.existingSecret)) (not .Values.database.url) (eq .Values.database.password "") }} -{{- $errors = append $errors "database.password must be set (or provide database.url, database.existingSecret, or global.existingSecret containing POSTGRES_PASSWORD)." }} +{{- if not .Values.database.bundled.enabled }} +{{- if and (not (or .Values.global.existingSecret .Values.database.external.existingSecret)) (not .Values.database.external.url) (eq .Values.database.external.password "") }} +{{- $errors = append $errors "database.external.password must be set (or provide database.external.url, database.external.existingSecret, or global.existingSecret containing POSTGRES_PASSWORD)." }} +{{- end }} +{{- if and (not .Values.database.external.url) (not .Values.database.external.host) }} +{{- $errors = append $errors "database.external.host (or database.external.url) must be set when database.bundled.enabled=false. An external PostgreSQL instance with pgvector is required." }} {{- end }} -{{- if and (not .Values.database.url) (not .Values.database.host) }} -{{- $errors = append $errors "database.host (or database.url) must be set. An external PostgreSQL instance with pgvector is required." }} {{- end }} {{- range $errors }} {{ . }} diff --git a/charts/agentregistry/templates/deployment.yaml b/charts/agentregistry/templates/deployment.yaml index 2c37518e..a9819768 100644 --- a/charts/agentregistry/templates/deployment.yaml +++ b/charts/agentregistry/templates/deployment.yaml @@ -84,6 +84,19 @@ spec: topologySpreadConstraints: {{- toYaml .Values.topologySpreadConstraints | nindent 8 }} {{- end }} + {{- if .Values.database.bundled.enabled }} + initContainers: + - name: wait-for-postgres + image: {{ include "agentregistry.postgresql.image" . }} + imagePullPolicy: {{ .Values.database.bundled.image.pullPolicy }} + command: + - sh + - -c + - | + until pg_isready -h {{ include "agentregistry.postgresql.fullname" . }} -p {{ .Values.database.bundled.service.port }} -U {{ .Values.database.bundled.auth.username | quote }}; do + echo "waiting for postgres..."; sleep 2 + done + {{- end }} containers: - name: agentregistry image: {{ include "agentregistry.image" . }} diff --git a/charts/agentregistry/templates/postgresql.yaml b/charts/agentregistry/templates/postgresql.yaml new file mode 100644 index 00000000..04699416 --- /dev/null +++ b/charts/agentregistry/templates/postgresql.yaml @@ -0,0 +1,145 @@ +{{- if .Values.database.bundled.enabled }} +{{- $pg := .Values.database.bundled }} +{{- $fullname := include "agentregistry.postgresql.fullname" . }} +--- +{{- if and $pg.persistence.enabled (not $pg.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ $fullname }} + labels: + {{- include "agentregistry.postgresql.labels" . | nindent 4 }} + {{- $annotations := include "agentregistry.annotations" (dict "annotations" dict "context" $) }} + {{- if $annotations }} + annotations: + {{- $annotations | nindent 4 }} + {{- end }} +spec: + accessModes: + {{- toYaml $pg.persistence.accessModes | nindent 4 }} + resources: + requests: + storage: {{ $pg.persistence.size }} + {{- $sc := include "agentregistry.storageClass" . | trim }} + {{- if $sc }} + storageClassName: {{ $sc | quote }} + {{- end }} +--- +{{- end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $fullname }} + labels: + {{- include "agentregistry.postgresql.labels" . | nindent 4 }} + {{- $annotations := include "agentregistry.annotations" (dict "annotations" dict "context" $) }} + {{- if $annotations }} + annotations: + {{- $annotations | nindent 4 }} + {{- end }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "agentregistry.postgresql.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "agentregistry.postgresql.selectorLabels" . | nindent 8 }} + spec: + {{- $podSec := include "agentregistry.podSecurityContext" $pg.podSecurityContext }} + {{- if $podSec }} + securityContext: + {{- $podSec | nindent 8 }} + {{- end }} + containers: + - name: postgresql + image: {{ include "agentregistry.postgresql.image" . }} + imagePullPolicy: {{ $pg.image.pullPolicy }} + {{- $containerSec := include "agentregistry.containerSecurityContext" $pg.containerSecurityContext }} + {{- if $containerSec }} + securityContext: + {{- $containerSec | nindent 12 }} + {{- end }} + ports: + - name: postgresql + containerPort: 5432 + protocol: TCP + env: + - name: POSTGRES_DB + value: {{ $pg.auth.database | quote }} + - name: POSTGRES_USER + value: {{ $pg.auth.username | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "agentregistry.passwordSecretName" . }} + key: POSTGRES_PASSWORD + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + livenessProbe: + exec: + command: + - pg_isready + - -U + - {{ $pg.auth.username | quote }} + - -d + - {{ $pg.auth.database | quote }} + initialDelaySeconds: {{ $pg.probes.liveness.initialDelaySeconds }} + periodSeconds: {{ $pg.probes.liveness.periodSeconds }} + timeoutSeconds: {{ $pg.probes.liveness.timeoutSeconds }} + failureThreshold: {{ $pg.probes.liveness.failureThreshold }} + successThreshold: {{ $pg.probes.liveness.successThreshold }} + readinessProbe: + exec: + command: + - pg_isready + - -U + - {{ $pg.auth.username | quote }} + - -d + - {{ $pg.auth.database | quote }} + initialDelaySeconds: {{ $pg.probes.readiness.initialDelaySeconds }} + periodSeconds: {{ $pg.probes.readiness.periodSeconds }} + timeoutSeconds: {{ $pg.probes.readiness.timeoutSeconds }} + failureThreshold: {{ $pg.probes.readiness.failureThreshold }} + successThreshold: {{ $pg.probes.readiness.successThreshold }} + {{- $res := include "agentregistry.resources" (dict "resources" $pg.resources "preset" $pg.resourcesPreset) }} + {{- if $res }} + resources: + {{- $res | nindent 12 }} + {{- end }} + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + volumes: + - name: data + {{- if $pg.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ $pg.persistence.existingClaim | default $fullname }} + {{- else }} + emptyDir: {} + {{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $fullname }} + labels: + {{- include "agentregistry.postgresql.labels" . | nindent 4 }} + {{- $annotations := include "agentregistry.annotations" (dict "annotations" dict "context" $) }} + {{- if $annotations }} + annotations: + {{- $annotations | nindent 4 }} + {{- end }} +spec: + type: {{ $pg.service.type }} + ports: + - name: postgresql + port: {{ $pg.service.port }} + targetPort: postgresql + protocol: TCP + selector: + {{- include "agentregistry.postgresql.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/agentregistry/templates/secrets.yaml b/charts/agentregistry/templates/secrets.yaml index 26819bee..4a1fe197 100644 --- a/charts/agentregistry/templates/secrets.yaml +++ b/charts/agentregistry/templates/secrets.yaml @@ -1,4 +1,6 @@ -{{- if and (not .Values.global.existingSecret) (or (not .Values.config.existingSecret) (not .Values.database.existingSecret)) }} +{{- $needPgPassword := and (not .Values.global.existingSecret) (or .Values.database.bundled.enabled (not .Values.database.external.existingSecret)) -}} +{{- $needJwtKey := and (not .Values.global.existingSecret) (not .Values.config.existingSecret) -}} +{{- if or $needPgPassword $needJwtKey }} apiVersion: v1 kind: Secret metadata: @@ -12,10 +14,14 @@ metadata: {{- end }} type: Opaque data: - {{- if not (or .Values.global.existingSecret .Values.database.existingSecret) }} - POSTGRES_PASSWORD: {{ .Values.database.password | toString | b64enc | quote }} + {{- if $needPgPassword }} + {{- if .Values.database.bundled.enabled }} + POSTGRES_PASSWORD: {{ .Values.database.bundled.auth.password | toString | b64enc | quote }} + {{- else }} + POSTGRES_PASSWORD: {{ .Values.database.external.password | toString | b64enc | quote }} {{- end }} - {{- if not (or .Values.global.existingSecret .Values.config.existingSecret) }} + {{- end }} + {{- if $needJwtKey }} AGENT_REGISTRY_JWT_PRIVATE_KEY: {{ .Values.config.jwtPrivateKey | toString | b64enc | quote }} {{- end }} {{- end }} diff --git a/charts/agentregistry/tests/deployment_test.yaml b/charts/agentregistry/tests/deployment_test.yaml index 17254b3b..3ec6a4e1 100644 --- a/charts/agentregistry/tests/deployment_test.yaml +++ b/charts/agentregistry/tests/deployment_test.yaml @@ -34,7 +34,7 @@ tests: # allow either lowercase or uppercase hex and permit extra trailing text pattern: "^ghcr.io/agentregistry-dev/agentregistry/server@sha256:[0-9a-fA-F]{64}" - + - it: sets replicaCount template: deployment.yaml @@ -89,14 +89,24 @@ tests: name: RELEASE-NAME-agentregistry key: AGENT_REGISTRY_JWT_PRIVATE_KEY - - it: sets AGENT_REGISTRY_DATABASE_URL with external host + - it: sets AGENT_REGISTRY_DATABASE_URL pointing at bundled postgres by default + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: AGENT_REGISTRY_DATABASE_URL + value: "postgres://agentregistry:$(POSTGRES_PASSWORD)@RELEASE-NAME-agentregistry-postgresql:5432/agent-registry?sslmode=disable" + + - it: sets AGENT_REGISTRY_DATABASE_URL with external host (bundled disabled) template: deployment.yaml set: - database.host: mydb.example.com - database.port: 5432 - database.database: mydb - database.username: myuser - database.sslMode: require + database.bundled.enabled: false + database.external.host: mydb.example.com + database.external.port: 5432 + database.external.database: mydb + database.external.username: myuser + database.external.sslMode: require asserts: - contains: path: spec.template.spec.containers[0].env @@ -104,10 +114,11 @@ tests: name: AGENT_REGISTRY_DATABASE_URL value: "postgres://myuser:$(POSTGRES_PASSWORD)@mydb.example.com:5432/mydb?sslmode=require" - - it: sets AGENT_REGISTRY_DATABASE_URL with external url + - it: sets AGENT_REGISTRY_DATABASE_URL with external url (bundled disabled) template: deployment.yaml set: - database.url: "postgres://user:pass@host:5432/db?sslmode=disable" + database.bundled.enabled: false + database.external.url: "postgres://user:pass@host:5432/db?sslmode=disable" asserts: - contains: path: spec.template.spec.containers[0].env @@ -193,10 +204,11 @@ tests: name: my-existing-secret key: AGENT_REGISTRY_JWT_PRIVATE_KEY - - it: uses database.existingSecret for POSTGRES_PASSWORD when set + - it: uses database.external.existingSecret for POSTGRES_PASSWORD when set (bundled disabled) template: deployment.yaml set: - database.existingSecret: db-secret + database.bundled.enabled: false + database.external.existingSecret: db-secret asserts: - contains: path: spec.template.spec.containers[0].env @@ -245,3 +257,22 @@ tests: - equal: path: metadata.name value: myapp + + # ── Init container (bundled PostgreSQL) ────────────────────────────────────── + + - it: includes wait-for-postgres init container when bundled is enabled + template: deployment.yaml + asserts: + - isNotNull: + path: spec.template.spec.initContainers + - equal: + path: spec.template.spec.initContainers[0].name + value: wait-for-postgres + + - it: does not include init container when bundled is disabled + template: deployment.yaml + set: + database.bundled.enabled: false + asserts: + - isNull: + path: spec.template.spec.initContainers diff --git a/charts/agentregistry/tests/postgresql_test.yaml b/charts/agentregistry/tests/postgresql_test.yaml new file mode 100644 index 00000000..f7124f91 --- /dev/null +++ b/charts/agentregistry/tests/postgresql_test.yaml @@ -0,0 +1,245 @@ +suite: Bundled PostgreSQL +templates: + - postgresql.yaml + - secrets.yaml + +tests: + # ── Bundled enabled (default) ───────────────────────────────────────────── + + - it: renders PVC, Deployment, and Service when bundled is enabled + template: postgresql.yaml + asserts: + - hasDocuments: + count: 3 + + - it: PVC has correct name + template: postgresql.yaml + documentIndex: 0 + asserts: + - isKind: + of: PersistentVolumeClaim + - equal: + path: metadata.name + value: RELEASE-NAME-agentregistry-postgresql + + - it: Deployment has correct name and Recreate strategy + template: postgresql.yaml + documentIndex: 1 + asserts: + - isKind: + of: Deployment + - equal: + path: metadata.name + value: RELEASE-NAME-agentregistry-postgresql + - equal: + path: spec.strategy.type + value: Recreate + + - it: Deployment uses pgvector image + template: postgresql.yaml + documentIndex: 1 + asserts: + - matchRegex: + path: spec.template.spec.containers[0].image + pattern: '^docker.io/pgvector/pgvector:0\.8\.2-pg16$' + + - it: Deployment references chart-managed secret for POSTGRES_PASSWORD + template: postgresql.yaml + documentIndex: 1 + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: RELEASE-NAME-agentregistry + key: POSTGRES_PASSWORD + + - it: Deployment uses global.existingSecret for POSTGRES_PASSWORD when set + template: postgresql.yaml + set: + global.existingSecret: global-secret + documentIndex: 1 + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: global-secret + key: POSTGRES_PASSWORD + + - it: Deployment has liveness and readiness probes + template: postgresql.yaml + documentIndex: 1 + asserts: + - isNotNull: + path: spec.template.spec.containers[0].livenessProbe + - isNotNull: + path: spec.template.spec.containers[0].readinessProbe + + - it: Deployment mounts PVC as data volume + template: postgresql.yaml + documentIndex: 1 + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: data + persistentVolumeClaim: + claimName: RELEASE-NAME-agentregistry-postgresql + + - it: Service has correct name and port + template: postgresql.yaml + documentIndex: 2 + asserts: + - isKind: + of: Service + - equal: + path: metadata.name + value: RELEASE-NAME-agentregistry-postgresql + - equal: + path: spec.ports[0].port + value: 5432 + + # ── Bundled disabled ────────────────────────────────────────────────────── + + - it: renders nothing when bundled is disabled + template: postgresql.yaml + set: + database.bundled.enabled: false + asserts: + - hasDocuments: + count: 0 + + # ── Persistence disabled ────────────────────────────────────────────────── + + - it: renders only Deployment and Service when persistence is disabled (no PVC) + template: postgresql.yaml + set: + database.bundled.persistence.enabled: false + asserts: + - hasDocuments: + count: 2 + + - it: Deployment uses emptyDir when persistence is disabled + template: postgresql.yaml + set: + database.bundled.persistence.enabled: false + documentIndex: 0 + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.volumes + content: + name: data + emptyDir: {} + + # ── Existing PVC claim ──────────────────────────────────────────────────── + + - it: does not create PVC when existingClaim is set + template: postgresql.yaml + set: + database.bundled.persistence.existingClaim: my-existing-pvc + asserts: + - hasDocuments: + count: 2 + + - it: uses existingClaim name in Deployment volume + template: postgresql.yaml + set: + database.bundled.persistence.existingClaim: my-existing-pvc + documentIndex: 0 + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.volumes + content: + name: data + persistentVolumeClaim: + claimName: my-existing-pvc + + # ── Security contexts ───────────────────────────────────────────────────── + + - it: applies pod security context by default + template: postgresql.yaml + documentIndex: 1 + asserts: + - isKind: + of: Deployment + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 999 + + - it: applies container security context by default + template: postgresql.yaml + documentIndex: 1 + asserts: + - isKind: + of: Deployment + - equal: + path: spec.template.spec.containers[0].securityContext.runAsNonRoot + value: true + - equal: + path: spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation + value: false + + - it: omits pod security context when disabled + template: postgresql.yaml + set: + database.bundled.podSecurityContext.enabled: false + documentIndex: 1 + asserts: + - isKind: + of: Deployment + - isNull: + path: spec.template.spec.securityContext + + - it: omits container security context when disabled + template: postgresql.yaml + set: + database.bundled.containerSecurityContext.enabled: false + documentIndex: 1 + asserts: + - isKind: + of: Deployment + - isNull: + path: spec.template.spec.containers[0].securityContext + + - it: respects custom fsGroup override + template: postgresql.yaml + set: + database.bundled.podSecurityContext.fsGroup: 1000 + documentIndex: 1 + asserts: + - isKind: + of: Deployment + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 1000 + + # ── Custom auth values ──────────────────────────────────────────────────── + + - it: uses custom bundled auth values + template: postgresql.yaml + set: + database.bundled.auth.database: mydb + database.bundled.auth.username: myuser + documentIndex: 1 + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.containers[0].env + content: + name: POSTGRES_DB + value: mydb + - contains: + path: spec.template.spec.containers[0].env + content: + name: POSTGRES_USER + value: myuser diff --git a/charts/agentregistry/tests/secrets_test.yaml b/charts/agentregistry/tests/secrets_test.yaml index 0efbd5ea..ee779df4 100644 --- a/charts/agentregistry/tests/secrets_test.yaml +++ b/charts/agentregistry/tests/secrets_test.yaml @@ -49,9 +49,20 @@ tests: # base64("deadbeef1234567890abcdef") value: "ZGVhZGJlZWYxMjM0NTY3ODkwYWJjZGVm" - - it: uses provided database.password value (base64-encoded) + - it: uses bundled auth password for POSTGRES_PASSWORD when bundled is enabled (default) set: - database.password: mypassword + database.bundled.auth.password: mybundledpass + documentIndex: 0 + asserts: + - equal: + path: data["POSTGRES_PASSWORD"] + # base64("mybundledpass") + value: "bXlidW5kbGVkcGFzcw==" + + - it: uses database.external.password for POSTGRES_PASSWORD when bundled is disabled + set: + database.bundled.enabled: false + database.external.password: mypassword documentIndex: 0 asserts: - equal: @@ -72,11 +83,12 @@ tests: - isNotNull: path: data["POSTGRES_PASSWORD"] - # ── database.existingSecret omits POSTGRES_PASSWORD ───────────────────────── + # ── database.external.existingSecret omits POSTGRES_PASSWORD (bundled disabled) ────────── - - it: omits POSTGRES_PASSWORD when database.existingSecret is set + - it: omits POSTGRES_PASSWORD when database.external.existingSecret is set (bundled disabled) set: - database.existingSecret: ext-db-secret + database.bundled.enabled: false + database.external.existingSecret: ext-db-secret asserts: - hasDocuments: count: 1 @@ -85,12 +97,13 @@ tests: - isNotNull: path: data["AGENT_REGISTRY_JWT_PRIVATE_KEY"] - # ── Both existingSecrets suppress the Secret entirely ─────────────────────── + # ── Both existingSecrets suppress the Secret entirely (bundled disabled) ──── - - it: renders no Secret when both existingSecrets are set + - it: renders no Secret when both existingSecrets are set (bundled disabled) set: + database.bundled.enabled: false config.existingSecret: my-creds - database.existingSecret: ext-db-secret + database.external.existingSecret: ext-db-secret asserts: - hasDocuments: count: 0 @@ -104,11 +117,22 @@ tests: - hasDocuments: count: 0 - - it: global.existingSecret overrides config.existingSecret and database.existingSecret + - it: global.existingSecret overrides config.existingSecret and database.external.existingSecret set: global.existingSecret: global-secret config.existingSecret: my-creds - database.existingSecret: ext-db-secret + database.external.existingSecret: ext-db-secret asserts: - hasDocuments: count: 0 + + # ── Bundled enabled: Secret is always created (unless global.existingSecret) ─ + + - it: renders Secret even when database.external.existingSecret is set if bundled is enabled + set: + database.external.existingSecret: ext-db-secret + asserts: + - hasDocuments: + count: 1 + - isNotNull: + path: data["POSTGRES_PASSWORD"] diff --git a/charts/agentregistry/tests/validation_test.yaml b/charts/agentregistry/tests/validation_test.yaml index a9ffc565..2779a641 100644 --- a/charts/agentregistry/tests/validation_test.yaml +++ b/charts/agentregistry/tests/validation_test.yaml @@ -5,20 +5,29 @@ templates: tests: # ── JWT key validation ─────────────────────────────────────────────────────── - - it: renders nothing when jwtPrivateKey and database.password are set + - it: renders nothing when bundled is enabled and jwtPrivateKey is set set: config.jwtPrivateKey: deadbeef1234567890abcdef - database.password: changeme - database.host: mydb.example.com asserts: - hasDocuments: count: 0 - - it: renders nothing when config.existingSecret is set + - it: renders nothing when jwtPrivateKey and database.external.password are set (bundled disabled) + set: + config.jwtPrivateKey: deadbeef1234567890abcdef + database.bundled.enabled: false + database.external.password: changeme + database.external.host: mydb.example.com + asserts: + - hasDocuments: + count: 0 + + - it: renders nothing when config.existingSecret is set (bundled disabled) set: config.existingSecret: my-existing-secret - database.password: changeme - database.host: mydb.example.com + database.bundled.enabled: false + database.external.password: changeme + database.external.host: mydb.example.com asserts: - hasDocuments: count: 0 @@ -26,14 +35,13 @@ tests: - it: renders nothing when global.existingSecret is set set: global.existingSecret: global-secret - database.host: mydb.example.com asserts: - hasDocuments: count: 0 - it: fails when jwtPrivateKey is empty and no existingSecret is provided set: - database.password: changeme + database.bundled.enabled: true asserts: - failedTemplate: errorPattern: "CHART CONFIGURATION ERROR" @@ -41,42 +49,56 @@ tests: - it: fails when jwtPrivateKey is not a valid hex string set: config.jwtPrivateKey: "not-hex!!" - database.password: changeme asserts: - failedTemplate: errorPattern: "CHART CONFIGURATION ERROR" - # ── Database password validation ───────────────────────────────────────────── + # ── Bundled PostgreSQL skips external validation ────────────────────────── + + - it: renders nothing with bundled enabled even when no external host or password + set: + config.jwtPrivateKey: deadbeef1234567890abcdef + asserts: + - hasDocuments: + count: 0 + + # ── Database password validation (bundled disabled) ─────────────────────── - - it: fails when database.password is empty and no existingSecret is provided + - it: fails when database.external.password is empty and no existingSecret (bundled disabled) set: config.jwtPrivateKey: deadbeef1234567890abcdef + database.bundled.enabled: false + database.external.host: mydb.example.com asserts: - failedTemplate: errorPattern: "CHART CONFIGURATION ERROR" - - it: renders nothing when database.existingSecret is set + - it: renders nothing when database.external.existingSecret is set (bundled disabled) set: config.jwtPrivateKey: deadbeef1234567890abcdef - database.existingSecret: pg-secret - database.host: mydb.example.com + database.bundled.enabled: false + database.external.existingSecret: pg-secret + database.external.host: mydb.example.com asserts: - hasDocuments: count: 0 - - it: renders nothing when database.url is set + - it: renders nothing when database.external.url is set (bundled disabled) set: config.jwtPrivateKey: deadbeef1234567890abcdef - database.url: "postgres://user:pass@host:5432/db" + database.bundled.enabled: false + database.external.url: "postgres://user:pass@host:5432/db" asserts: - hasDocuments: count: 0 - # ── Database host validation ───────────────────────────────────────────────── - - it: fails when database.host and database.url are both unset + # ── Database host validation (bundled disabled) ─────────────────────────── + + - it: fails when database.external.host and database.external.url are both unset (bundled disabled) set: config.jwtPrivateKey: deadbeef1234567890abcdef - database.password: changeme + database.bundled.enabled: false + database.external.password: changeme asserts: - failedTemplate: errorPattern: "CHART CONFIGURATION ERROR" diff --git a/charts/agentregistry/values.yaml b/charts/agentregistry/values.yaml index 28dea77c..a10741f6 100644 --- a/charts/agentregistry/values.yaml +++ b/charts/agentregistry/values.yaml @@ -263,19 +263,107 @@ rbac: # @section Database database: - # -- Full PostgreSQL connection string (overrides all other database fields when set) - url: "" - # -- External database host - host: "" - # -- External database port - port: 5432 - # -- External database name - database: "agent-registry" - # -- External database user - username: "agentregistry" - # -- External database password (ignored when existingSecret is set) - password: "" - # -- Name of an existing Secret containing the database password (key: POSTGRES_PASSWORD) - existingSecret: "" - # -- External database SSL mode (require, verify-ca, verify-full, disable). Defaults to require for encrypted connections. - sslMode: "require" + # -- Bundled PostgreSQL (dev/test). Enabled by default; set bundled.enabled=false to bring your own. + bundled: + # -- Deploy a PostgreSQL instance alongside Agent Registry + enabled: true + image: + # -- Bundled PostgreSQL image registry + registry: docker.io + # -- Bundled PostgreSQL image repository + repository: pgvector/pgvector + # -- Bundled PostgreSQL image tag + tag: "0.8.2-pg16" + # -- Bundled PostgreSQL image pull policy + pullPolicy: IfNotPresent + auth: + # -- Database name to create + database: "agent-registry" + # -- Database user to create + username: "agentregistry" + # -- Database password (override in production) + password: "agentregistry" + # -- SSL mode for the bundled PostgreSQL connection + sslMode: "disable" + service: + # -- Service type for bundled PostgreSQL + type: ClusterIP + # -- Service port for bundled PostgreSQL + port: 5432 + persistence: + # -- Enable persistent storage for bundled PostgreSQL + enabled: true + # -- PVC size + size: 5Gi + # -- PVC access modes + accessModes: [ReadWriteOnce] + # -- StorageClass name (empty = cluster default) + storageClass: "" + # -- Use an existing PVC instead of creating one + existingClaim: "" + podSecurityContext: + # -- Enable pod-level security context for bundled PostgreSQL + enabled: true + # -- Group ID for the bundled PostgreSQL pod filesystem (matches the postgres user in the pgvector image) + fsGroup: 999 + # -- Policy for changing fsGroup ownership + fsGroupChangePolicy: Always + + containerSecurityContext: + # -- Enable container-level security context for bundled PostgreSQL + enabled: true + # -- User ID to run the bundled PostgreSQL container as (postgres user in the pgvector image) + runAsUser: 999 + # -- Group ID to run the bundled PostgreSQL container as + runAsGroup: 999 + # -- Prevent running as root + runAsNonRoot: true + # -- Allow privilege escalation + allowPrivilegeEscalation: false + + # -- Resource requests/limits for the bundled PostgreSQL container (overrides resourcesPreset when set) + resources: {} + # -- Resource preset for the bundled PostgreSQL container (none, nano, micro, small, medium, large, xlarge, 2xlarge) + resourcesPreset: "small" + probes: + liveness: + # -- Initial delay before bundled PostgreSQL liveness check + initialDelaySeconds: 20 + # -- Period between bundled PostgreSQL liveness checks + periodSeconds: 10 + # -- Timeout for bundled PostgreSQL liveness check + timeoutSeconds: 5 + # -- Failure threshold for bundled PostgreSQL liveness check + failureThreshold: 6 + # -- Success threshold for bundled PostgreSQL liveness check + successThreshold: 1 + readiness: + # -- Initial delay before bundled PostgreSQL readiness check + initialDelaySeconds: 5 + # -- Period between bundled PostgreSQL readiness checks + periodSeconds: 5 + # -- Timeout for bundled PostgreSQL readiness check + timeoutSeconds: 3 + # -- Failure threshold for bundled PostgreSQL readiness check + failureThreshold: 3 + # -- Success threshold for bundled PostgreSQL readiness check + successThreshold: 1 + + # -- External PostgreSQL settings (used when bundled.enabled=false) + external: + # -- Full PostgreSQL connection string (overrides all other external fields when set) + url: "" + # -- External database host + host: "" + # -- External database port + port: 5432 + # -- External database name + database: "agent-registry" + # -- External database user + username: "agentregistry" + # -- External database password (ignored when existingSecret is set) + password: "" + # -- Name of an existing Secret containing the database password (key: POSTGRES_PASSWORD) + existingSecret: "" + # -- External database SSL mode (require, verify-ca, verify-full, disable) + sslMode: "require" From 124bd18f76bc7b66db80015973e45a18944adc13 Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Tue, 17 Mar 2026 16:42:32 -0700 Subject: [PATCH 02/15] some cleanup --- charts/agentregistry/templates/_helpers.tpl | 83 --------- .../agentregistry/templates/deployment.yaml | 5 +- .../agentregistry/templates/postgresql.yaml | 54 ++---- .../agentregistry/tests/postgresql_test.yaml | 173 +++++++----------- charts/agentregistry/values.yaml | 78 ++------ 5 files changed, 103 insertions(+), 290 deletions(-) diff --git a/charts/agentregistry/templates/_helpers.tpl b/charts/agentregistry/templates/_helpers.tpl index 931ea11d..af43dcb3 100644 --- a/charts/agentregistry/templates/_helpers.tpl +++ b/charts/agentregistry/templates/_helpers.tpl @@ -187,75 +187,6 @@ Otherwise: if database.external.url is set, use it directly; else build from ind {{- end }} {{- end }} -{{/* ====================================================================== - Resource management - ====================================================================== */}} - -{{/* -Return resource requests/limits. -If .resources is non-empty, use it directly. -Otherwise, map .resourcesPreset to a set of defaults. - -Usage: include "agentregistry.resources" (dict "resources" .Values.resources "preset" .Values.resourcesPreset) -*/}} -{{- define "agentregistry.resources" -}} -{{- if .resources }} -{{- toYaml .resources }} -{{- else }} -{{- $preset := .preset | default "none" }} -{{- if eq $preset "nano" }} -requests: - cpu: 100m - memory: 128Mi -limits: - cpu: 200m - memory: 256Mi -{{- else if eq $preset "micro" }} -requests: - cpu: 250m - memory: 256Mi -limits: - cpu: 500m - memory: 512Mi -{{- else if eq $preset "small" }} -requests: - cpu: 250m - memory: 256Mi -limits: - cpu: "1" - memory: 1Gi -{{- else if eq $preset "medium" }} -requests: - cpu: 500m - memory: 512Mi -limits: - cpu: "2" - memory: 2Gi -{{- else if eq $preset "large" }} -requests: - cpu: "1" - memory: 1Gi -limits: - cpu: "4" - memory: 4Gi -{{- else if eq $preset "xlarge" }} -requests: - cpu: "2" - memory: 2Gi -limits: - cpu: "8" - memory: 8Gi -{{- else if eq $preset "2xlarge" }} -requests: - cpu: "4" - memory: 4Gi -limits: - cpu: "16" - memory: 16Gi -{{- end }} -{{- end }} -{{- end }} - {{/* ====================================================================== Security context helpers ====================================================================== */}} @@ -431,20 +362,6 @@ Respects global.imageRegistry override. {{- printf "%s/%s:%s" $registry .Values.database.bundled.image.repository .Values.database.bundled.image.tag }} {{- end }} -{{/* -Resolve the storageClassName for the bundled PostgreSQL PVC. -Prefers global.storageClass over database.bundled.persistence.storageClass. -*/}} -{{- define "agentregistry.storageClass" -}} -{{- $sc := .Values.database.bundled.persistence.storageClass -}} -{{- if .Values.global }} - {{- if .Values.global.storageClass }} - {{- $sc = .Values.global.storageClass -}} - {{- end }} -{{- end }} -{{- $sc }} -{{- end }} - {{/* ====================================================================== Validation ====================================================================== */}} diff --git a/charts/agentregistry/templates/deployment.yaml b/charts/agentregistry/templates/deployment.yaml index a9819768..192fe417 100644 --- a/charts/agentregistry/templates/deployment.yaml +++ b/charts/agentregistry/templates/deployment.yaml @@ -174,10 +174,9 @@ spec: lifecycle: {{- toYaml .Values.lifecycleHooks | nindent 12 }} {{- end }} - {{- $res := include "agentregistry.resources" (dict "resources" .Values.resources "preset" .Values.resourcesPreset) }} - {{- if $res }} + {{- if .Values.resources }} resources: - {{- $res | nindent 12 }} + {{- toYaml .Values.resources | nindent 12 }} {{- end }} volumeMounts: {{- if .Values.containerSecurityContext.enabled }} diff --git a/charts/agentregistry/templates/postgresql.yaml b/charts/agentregistry/templates/postgresql.yaml index 04699416..7e6d2f68 100644 --- a/charts/agentregistry/templates/postgresql.yaml +++ b/charts/agentregistry/templates/postgresql.yaml @@ -2,7 +2,6 @@ {{- $pg := .Values.database.bundled }} {{- $fullname := include "agentregistry.postgresql.fullname" . }} --- -{{- if and $pg.persistence.enabled (not $pg.persistence.existingClaim) }} apiVersion: v1 kind: PersistentVolumeClaim metadata: @@ -16,16 +15,11 @@ metadata: {{- end }} spec: accessModes: - {{- toYaml $pg.persistence.accessModes | nindent 4 }} + - ReadWriteOnce resources: requests: - storage: {{ $pg.persistence.size }} - {{- $sc := include "agentregistry.storageClass" . | trim }} - {{- if $sc }} - storageClassName: {{ $sc | quote }} - {{- end }} + storage: {{ $pg.storage }} --- -{{- end }} apiVersion: apps/v1 kind: Deployment metadata: @@ -49,20 +43,17 @@ spec: labels: {{- include "agentregistry.postgresql.selectorLabels" . | nindent 8 }} spec: - {{- $podSec := include "agentregistry.podSecurityContext" $pg.podSecurityContext }} - {{- if $podSec }} securityContext: - {{- $podSec | nindent 8 }} - {{- end }} + fsGroup: 999 + runAsUser: 999 + runAsGroup: 999 + runAsNonRoot: true containers: - name: postgresql image: {{ include "agentregistry.postgresql.image" . }} imagePullPolicy: {{ $pg.image.pullPolicy }} - {{- $containerSec := include "agentregistry.containerSecurityContext" $pg.containerSecurityContext }} - {{- if $containerSec }} securityContext: - {{- $containerSec | nindent 12 }} - {{- end }} + allowPrivilegeEscalation: false ports: - name: postgresql containerPort: 5432 @@ -87,11 +78,11 @@ spec: - {{ $pg.auth.username | quote }} - -d - {{ $pg.auth.database | quote }} - initialDelaySeconds: {{ $pg.probes.liveness.initialDelaySeconds }} - periodSeconds: {{ $pg.probes.liveness.periodSeconds }} - timeoutSeconds: {{ $pg.probes.liveness.timeoutSeconds }} - failureThreshold: {{ $pg.probes.liveness.failureThreshold }} - successThreshold: {{ $pg.probes.liveness.successThreshold }} + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 readinessProbe: exec: command: @@ -100,27 +91,22 @@ spec: - {{ $pg.auth.username | quote }} - -d - {{ $pg.auth.database | quote }} - initialDelaySeconds: {{ $pg.probes.readiness.initialDelaySeconds }} - periodSeconds: {{ $pg.probes.readiness.periodSeconds }} - timeoutSeconds: {{ $pg.probes.readiness.timeoutSeconds }} - failureThreshold: {{ $pg.probes.readiness.failureThreshold }} - successThreshold: {{ $pg.probes.readiness.successThreshold }} - {{- $res := include "agentregistry.resources" (dict "resources" $pg.resources "preset" $pg.resourcesPreset) }} - {{- if $res }} + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + successThreshold: 1 + {{- if $pg.resources }} resources: - {{- $res | nindent 12 }} + {{- toYaml $pg.resources | nindent 12 }} {{- end }} volumeMounts: - name: data mountPath: /var/lib/postgresql/data volumes: - name: data - {{- if $pg.persistence.enabled }} persistentVolumeClaim: - claimName: {{ $pg.persistence.existingClaim | default $fullname }} - {{- else }} - emptyDir: {} - {{- end }} + claimName: {{ $fullname }} --- apiVersion: v1 kind: Service diff --git a/charts/agentregistry/tests/postgresql_test.yaml b/charts/agentregistry/tests/postgresql_test.yaml index f7124f91..04f7e7ea 100644 --- a/charts/agentregistry/tests/postgresql_test.yaml +++ b/charts/agentregistry/tests/postgresql_test.yaml @@ -12,7 +12,7 @@ tests: - hasDocuments: count: 3 - - it: PVC has correct name + - it: PVC has correct name and storage size template: postgresql.yaml documentIndex: 0 asserts: @@ -21,6 +21,12 @@ tests: - equal: path: metadata.name value: RELEASE-NAME-agentregistry-postgresql + - equal: + path: spec.resources.requests.storage + value: 5Gi + - equal: + path: spec.accessModes[0] + value: ReadWriteOnce - it: Deployment has correct name and Recreate strategy template: postgresql.yaml @@ -43,6 +49,32 @@ tests: path: spec.template.spec.containers[0].image pattern: '^docker.io/pgvector/pgvector:0\.8\.2-pg16$' + - it: Deployment has hardcoded pod security context + template: postgresql.yaml + documentIndex: 1 + asserts: + - isKind: + of: Deployment + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 999 + - equal: + path: spec.template.spec.securityContext.runAsUser + value: 999 + - equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + + - it: Deployment has hardcoded container security context + template: postgresql.yaml + documentIndex: 1 + asserts: + - isKind: + of: Deployment + - equal: + path: spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation + value: false + - it: Deployment references chart-managed secret for POSTGRES_PASSWORD template: postgresql.yaml documentIndex: 1 @@ -77,8 +109,14 @@ tests: asserts: - isNotNull: path: spec.template.spec.containers[0].livenessProbe + - equal: + path: spec.template.spec.containers[0].livenessProbe.initialDelaySeconds + value: 20 - isNotNull: path: spec.template.spec.containers[0].readinessProbe + - equal: + path: spec.template.spec.containers[0].readinessProbe.initialDelaySeconds + value: 5 - it: Deployment mounts PVC as data volume template: postgresql.yaml @@ -91,6 +129,17 @@ tests: persistentVolumeClaim: claimName: RELEASE-NAME-agentregistry-postgresql + - it: Deployment renders resources when set + template: postgresql.yaml + documentIndex: 1 + asserts: + - equal: + path: spec.template.spec.containers[0].resources.requests.cpu + value: 250m + - equal: + path: spec.template.spec.containers[0].resources.limits.cpu + value: "1" + - it: Service has correct name and port template: postgresql.yaml documentIndex: 2 @@ -114,114 +163,6 @@ tests: - hasDocuments: count: 0 - # ── Persistence disabled ────────────────────────────────────────────────── - - - it: renders only Deployment and Service when persistence is disabled (no PVC) - template: postgresql.yaml - set: - database.bundled.persistence.enabled: false - asserts: - - hasDocuments: - count: 2 - - - it: Deployment uses emptyDir when persistence is disabled - template: postgresql.yaml - set: - database.bundled.persistence.enabled: false - documentIndex: 0 - asserts: - - isKind: - of: Deployment - - contains: - path: spec.template.spec.volumes - content: - name: data - emptyDir: {} - - # ── Existing PVC claim ──────────────────────────────────────────────────── - - - it: does not create PVC when existingClaim is set - template: postgresql.yaml - set: - database.bundled.persistence.existingClaim: my-existing-pvc - asserts: - - hasDocuments: - count: 2 - - - it: uses existingClaim name in Deployment volume - template: postgresql.yaml - set: - database.bundled.persistence.existingClaim: my-existing-pvc - documentIndex: 0 - asserts: - - isKind: - of: Deployment - - contains: - path: spec.template.spec.volumes - content: - name: data - persistentVolumeClaim: - claimName: my-existing-pvc - - # ── Security contexts ───────────────────────────────────────────────────── - - - it: applies pod security context by default - template: postgresql.yaml - documentIndex: 1 - asserts: - - isKind: - of: Deployment - - equal: - path: spec.template.spec.securityContext.fsGroup - value: 999 - - - it: applies container security context by default - template: postgresql.yaml - documentIndex: 1 - asserts: - - isKind: - of: Deployment - - equal: - path: spec.template.spec.containers[0].securityContext.runAsNonRoot - value: true - - equal: - path: spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation - value: false - - - it: omits pod security context when disabled - template: postgresql.yaml - set: - database.bundled.podSecurityContext.enabled: false - documentIndex: 1 - asserts: - - isKind: - of: Deployment - - isNull: - path: spec.template.spec.securityContext - - - it: omits container security context when disabled - template: postgresql.yaml - set: - database.bundled.containerSecurityContext.enabled: false - documentIndex: 1 - asserts: - - isKind: - of: Deployment - - isNull: - path: spec.template.spec.containers[0].securityContext - - - it: respects custom fsGroup override - template: postgresql.yaml - set: - database.bundled.podSecurityContext.fsGroup: 1000 - documentIndex: 1 - asserts: - - isKind: - of: Deployment - - equal: - path: spec.template.spec.securityContext.fsGroup - value: 1000 - # ── Custom auth values ──────────────────────────────────────────────────── - it: uses custom bundled auth values @@ -243,3 +184,17 @@ tests: content: name: POSTGRES_USER value: myuser + + # ── Custom storage size ─────────────────────────────────────────────────── + + - it: uses custom storage size + template: postgresql.yaml + set: + database.bundled.storage: 10Gi + documentIndex: 0 + asserts: + - isKind: + of: PersistentVolumeClaim + - equal: + path: spec.resources.requests.storage + value: 10Gi diff --git a/charts/agentregistry/values.yaml b/charts/agentregistry/values.yaml index a10741f6..b7ba080d 100644 --- a/charts/agentregistry/values.yaml +++ b/charts/agentregistry/values.yaml @@ -124,9 +124,13 @@ containerSecurityContext: # @section Resource management # -- Resource requests and limits for the Agent Registry container -resources: {} -# -- Resource preset to use when resources is empty (none, nano, micro, small, medium, large, xlarge, 2xlarge) -resourcesPreset: "small" +resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi # @section Liveness probe @@ -290,64 +294,16 @@ database: type: ClusterIP # -- Service port for bundled PostgreSQL port: 5432 - persistence: - # -- Enable persistent storage for bundled PostgreSQL - enabled: true - # -- PVC size - size: 5Gi - # -- PVC access modes - accessModes: [ReadWriteOnce] - # -- StorageClass name (empty = cluster default) - storageClass: "" - # -- Use an existing PVC instead of creating one - existingClaim: "" - podSecurityContext: - # -- Enable pod-level security context for bundled PostgreSQL - enabled: true - # -- Group ID for the bundled PostgreSQL pod filesystem (matches the postgres user in the pgvector image) - fsGroup: 999 - # -- Policy for changing fsGroup ownership - fsGroupChangePolicy: Always - - containerSecurityContext: - # -- Enable container-level security context for bundled PostgreSQL - enabled: true - # -- User ID to run the bundled PostgreSQL container as (postgres user in the pgvector image) - runAsUser: 999 - # -- Group ID to run the bundled PostgreSQL container as - runAsGroup: 999 - # -- Prevent running as root - runAsNonRoot: true - # -- Allow privilege escalation - allowPrivilegeEscalation: false - - # -- Resource requests/limits for the bundled PostgreSQL container (overrides resourcesPreset when set) - resources: {} - # -- Resource preset for the bundled PostgreSQL container (none, nano, micro, small, medium, large, xlarge, 2xlarge) - resourcesPreset: "small" - probes: - liveness: - # -- Initial delay before bundled PostgreSQL liveness check - initialDelaySeconds: 20 - # -- Period between bundled PostgreSQL liveness checks - periodSeconds: 10 - # -- Timeout for bundled PostgreSQL liveness check - timeoutSeconds: 5 - # -- Failure threshold for bundled PostgreSQL liveness check - failureThreshold: 6 - # -- Success threshold for bundled PostgreSQL liveness check - successThreshold: 1 - readiness: - # -- Initial delay before bundled PostgreSQL readiness check - initialDelaySeconds: 5 - # -- Period between bundled PostgreSQL readiness checks - periodSeconds: 5 - # -- Timeout for bundled PostgreSQL readiness check - timeoutSeconds: 3 - # -- Failure threshold for bundled PostgreSQL readiness check - failureThreshold: 3 - # -- Success threshold for bundled PostgreSQL readiness check - successThreshold: 1 + # -- PersistentVolumeClaim size for the bundled PostgreSQL data directory + storage: 5Gi + # -- Resource requests/limits for the bundled PostgreSQL container + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi # -- External PostgreSQL settings (used when bundled.enabled=false) external: From ed9c34c315ef72db4b672b27e0bd13cc293fb741 Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Tue, 17 Mar 2026 17:17:42 -0700 Subject: [PATCH 03/15] image --- charts/agentregistry/tests/postgresql_test.yaml | 2 +- charts/agentregistry/values.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/agentregistry/tests/postgresql_test.yaml b/charts/agentregistry/tests/postgresql_test.yaml index 04f7e7ea..35c888d6 100644 --- a/charts/agentregistry/tests/postgresql_test.yaml +++ b/charts/agentregistry/tests/postgresql_test.yaml @@ -47,7 +47,7 @@ tests: asserts: - matchRegex: path: spec.template.spec.containers[0].image - pattern: '^docker.io/pgvector/pgvector:0\.8\.2-pg16$' + pattern: '^docker.io/pgvector/pgvector:pg16$' - it: Deployment has hardcoded pod security context template: postgresql.yaml diff --git a/charts/agentregistry/values.yaml b/charts/agentregistry/values.yaml index b7ba080d..f78da823 100644 --- a/charts/agentregistry/values.yaml +++ b/charts/agentregistry/values.yaml @@ -277,7 +277,7 @@ database: # -- Bundled PostgreSQL image repository repository: pgvector/pgvector # -- Bundled PostgreSQL image tag - tag: "0.8.2-pg16" + tag: "pg16" # -- Bundled PostgreSQL image pull policy pullPolicy: IfNotPresent auth: From 70d57adc9a3116bd8b51fb507e896d569122186f Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Tue, 17 Mar 2026 17:28:10 -0700 Subject: [PATCH 04/15] update local setup to use the bundled DB --- DEVELOPMENT.md | 11 +++++------ Makefile | 16 +++------------- README.md | 22 ++++++++++------------ scripts/kind/README.md | 24 +++++++++++------------- 4 files changed, 29 insertions(+), 44 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 1a9653ce..b2f75734 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -2,7 +2,7 @@ ## Local Kubernetes Environment -The fastest way to run the full stack locally is with [Kind](https://kind.sigs.k8s.io/). A single `make` target creates the cluster, deploys PostgreSQL/pgvector, builds the server image, and installs AgentRegistry via Helm. +The fastest way to run the full stack locally is with [Kind](https://kind.sigs.k8s.io/). A single `make` target creates the cluster, builds the server image, and installs AgentRegistry via Helm — PostgreSQL/pgvector is bundled and deployed automatically by the Helm chart. ### Prerequisites @@ -19,13 +19,12 @@ The fastest way to run the full stack locally is with [Kind](https://kind.sigs.k make setup-kind-cluster ``` -This runs three steps in order: +This runs two steps in order: | Step | Target | What it does | |------|--------|-------------| | 1 | `create-kind-cluster` | Installs `kind` to `./bin/`, creates Kind cluster + local registry (`localhost:5001`) + MetalLB | -| 2 | `install-postgresql` | Deploys `pgvector/pgvector:0.8.2-pg16` into the `agentregistry` namespace | -| 3 | `install-agentregistry` | Builds server image, pushes to local registry, Helm installs AgentRegistry | +| 2 | `install-agentregistry` | Builds server image, pushes to local registry, Helm installs AgentRegistry (PostgreSQL/pgvector bundled) | Each target can also be run independently — useful when iterating on code: @@ -48,8 +47,8 @@ On subsequent runs, `install-agentregistry` reuses the `jwtPrivateKey` already s kubectl --context kind-agentregistry port-forward -n agentregistry svc/agentregistry 12121:12121 # open http://localhost:12121 -# PostgreSQL (for direct inspection) -kubectl --context kind-agentregistry port-forward -n agentregistry svc/postgres-pgvector 5432:5432 +# Bundled PostgreSQL (for direct inspection) +kubectl --context kind-agentregistry port-forward -n agentregistry svc/agentregistry-postgresql 5432:5432 psql -h localhost -U agentregistry -d agent-registry ``` diff --git a/Makefile b/Makefile index ad5e8442..7a01baae 100644 --- a/Makefile +++ b/Makefile @@ -334,11 +334,6 @@ kind-debug: ## Shell into Kind control-plane and run btop for resource monitorin docker exec -it $(KIND_CLUSTER_NAME)-control-plane bash -c 'apt-get update -qq && apt-get install -y --no-install-recommends btop htop' docker exec -it $(KIND_CLUSTER_NAME)-control-plane bash -c 'btop --utf-force' -.PHONY: install-postgresql -install-postgresql: ## Deploy standalone PostgreSQL/pgvector into the Kind cluster - kubectl --context $(KIND_CLUSTER_CONTEXT) apply -f examples/postgres-pgvector.yaml - kubectl --context $(KIND_CLUSTER_CONTEXT) -n agentregistry wait --for=condition=ready pod -l app=postgres-pgvector --timeout=120s - BUILD ?= true .PHONY: install-agentregistry @@ -358,9 +353,6 @@ endif --set image.registry=$(DOCKER_REGISTRY) \ --set image.repository=$(DOCKER_REPO)/server \ --set image.tag=$(VERSION) \ - --set database.host=postgres-pgvector.$(KIND_NAMESPACE).svc.cluster.local \ - --set database.password=agentregistry \ - --set database.sslMode=disable \ --set config.jwtPrivateKey="$$JWT_KEY" \ --set config.enableAnonymousAuth="true" \ --wait \ @@ -370,9 +362,9 @@ endif install-kagent: ## Install kagent on the Kind cluster (downloads CLI if absent) KUBE_CONTEXT=$(KIND_CLUSTER_CONTEXT) KAGENT_VERSION=$(KAGENT_VERSION) bash ./scripts/kind/install-kagent.sh -## Set up a full local K8s dev environment (Kind + PostgreSQL/pgvector + AgentRegistry + kagent). +## Set up a full local K8s dev environment (Kind + AgentRegistry with bundled PostgreSQL + kagent). .PHONY: setup-kind-cluster -setup-kind-cluster: create-kind-cluster install-postgresql install-agentregistry install-kagent ## Set up the full local Kind development environment +setup-kind-cluster: create-kind-cluster install-agentregistry install-kagent ## Set up the full local Kind development environment .PHONY: dump-kind-state dump-kind-state: ## Dump Kind cluster state for debugging (pods, events, kagent logs) @@ -496,9 +488,7 @@ charts-render-test: charts-deps ## Render chart templates as a smoke test @echo "Rendering chart templates for $(HELM_CHART_DIR)..." $(HELM) template test-release $(HELM_CHART_DIR) \ --values $(HELM_CHART_DIR)/values.yaml \ - --set config.jwtPrivateKey=deadbeef1234567890abcdef12345678 \ - --set database.password=ci-password \ - --set database.host=postgres.example.com + --set config.jwtPrivateKey=deadbeef1234567890abcdef12345678 # Package the chart into $(HELM_PACKAGE_DIR)/. .PHONY: charts-package diff --git a/README.md b/README.md index 090e41dd..1c1f564e 100644 --- a/README.md +++ b/README.md @@ -108,28 +108,26 @@ Open `http://localhost:12121` to use the web UI. ### ☸️ Kubernetes -Run Agent Registry in a cluster when you want shared discovery and deployment workflows. An external PostgreSQL instance with the [pgvector](https://github.com/pgvector/pgvector) extension is required. +Run Agent Registry in a cluster when you want shared discovery and deployment workflows. PostgreSQL with the [pgvector](https://github.com/pgvector/pgvector) extension is **bundled by default** — no separate database setup required for development and testing. -#### PostgreSQL - -Deploy a single-instance PostgreSQL and pgvector into your cluster using the provided example manifest: +#### Install Agent Registry ```bash -kubectl apply -f https://raw.githubusercontent.com/agentregistry-dev/agentregistry/main/examples/postgres-pgvector.yaml -kubectl -n agentregistry wait --for=condition=ready pod -l app=postgres-pgvector --timeout=120s +helm install agentregistry oci://ghcr.io/agentregistry-dev/agentregistry/charts/agentregistry \ + --namespace agentregistry \ + --create-namespace \ + --set config.jwtPrivateKey=$(openssl rand -hex 32) ``` -This setup is intended for development and testing. For production, use a managed PostgreSQL service or a production-grade operator. - -#### Install Agent Registry +For production, disable the bundled database and point to a managed PostgreSQL service: ```bash helm install agentregistry oci://ghcr.io/agentregistry-dev/agentregistry/charts/agentregistry \ --namespace agentregistry \ --create-namespace \ - --set database.host=postgres-pgvector.agentregistry.svc.cluster.local \ - --set database.password=agentregistry \ - --set database.sslMode=disable \ + --set database.bundled.enabled=false \ + --set database.external.host= \ + --set database.external.password= \ --set config.jwtPrivateKey=$(openssl rand -hex 32) ``` diff --git a/scripts/kind/README.md b/scripts/kind/README.md index f37d5d93..d9161cef 100644 --- a/scripts/kind/README.md +++ b/scripts/kind/README.md @@ -20,21 +20,20 @@ This single command sets up the full local environment. ## What It Does -`make setup-kind-cluster` runs three sub-targets in order: +`make setup-kind-cluster` runs two sub-targets in order: 1. **`make create-kind-cluster`** — creates a Kind cluster named `agentregistry` with a local container registry on `localhost:5001` and MetalLB for LoadBalancer support -2. **`make install-postgresql`** — creates the `agentregistry` namespace and deploys standalone PostgreSQL/pgvector using `pgvector/pgvector:0.8.2-pg16` -3. **`make install-agentregistry`** — builds server images, pushes them to the local registry, and Helm installs AgentRegistry connected to the local PostgreSQL +2. **`make install-agentregistry`** — builds server images, pushes them to the local registry, and Helm installs AgentRegistry with a bundled PostgreSQL/pgvector instance You can also run any sub-target individually, e.g. `make install-agentregistry` to redeploy after a code change. ## Database Details -The local PostgreSQL instance is configured as follows: +PostgreSQL/pgvector is bundled in the Helm chart and deployed automatically. The default configuration is: | Setting | Value | |----------|----------------------------------| -| Host | `postgres-pgvector.agentregistry.svc.cluster.local` (in-cluster) | +| Host | `agentregistry-postgresql.agentregistry.svc.cluster.local` (in-cluster) | | Port | `5432` | | Database | `agent-registry` | | Username | `agentregistry` | @@ -45,7 +44,7 @@ The local PostgreSQL instance is configured as follows: Port-forward to access PostgreSQL from your local machine: ```bash -kubectl --context kind-agentregistry port-forward -n agentregistry svc/postgres-pgvector 5432:5432 +kubectl --context kind-agentregistry port-forward -n agentregistry svc/agentregistry-postgresql 5432:5432 ``` Then connect with psql: @@ -119,7 +118,7 @@ JWT_KEY=mysecretkey VERSION=v0.2.0 make setup-kind-cluster Check pod logs: ```bash -kubectl --context kind-agentregistry logs -n agentregistry -l app=postgres-pgvector +kubectl --context kind-agentregistry logs -n agentregistry -l app.kubernetes.io/component=database ``` ### Images not found @@ -156,9 +155,8 @@ make delete-kind-cluster && make setup-kind-cluster ## Scripts -| File | Purpose | -|-----------------------------------------|------------------------------------------------| -| `setup-kind.sh` | Creates Kind cluster with local registry | -| `setup-metallb.sh` | Installs and configures MetalLB | -| `../../examples/postgres-pgvector.yaml` | Kubernetes manifests for standalone PostgreSQL | -| `kind-config.yaml` | Kind cluster configuration | +| File | Purpose | +|--------------------|------------------------------------------| +| `setup-kind.sh` | Creates Kind cluster with local registry | +| `setup-metallb.sh` | Installs and configures MetalLB | +| `kind-config.yaml` | Kind cluster configuration | From 7b6ebfc86d95466f0100fc6b264087cffac7a094 Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Tue, 17 Mar 2026 17:50:47 -0700 Subject: [PATCH 05/15] nits --- charts/agentregistry/values.yaml | 4 +- examples/postgres-pgvector.yaml | 114 ------------------------------- 2 files changed, 2 insertions(+), 116 deletions(-) delete mode 100644 examples/postgres-pgvector.yaml diff --git a/charts/agentregistry/values.yaml b/charts/agentregistry/values.yaml index f78da823..e6178f88 100644 --- a/charts/agentregistry/values.yaml +++ b/charts/agentregistry/values.yaml @@ -5,7 +5,7 @@ global: imageRegistry: "" # -- Global Docker registry secret names imagePullSecrets: [] - # -- Name of an existing Secret containing all credentials (POSTGRES_PASSWORD and AGENT_REGISTRY_JWT_PRIVATE_KEY). Overrides config.existingSecret and database.existingSecret when set. + # -- Name of an existing Secret containing all credentials (POSTGRES_PASSWORD and AGENT_REGISTRY_JWT_PRIVATE_KEY). Overrides config.existingSecret and database.external.existingSecret when set. When database.bundled.enabled=true, this secret must contain POSTGRES_PASSWORD — the chart will not create its own secret and the bundled PostgreSQL pod will reference it directly. existingSecret: "" # @section Common parameters @@ -285,7 +285,7 @@ database: database: "agent-registry" # -- Database user to create username: "agentregistry" - # -- Database password (override in production) + # -- Database password. This default is intentionally weak — always override for any non-local deployment. password: "agentregistry" # -- SSL mode for the bundled PostgreSQL connection sslMode: "disable" diff --git a/examples/postgres-pgvector.yaml b/examples/postgres-pgvector.yaml deleted file mode 100644 index 70de683b..00000000 --- a/examples/postgres-pgvector.yaml +++ /dev/null @@ -1,114 +0,0 @@ ---- -apiVersion: v1 -kind: Namespace -metadata: - name: agentregistry ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: postgres-pgvector-data - namespace: agentregistry -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 5Gi ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: postgres-pgvector - namespace: agentregistry - labels: - app: postgres-pgvector -spec: - replicas: 1 - strategy: - type: Recreate - selector: - matchLabels: - app: postgres-pgvector - template: - metadata: - labels: - app: postgres-pgvector - spec: - securityContext: - fsGroup: 999 - containers: - - name: postgres - image: pgvector/pgvector:0.8.2-pg16 - imagePullPolicy: IfNotPresent - securityContext: - runAsUser: 999 - runAsGroup: 999 - runAsNonRoot: true - allowPrivilegeEscalation: false - ports: - - name: postgres - containerPort: 5432 - protocol: TCP - env: - - name: POSTGRES_DB - value: "agent-registry" - - name: POSTGRES_USER - value: "agentregistry" - - name: POSTGRES_PASSWORD - value: "agentregistry" - - name: PGDATA - value: /var/lib/postgresql/data/pgdata - livenessProbe: - exec: - command: - - sh - - -c - - pg_isready -U agentregistry -d agent-registry - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 6 - successThreshold: 1 - readinessProbe: - exec: - command: - - sh - - -c - - pg_isready -U agentregistry -d agent-registry - initialDelaySeconds: 5 - periodSeconds: 5 - timeoutSeconds: 3 - failureThreshold: 3 - successThreshold: 1 - resources: - requests: - cpu: 250m - memory: 256Mi - limits: - cpu: "1" - memory: 1Gi - volumeMounts: - - name: postgres-data - mountPath: /var/lib/postgresql/data - volumes: - - name: postgres-data - persistentVolumeClaim: - claimName: postgres-pgvector-data ---- -apiVersion: v1 -kind: Service -metadata: - name: postgres-pgvector - namespace: agentregistry - labels: - app: postgres-pgvector -spec: - type: ClusterIP - selector: - app: postgres-pgvector - ports: - - name: postgres - port: 5432 - targetPort: 5432 - protocol: TCP From 8718987bfae22d4d8bdacd329d1f6ce7fc85a1d4 Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Thu, 19 Mar 2026 15:32:06 -0700 Subject: [PATCH 06/15] update based on feedback --- DEVELOPMENT.md | 202 ++---------------- Makefile | 4 + README.md | 11 +- charts/agentregistry/Chart-template.yaml | 2 + charts/agentregistry/templates/NOTES.txt | 33 ++- charts/agentregistry/templates/_helpers.tpl | 90 ++------ .../agentregistry/templates/deployment.yaml | 31 ++- .../templates/postgresql-secret.yaml | 17 ++ .../agentregistry/templates/postgresql.yaml | 22 +- charts/agentregistry/templates/secrets.yaml | 14 +- .../agentregistry/tests/deployment_test.yaml | 102 ++++----- .../agentregistry/tests/postgresql_test.yaml | 138 +++++++++--- charts/agentregistry/tests/secrets_test.yaml | 102 +-------- .../agentregistry/tests/validation_test.yaml | 54 ++--- charts/agentregistry/values.yaml | 97 ++++----- 15 files changed, 328 insertions(+), 591 deletions(-) create mode 100644 charts/agentregistry/templates/postgresql-secret.yaml diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b2f75734..b399270e 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -2,7 +2,7 @@ ## Local Kubernetes Environment -The fastest way to run the full stack locally is with [Kind](https://kind.sigs.k8s.io/). A single `make` target creates the cluster, builds the server image, and installs AgentRegistry via Helm — PostgreSQL/pgvector is bundled and deployed automatically by the Helm chart. +The fastest way to run the full stack locally is with [Kind](https://kind.sigs.k8s.io/). A single `make` target creates the cluster, builds the server image, and installs AgentRegistry via Helm — a PostgreSQL instance with pgvector is bundled and deployed automatically by the Helm chart for local development. ### Prerequisites @@ -24,7 +24,7 @@ This runs two steps in order: | Step | Target | What it does | |------|--------|-------------| | 1 | `create-kind-cluster` | Installs `kind` to `./bin/`, creates Kind cluster + local registry (`localhost:5001`) + MetalLB | -| 2 | `install-agentregistry` | Builds server image, pushes to local registry, Helm installs AgentRegistry (PostgreSQL/pgvector bundled) | +| 2 | `install-agentregistry` | Builds server image, pushes to local registry, Helm installs AgentRegistry (bundled PostgreSQL with pgvector override for local dev) | Each target can also be run independently — useful when iterating on code: @@ -49,7 +49,7 @@ kubectl --context kind-agentregistry port-forward -n agentregistry svc/agentregi # Bundled PostgreSQL (for direct inspection) kubectl --context kind-agentregistry port-forward -n agentregistry svc/agentregistry-postgresql 5432:5432 -psql -h localhost -U agentregistry -d agent-registry +psql -h localhost -U agentregistry -d agentregistry ``` ### Teardown @@ -110,208 +110,30 @@ The UI is available at `http://localhost:12121`. # Architecture Overview -### 1. CLI Layer (cmd/) +**Tech stack:** Go 1.25+ · PostgreSQL + pgvector (pgx) · [Huma](https://huma.rocks/) (OpenAPI) · [Cobra](https://cobra.dev/) (CLI) · Next.js 14 (App Router) · Tailwind CSS · shadcn/ui -Built with [Cobra](https://github.com/spf13/cobra), provides all command-line functionality: - -- **Registry Management**: connect, disconnect, refresh -- **Resource Discovery**: list, search, show -- **Installation**: install, uninstall -- **Configuration**: configure clients -- **UI**: launch web interface - -Each command has placeholder implementations ready to be filled with actual logic. - -### 2. Data Layer (internal/database/) - -Uses **SQLite** for local storage: - -**Tables:** -- `registries` - Connected registries -- `servers` - MCP servers from registries -- `skills` - Skills from registries -- `installations` - Installed resources - -**Location:** `~/.arctl/arctl.db` - -The schema is based on the MCP Registry JSON schema provided, supporting the full `ServerDetail` structure. - -### 3. API Layer (internal/api/) - -Built with [Gin](https://github.com/gin-gonic/gin), provides REST API: - -**Endpoints:** -- `GET /api/health` - Health check -- `GET /api/registries` - List registries -- `GET /api/servers` - List MCP servers -- `GET /api/skills` - List skills -- `GET /api/installations` - List installed resources -- `GET /*` - Serve embedded UI - -**Port:** 8080 (configurable with `--port`) - -### 4. UI Layer (ui/) - -Built with: -- **Framework:** Next.js 14 (App Router) -- **Language:** TypeScript -- **Styling:** Tailwind CSS -- **Components:** shadcn/ui -- **Icons:** Lucide React - -**Features:** -- Dashboard with statistics -- Resource browser (registries, MCP servers, skills) -- Real-time data from API -- Responsive design -- Installation status indicators - -**Build Output:** Static files exported to `internal/registry/api/ui/dist/` - -## Data Flow - -### CLI Command Execution - -``` -User Input - ↓ -Cobra Command (cmd/) - ↓ -Business Logic (TODO) - ↓ -Database Layer (internal/database/) - ↓ -SQLite (~/.arctl/arctl.db) -``` - -### Web UI Request - -``` -Browser Request - ↓ -Gin Router (internal/api/) - ↓ -API Handler - ↓ -Database Query - ↓ -JSON Response - ↓ -React Component (ui/) - ↓ -User Interface -``` - -## Embedding Strategy - -### How It Works - -1. **Build Phase** (`make build-ui`): - - Next.js builds static files - - Output goes to `internal/registry/api/ui/dist/` - -2. **Compile Phase** (`make build-cli`): - - Go's `embed` directive includes entire `ui/dist/` directory - - Files become part of the binary - -3. **Runtime Phase** (`./bin/arctl ui`): - - Gin serves files from embedded FS - - No external dependencies needed - -### Embed Directive - -```go -//go:embed ui/dist/* -var embeddedUI embed.FS -``` - -This embeds all files in `internal/registry/api/ui/dist/` at compile time. +For a detailed breakdown of layers, conventions, and contribution guidelines see [`AGENTS.md`](AGENTS.md). ## Build Process -### Development - ```bash # UI only (hot reload) make dev-ui -# CLI only (quick iteration) -go build -o bin/arctl main.go -``` - -### Production +# Build CLI binary +make build-cli -```bash -# Full build with embedding -make build +# Build server binary +make build-server -# Creates: ./bin/arctl (single binary with UI embedded) +# Build UI static assets +make build-ui ``` -## Extension Points - -### Adding a New CLI Command - -1. Create `cmd/mycommand.go` -2. Define the command with Cobra -3. Add to `rootCmd` in `init()` -4. Implement logic (call database layer) - -### Adding a New API Endpoint - -1. Add handler in `internal/api/server.go` -2. Register route in `StartServer()` -3. Call database layer -4. Return JSON response - -### Adding a New UI Page - -1. Create `ui/app/mypage/page.tsx` -2. Fetch data from `/api/*` endpoints -3. Use shadcn components for UI -4. Rebuild with `make build-ui` - -### Adding Database Tables - -1. Update schema in `internal/database/database.go` -2. Add model in `internal/models/models.go` -3. Add query methods in database package -4. Database auto-migrates on first run - -## Security Considerations - -### Database - -- Stored in user's home directory (`~/.arctl/`) -- No network access -- File permissions: 0755 (directory), default (file) - -### API Server - -- Localhost only by default -- CORS not configured (local use) -- No authentication (local tool) - -### Embedded UI - -- Static files only -- No server-side execution -- Served from memory (embedded) - -## Contributing - -When adding features: - -1. Add placeholder implementations first -2. Create tests (TODO) -3. Update documentation -4. Rebuild with `make build` -5. Test the binary - ## Resources - [Cobra Documentation](https://cobra.dev/) -- [Gin Documentation](https://gin-gonic.com/) +- [Huma Documentation](https://huma.rocks/) - [Next.js Documentation](https://nextjs.org/docs) - [shadcn/ui Components](https://ui.shadcn.com/) - [MCP Protocol Specification](https://spec.modelcontextprotocol.io/) diff --git a/Makefile b/Makefile index 7a01baae..1f1a3f96 100644 --- a/Makefile +++ b/Makefile @@ -355,6 +355,10 @@ endif --set image.tag=$(VERSION) \ --set config.jwtPrivateKey="$$JWT_KEY" \ --set config.enableAnonymousAuth="true" \ + --set database.postgres.bundled.image.repository=pgvector \ + --set database.postgres.bundled.image.name=pgvector \ + --set database.postgres.bundled.image.tag=pg16 \ + --set database.postgres.vectorEnabled=true \ --wait \ --timeout=5m; diff --git a/README.md b/README.md index 1c1f564e..704739ca 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Open `http://localhost:12121` to use the web UI. ### ☸️ Kubernetes -Run Agent Registry in a cluster when you want shared discovery and deployment workflows. PostgreSQL with the [pgvector](https://github.com/pgvector/pgvector) extension is **bundled by default** — no separate database setup required for development and testing. +Run Agent Registry in a cluster when you want shared discovery and deployment workflows. A PostgreSQL instance is **bundled by default** — no separate database setup required for development and testing. #### Install Agent Registry @@ -119,18 +119,19 @@ helm install agentregistry oci://ghcr.io/agentregistry-dev/agentregistry/charts/ --set config.jwtPrivateKey=$(openssl rand -hex 32) ``` -For production, disable the bundled database and point to a managed PostgreSQL service: +For production, disable the bundled database and provide a connection string to your own PostgreSQL service: ```bash helm install agentregistry oci://ghcr.io/agentregistry-dev/agentregistry/charts/agentregistry \ --namespace agentregistry \ --create-namespace \ - --set database.bundled.enabled=false \ - --set database.external.host= \ - --set database.external.password= \ + --set database.postgres.bundled.enabled=false \ + --set database.postgres.url=postgres://:@:5432/ \ --set config.jwtPrivateKey=$(openssl rand -hex 32) ``` +> **Semantic search** requires a vector-enabled PostgreSQL instance. Add `--set database.postgres.vectorEnabled=true` when your database has vector support. The bundled database does not include vector support — semantic search will not be available when using it. + Then port-forward to access the UI: ```bash diff --git a/charts/agentregistry/Chart-template.yaml b/charts/agentregistry/Chart-template.yaml index ed1cf897..e5a65aa0 100644 --- a/charts/agentregistry/Chart-template.yaml +++ b/charts/agentregistry/Chart-template.yaml @@ -26,3 +26,5 @@ annotations: artifacthub.io/images: | - name: agentregistry image: ghcr.io/agentregistry-dev/agentregistry/server:${CHART_VERSION} + - name: postgresql + image: docker.io/library/postgres:16 diff --git a/charts/agentregistry/templates/NOTES.txt b/charts/agentregistry/templates/NOTES.txt index 8bb3dc92..480262d1 100644 --- a/charts/agentregistry/templates/NOTES.txt +++ b/charts/agentregistry/templates/NOTES.txt @@ -10,14 +10,6 @@ Chart: {{ include "agentregistry.chart" . }} Release: {{ .Release.Name }} Namespace: {{ .Release.Namespace }} -{{- if .Values.database.host }} -PostgreSQL: {{ .Values.database.host }}:{{ .Values.database.port }}/{{ .Values.database.database }} -{{- else if .Values.database.url }} -PostgreSQL: external (connection string provided) -{{- else }} -PostgreSQL: *** NOT CONFIGURED *** -{{- end }} - To access the Agent Registry UI and API: 1. Port-forward (quickest): @@ -34,4 +26,29 @@ Useful endpoints (after port-forward): gRPC endpoint (Agent Gateway): - kubectl -n {{ .Release.Namespace }} port-forward svc/{{ $fullName }} {{ $grpcPort }}:{{ $grpcPort }} +{{ if .Values.database.postgres.bundled.enabled -}} +################################################################################ +{{- if and (eq .Values.database.postgres.url "") (eq .Values.database.postgres.urlFile "") }} +# WARNING: BUNDLED DATABASE IN USE # +################################################################################ + The bundled PostgreSQL instance is enabled. It is intended for development and + evaluation only — not suitable for production use. Data may be lost if the + pod is restarted or rescheduled. + + To use an external database, set: + database.postgres.url= or database.postgres.urlFile= +{{- else }} +# NOTE: BUNDLED DATABASE DEPLOYED BUT NOT IN USE BY CONTROLLER # +################################################################################ + The bundled PostgreSQL pod is running, but the controller is connected to an + external database (database.postgres.url or database.postgres.urlFile is set). + + To connect the controller to the bundled instance instead, unset url/urlFile: + database.postgres.url="" + To stop deploying the bundled pod entirely, set: + database.postgres.bundled.enabled=false +{{- end }} +{{- end }} +################################################################################ + {{ include "agentregistry.validateValues" . }} diff --git a/charts/agentregistry/templates/_helpers.tpl b/charts/agentregistry/templates/_helpers.tpl index af43dcb3..c8a6f074 100644 --- a/charts/agentregistry/templates/_helpers.tpl +++ b/charts/agentregistry/templates/_helpers.tpl @@ -86,9 +86,9 @@ Digest takes precedence over tag. {{- end }} {{- end }} {{- if .Values.image.digest }} -{{- printf "%s/%s@%s" $registry .Values.image.repository .Values.image.digest }} +{{- printf "%s/%s/%s@%s" $registry .Values.image.repository .Values.image.name .Values.image.digest }} {{- else }} -{{- printf "%s/%s:%s" $registry .Values.image.repository (.Values.image.tag | default .Chart.AppVersion) }} +{{- printf "%s/%s/%s:%s" $registry .Values.image.repository .Values.image.name (.Values.image.tag | default .Chart.AppVersion) }} {{- end }} {{- end }} @@ -135,58 +135,6 @@ Create the name of the service account to use. {{- end }} {{- end }} -{{/* ====================================================================== - Secret helpers - ====================================================================== */}} - -{{/* -Return the secret name containing AGENT_REGISTRY_JWT_PRIVATE_KEY. -Priority: global.existingSecret → config.existingSecret → chart-managed secret. -*/}} -{{- define "agentregistry.secretName" -}} -{{- .Values.global.existingSecret | default .Values.config.existingSecret | default (include "agentregistry.fullname" .) }} -{{- end }} - -{{/* -Return the secret name that holds POSTGRES_PASSWORD. -Priority: global.existingSecret → database.external.existingSecret → chart-managed secret. -When database.bundled.enabled, external.existingSecret is not meaningful; the chart-managed secret holds the bundled password. -*/}} -{{- define "agentregistry.passwordSecretName" -}} -{{- .Values.global.existingSecret | default .Values.database.external.existingSecret | default (include "agentregistry.fullname" .) }} -{{- end }} - -{{/* ====================================================================== - Database URL - ====================================================================== */}} - -{{/* -Return the PostgreSQL database URL. -When database.bundled.enabled, points at the in-chart PostgreSQL service. -Otherwise: if database.external.url is set, use it directly; else build from individual external fields. -*/}} -{{- define "agentregistry.databaseUrl" -}} -{{- if .Values.database.bundled.enabled }} -{{- printf "postgres://%s:$(%s)@%s:%s/%s?sslmode=%s" - .Values.database.bundled.auth.username - "POSTGRES_PASSWORD" - (include "agentregistry.postgresql.fullname" .) - (toString .Values.database.bundled.service.port) - .Values.database.bundled.auth.database - .Values.database.bundled.sslMode }} -{{- else if .Values.database.external.url }} -{{- .Values.database.external.url }} -{{- else }} -{{- printf "postgres://%s:$(%s)@%s:%s/%s?sslmode=%s" - .Values.database.external.username - "POSTGRES_PASSWORD" - .Values.database.external.host - (toString .Values.database.external.port) - .Values.database.external.database - .Values.database.external.sslMode }} -{{- end }} -{{- end }} - {{/* ====================================================================== Security context helpers ====================================================================== */}} @@ -327,16 +275,8 @@ Full name for the bundled PostgreSQL resources. Standard labels for bundled PostgreSQL resources. */}} {{- define "agentregistry.postgresql.labels" -}} -helm.sh/chart: {{ include "agentregistry.chart" . }} -{{ include "agentregistry.postgresql.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -app.kubernetes.io/part-of: {{ include "agentregistry.name" . }} -{{- if .Values.commonLabels }} -{{ toYaml .Values.commonLabels }} -{{- end }} +{{ include "agentregistry.labels" . }} +app.kubernetes.io/component: database {{- end }} {{/* @@ -353,13 +293,14 @@ Return the bundled PostgreSQL image string. Respects global.imageRegistry override. */}} {{- define "agentregistry.postgresql.image" -}} -{{- $registry := .Values.database.bundled.image.registry -}} +{{- $pg := .Values.database.postgres.bundled -}} +{{- $registry := $pg.image.registry -}} {{- if .Values.global }} {{- if .Values.global.imageRegistry }} {{- $registry = .Values.global.imageRegistry -}} {{- end }} {{- end }} -{{- printf "%s/%s:%s" $registry .Values.database.bundled.image.repository .Values.database.bundled.image.tag }} +{{- printf "%s/%s/%s:%s" $registry $pg.image.repository $pg.image.name $pg.image.tag }} {{- end }} {{/* ====================================================================== @@ -369,22 +310,17 @@ Respects global.imageRegistry override. {{/* Compile hard-error validations. Any non-empty result triggers fail. Called from templates/validate.yaml so it fires during helm template/install. -When database.bundled.enabled, external database host/password validation is skipped. */}} {{- define "agentregistry.validateValues.errors" -}} {{- $errors := list }} -{{- $hasExternalJwt := or .Values.global.existingSecret .Values.config.existingSecret }} -{{- if and (not $hasExternalJwt) (eq .Values.config.jwtPrivateKey "") }} -{{- $errors = append $errors "config.jwtPrivateKey must be set (or provide config.existingSecret / global.existingSecret containing AGENT_REGISTRY_JWT_PRIVATE_KEY)." }} -{{- else if and (not $hasExternalJwt) (not (regexMatch "^[0-9a-fA-F]+$" .Values.config.jwtPrivateKey)) }} +{{- if and (not .Values.config.existingSecret) (eq .Values.config.jwtPrivateKey "") }} +{{- $errors = append $errors "config.jwtPrivateKey must be set (or provide config.existingSecret containing AGENT_REGISTRY_JWT_PRIVATE_KEY)." }} +{{- else if and (not .Values.config.existingSecret) (not (regexMatch "^[0-9a-fA-F]+$" .Values.config.jwtPrivateKey)) }} {{- $errors = append $errors "config.jwtPrivateKey must be a valid hex string (e.g. generated with: openssl rand -hex 32)." }} {{- end }} -{{- if not .Values.database.bundled.enabled }} -{{- if and (not (or .Values.global.existingSecret .Values.database.external.existingSecret)) (not .Values.database.external.url) (eq .Values.database.external.password "") }} -{{- $errors = append $errors "database.external.password must be set (or provide database.external.url, database.external.existingSecret, or global.existingSecret containing POSTGRES_PASSWORD)." }} -{{- end }} -{{- if and (not .Values.database.external.url) (not .Values.database.external.host) }} -{{- $errors = append $errors "database.external.host (or database.external.url) must be set when database.bundled.enabled=false. An external PostgreSQL instance with pgvector is required." }} +{{- if not .Values.database.postgres.bundled.enabled }} +{{- if and (not .Values.database.postgres.url) (not .Values.database.postgres.urlFile) }} +{{- $errors = append $errors "database.postgres.url or database.postgres.urlFile must be set when database.postgres.bundled.enabled=false. An external PostgreSQL instance with pgvector is required." }} {{- end }} {{- end }} {{- range $errors }} diff --git a/charts/agentregistry/templates/deployment.yaml b/charts/agentregistry/templates/deployment.yaml index 192fe417..0bf9ad98 100644 --- a/charts/agentregistry/templates/deployment.yaml +++ b/charts/agentregistry/templates/deployment.yaml @@ -24,6 +24,7 @@ spec: annotations: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} checksum/secret: {{ include (print $.Template.BasePath "/secrets.yaml") . | sha256sum }} + checksum/postgresql-secret: {{ include (print $.Template.BasePath "/postgresql-secret.yaml") . | sha256sum }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} @@ -84,16 +85,16 @@ spec: topologySpreadConstraints: {{- toYaml .Values.topologySpreadConstraints | nindent 8 }} {{- end }} - {{- if .Values.database.bundled.enabled }} + {{- if .Values.database.postgres.bundled.enabled }} initContainers: - name: wait-for-postgres image: {{ include "agentregistry.postgresql.image" . }} - imagePullPolicy: {{ .Values.database.bundled.image.pullPolicy }} + imagePullPolicy: {{ .Values.database.postgres.bundled.image.pullPolicy }} command: - sh - -c - | - until pg_isready -h {{ include "agentregistry.postgresql.fullname" . }} -p {{ .Values.database.bundled.service.port }} -U {{ .Values.database.bundled.auth.username | quote }}; do + until pg_isready -h {{ include "agentregistry.postgresql.fullname" . }} -p 5432 -U agentregistry; do echo "waiting for postgres..."; sleep 2 done {{- end }} @@ -125,18 +126,28 @@ spec: - configMapRef: name: {{ include "agentregistry.fullname" . }}-config env: - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ include "agentregistry.passwordSecretName" . }} - key: POSTGRES_PASSWORD - name: AGENT_REGISTRY_JWT_PRIVATE_KEY valueFrom: secretKeyRef: - name: {{ include "agentregistry.secretName" . }} + name: {{ .Values.config.existingSecret | default (include "agentregistry.fullname" .) }} key: AGENT_REGISTRY_JWT_PRIVATE_KEY + {{- if .Values.database.postgres.urlFile }} + - name: POSTGRES_DATABASE_URL_FILE + value: {{ .Values.database.postgres.urlFile | quote }} + {{- else if .Values.database.postgres.url }} + - name: AGENT_REGISTRY_DATABASE_URL + value: {{ .Values.database.postgres.url | quote }} + {{- else if .Values.database.postgres.bundled.enabled }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "agentregistry.fullname" . }}-postgresql + key: POSTGRES_PASSWORD - name: AGENT_REGISTRY_DATABASE_URL - value: {{ include "agentregistry.databaseUrl" . | quote }} + value: {{ printf "postgres://agentregistry:$(POSTGRES_PASSWORD)@%s:5432/agentregistry?sslmode=disable" (include "agentregistry.postgresql.fullname" .) | quote }} + {{- else }} + {{ fail "No database connection configured. Set database.postgres.url, database.postgres.urlFile, or enable database.postgres.bundled." }} + {{- end }} {{- if .Values.extraEnvVars }} {{- tpl (toYaml .Values.extraEnvVars) . | nindent 12 }} {{- end }} diff --git a/charts/agentregistry/templates/postgresql-secret.yaml b/charts/agentregistry/templates/postgresql-secret.yaml new file mode 100644 index 00000000..8e36abe9 --- /dev/null +++ b/charts/agentregistry/templates/postgresql-secret.yaml @@ -0,0 +1,17 @@ +{{- if .Values.database.postgres.bundled.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "agentregistry.fullname" . }}-postgresql + labels: + {{- include "agentregistry.labels" . | nindent 4 }} + app.kubernetes.io/component: database + {{- $annotations := include "agentregistry.annotations" (dict "annotations" dict "context" $) }} + {{- if $annotations }} + annotations: + {{- $annotations | nindent 4 }} + {{- end }} +type: Opaque +data: + POSTGRES_PASSWORD: {{ "agentregistry" | b64enc | quote }} +{{- end }} diff --git a/charts/agentregistry/templates/postgresql.yaml b/charts/agentregistry/templates/postgresql.yaml index 7e6d2f68..e32317fc 100644 --- a/charts/agentregistry/templates/postgresql.yaml +++ b/charts/agentregistry/templates/postgresql.yaml @@ -1,5 +1,5 @@ -{{- if .Values.database.bundled.enabled }} -{{- $pg := .Values.database.bundled }} +{{- if .Values.database.postgres.bundled.enabled }} +{{- $pg := .Values.database.postgres.bundled }} {{- $fullname := include "agentregistry.postgresql.fullname" . }} --- apiVersion: v1 @@ -60,13 +60,13 @@ spec: protocol: TCP env: - name: POSTGRES_DB - value: {{ $pg.auth.database | quote }} + value: "agentregistry" - name: POSTGRES_USER - value: {{ $pg.auth.username | quote }} + value: "agentregistry" - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: - name: {{ include "agentregistry.passwordSecretName" . }} + name: {{ include "agentregistry.fullname" . }}-postgresql key: POSTGRES_PASSWORD - name: PGDATA value: /var/lib/postgresql/data/pgdata @@ -75,9 +75,9 @@ spec: command: - pg_isready - -U - - {{ $pg.auth.username | quote }} + - agentregistry - -d - - {{ $pg.auth.database | quote }} + - agentregistry initialDelaySeconds: 20 periodSeconds: 10 timeoutSeconds: 5 @@ -88,9 +88,9 @@ spec: command: - pg_isready - -U - - {{ $pg.auth.username | quote }} + - agentregistry - -d - - {{ $pg.auth.database | quote }} + - agentregistry initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 @@ -120,10 +120,10 @@ metadata: {{- $annotations | nindent 4 }} {{- end }} spec: - type: {{ $pg.service.type }} + type: ClusterIP ports: - name: postgresql - port: {{ $pg.service.port }} + port: 5432 targetPort: postgresql protocol: TCP selector: diff --git a/charts/agentregistry/templates/secrets.yaml b/charts/agentregistry/templates/secrets.yaml index 4a1fe197..d4db8273 100644 --- a/charts/agentregistry/templates/secrets.yaml +++ b/charts/agentregistry/templates/secrets.yaml @@ -1,6 +1,5 @@ -{{- $needPgPassword := and (not .Values.global.existingSecret) (or .Values.database.bundled.enabled (not .Values.database.external.existingSecret)) -}} -{{- $needJwtKey := and (not .Values.global.existingSecret) (not .Values.config.existingSecret) -}} -{{- if or $needPgPassword $needJwtKey }} +{{- $needJwtKey := not .Values.config.existingSecret -}} +{{- if $needJwtKey }} apiVersion: v1 kind: Secret metadata: @@ -14,14 +13,5 @@ metadata: {{- end }} type: Opaque data: - {{- if $needPgPassword }} - {{- if .Values.database.bundled.enabled }} - POSTGRES_PASSWORD: {{ .Values.database.bundled.auth.password | toString | b64enc | quote }} - {{- else }} - POSTGRES_PASSWORD: {{ .Values.database.external.password | toString | b64enc | quote }} - {{- end }} - {{- end }} - {{- if $needJwtKey }} AGENT_REGISTRY_JWT_PRIVATE_KEY: {{ .Values.config.jwtPrivateKey | toString | b64enc | quote }} - {{- end }} {{- end }} diff --git a/charts/agentregistry/tests/deployment_test.yaml b/charts/agentregistry/tests/deployment_test.yaml index 3ec6a4e1..b63a789a 100644 --- a/charts/agentregistry/tests/deployment_test.yaml +++ b/charts/agentregistry/tests/deployment_test.yaml @@ -3,6 +3,7 @@ templates: - deployment.yaml - configmap.yaml - secrets.yaml + - postgresql-secret.yaml tests: - it: renders with default values @@ -17,7 +18,7 @@ tests: path: spec.replicas value: 1 - - it: uses server image as repo:tag by default + - it: uses server image as registry/repository/name:tag by default template: deployment.yaml asserts: - matchRegex: @@ -31,7 +32,6 @@ tests: asserts: - matchRegex: path: spec.template.spec.containers[0].image - # allow either lowercase or uppercase hex and permit extra trailing text pattern: "^ghcr.io/agentregistry-dev/agentregistry/server@sha256:[0-9a-fA-F]{64}" @@ -65,7 +65,7 @@ tests: content: app.kubernetes.io/name: agentregistry - - it: injects POSTGRES_PASSWORD from secret + - it: injects POSTGRES_PASSWORD from postgresql secret when bundled is enabled template: deployment.yaml asserts: - contains: @@ -74,7 +74,7 @@ tests: name: POSTGRES_PASSWORD valueFrom: secretKeyRef: - name: RELEASE-NAME-agentregistry + name: RELEASE-NAME-agentregistry-postgresql key: POSTGRES_PASSWORD - it: injects JWT private key from secret @@ -96,35 +96,51 @@ tests: path: spec.template.spec.containers[0].env content: name: AGENT_REGISTRY_DATABASE_URL - value: "postgres://agentregistry:$(POSTGRES_PASSWORD)@RELEASE-NAME-agentregistry-postgresql:5432/agent-registry?sslmode=disable" + value: "postgres://agentregistry:$(POSTGRES_PASSWORD)@RELEASE-NAME-agentregistry-postgresql:5432/agentregistry?sslmode=disable" - - it: sets AGENT_REGISTRY_DATABASE_URL with external host (bundled disabled) + - it: sets AGENT_REGISTRY_DATABASE_URL directly when database.postgres.url is set template: deployment.yaml set: - database.bundled.enabled: false - database.external.host: mydb.example.com - database.external.port: 5432 - database.external.database: mydb - database.external.username: myuser - database.external.sslMode: require + database.postgres.bundled.enabled: false + database.postgres.url: "postgres://user:pass@host:5432/db?sslmode=disable" asserts: - contains: path: spec.template.spec.containers[0].env content: name: AGENT_REGISTRY_DATABASE_URL - value: "postgres://myuser:$(POSTGRES_PASSWORD)@mydb.example.com:5432/mydb?sslmode=require" + value: "postgres://user:pass@host:5432/db?sslmode=disable" - - it: sets AGENT_REGISTRY_DATABASE_URL with external url (bundled disabled) + - it: sets POSTGRES_DATABASE_URL_FILE when database.postgres.urlFile is set template: deployment.yaml set: - database.bundled.enabled: false - database.external.url: "postgres://user:pass@host:5432/db?sslmode=disable" + database.postgres.bundled.enabled: false + database.postgres.urlFile: "/run/secrets/db-url" asserts: - contains: path: spec.template.spec.containers[0].env content: - name: AGENT_REGISTRY_DATABASE_URL - value: "postgres://user:pass@host:5432/db?sslmode=disable" + name: POSTGRES_DATABASE_URL_FILE + value: "/run/secrets/db-url" + + - it: does not inject POSTGRES_PASSWORD when url is set (bundled enabled or disabled) + template: deployment.yaml + set: + database.postgres.url: "postgres://user:pass@host:5432/db" + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: POSTGRES_PASSWORD + + - it: does not inject POSTGRES_PASSWORD when urlFile is set + template: deployment.yaml + set: + database.postgres.urlFile: "/run/secrets/db-url" + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: POSTGRES_PASSWORD - it: applies pod security context by default template: deployment.yaml @@ -204,42 +220,6 @@ tests: name: my-existing-secret key: AGENT_REGISTRY_JWT_PRIVATE_KEY - - it: uses database.external.existingSecret for POSTGRES_PASSWORD when set (bundled disabled) - template: deployment.yaml - set: - database.bundled.enabled: false - database.external.existingSecret: db-secret - asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: db-secret - key: POSTGRES_PASSWORD - - - it: uses global.existingSecret for both AGENT_REGISTRY_JWT_PRIVATE_KEY and POSTGRES_PASSWORD - template: deployment.yaml - set: - global.existingSecret: global-secret - asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: AGENT_REGISTRY_JWT_PRIVATE_KEY - valueFrom: - secretKeyRef: - name: global-secret - key: AGENT_REGISTRY_JWT_PRIVATE_KEY - - contains: - path: spec.template.spec.containers[0].env - content: - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: global-secret - key: POSTGRES_PASSWORD - it: adds checksum annotations for config and secret template: deployment.yaml @@ -272,7 +252,19 @@ tests: - it: does not include init container when bundled is disabled template: deployment.yaml set: - database.bundled.enabled: false + database.postgres.bundled.enabled: false + database.postgres.url: "postgres://user:pass@host:5432/db" asserts: - isNull: path: spec.template.spec.initContainers + + - it: still includes init container when bundled is enabled and url is also set + template: deployment.yaml + set: + database.postgres.url: "postgres://user:pass@host:5432/db" + asserts: + - isNotNull: + path: spec.template.spec.initContainers + - equal: + path: spec.template.spec.initContainers[0].name + value: wait-for-postgres diff --git a/charts/agentregistry/tests/postgresql_test.yaml b/charts/agentregistry/tests/postgresql_test.yaml index 35c888d6..39aa2e10 100644 --- a/charts/agentregistry/tests/postgresql_test.yaml +++ b/charts/agentregistry/tests/postgresql_test.yaml @@ -1,7 +1,7 @@ suite: Bundled PostgreSQL templates: - postgresql.yaml - - secrets.yaml + - postgresql-secret.yaml tests: # ── Bundled enabled (default) ───────────────────────────────────────────── @@ -41,13 +41,25 @@ tests: path: spec.strategy.type value: Recreate - - it: Deployment uses pgvector image + - it: Deployment uses official postgres image as registry/repository/name:tag by default template: postgresql.yaml documentIndex: 1 asserts: - - matchRegex: + - equal: path: spec.template.spec.containers[0].image - pattern: '^docker.io/pgvector/pgvector:pg16$' + value: 'docker.io/library/postgres:16' + + - it: Deployment image can be overridden to pgvector via component fields + template: postgresql.yaml + set: + database.postgres.bundled.image.repository: pgvector + database.postgres.bundled.image.name: pgvector + database.postgres.bundled.image.tag: pg16 + documentIndex: 1 + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: docker.io/pgvector/pgvector:pg16 - it: Deployment has hardcoded pod security context template: postgresql.yaml @@ -75,23 +87,23 @@ tests: path: spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation value: false - - it: Deployment references chart-managed secret for POSTGRES_PASSWORD + - it: Deployment uses hardcoded POSTGRES_DB and POSTGRES_USER template: postgresql.yaml documentIndex: 1 asserts: - contains: path: spec.template.spec.containers[0].env content: - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: RELEASE-NAME-agentregistry - key: POSTGRES_PASSWORD + name: POSTGRES_DB + value: agentregistry + - contains: + path: spec.template.spec.containers[0].env + content: + name: POSTGRES_USER + value: agentregistry - - it: Deployment uses global.existingSecret for POSTGRES_PASSWORD when set + - it: Deployment reads POSTGRES_PASSWORD from chart-managed postgresql secret template: postgresql.yaml - set: - global.existingSecret: global-secret documentIndex: 1 asserts: - contains: @@ -100,9 +112,10 @@ tests: name: POSTGRES_PASSWORD valueFrom: secretKeyRef: - name: global-secret + name: RELEASE-NAME-agentregistry-postgresql key: POSTGRES_PASSWORD + - it: Deployment has liveness and readiness probes template: postgresql.yaml documentIndex: 1 @@ -153,44 +166,47 @@ tests: path: spec.ports[0].port value: 5432 + - it: Service type is hardcoded to ClusterIP + template: postgresql.yaml + documentIndex: 2 + asserts: + - equal: + path: spec.type + value: ClusterIP + # ── Bundled disabled ────────────────────────────────────────────────────── - it: renders nothing when bundled is disabled template: postgresql.yaml set: - database.bundled.enabled: false + database.postgres.bundled.enabled: false + database.postgres.url: "postgres://user:pass@host:5432/db" asserts: - hasDocuments: count: 0 - # ── Custom auth values ──────────────────────────────────────────────────── + - it: still renders when bundled is enabled and url is also set + template: postgresql.yaml + set: + database.postgres.url: "postgres://user:pass@host:5432/db" + asserts: + - hasDocuments: + count: 3 - - it: uses custom bundled auth values + - it: still renders when bundled is enabled and urlFile is also set template: postgresql.yaml set: - database.bundled.auth.database: mydb - database.bundled.auth.username: myuser - documentIndex: 1 + database.postgres.urlFile: "/run/secrets/db-url" asserts: - - isKind: - of: Deployment - - contains: - path: spec.template.spec.containers[0].env - content: - name: POSTGRES_DB - value: mydb - - contains: - path: spec.template.spec.containers[0].env - content: - name: POSTGRES_USER - value: myuser + - hasDocuments: + count: 3 # ── Custom storage size ─────────────────────────────────────────────────── - it: uses custom storage size template: postgresql.yaml set: - database.bundled.storage: 10Gi + database.postgres.bundled.storage: 10Gi documentIndex: 0 asserts: - isKind: @@ -198,3 +214,59 @@ tests: - equal: path: spec.resources.requests.storage value: 10Gi + + # ── postgresql-secret.yaml ──────────────────────────────────────────────── + + - it: creates postgresql secret when bundled is enabled + template: postgresql-secret.yaml + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Secret + - equal: + path: metadata.name + value: RELEASE-NAME-agentregistry-postgresql + - isNotNull: + path: data["POSTGRES_PASSWORD"] + + - it: postgresql secret has hardcoded agentregistry password (base64) + template: postgresql-secret.yaml + asserts: + - equal: + path: data["POSTGRES_PASSWORD"] + # base64("agentregistry") + value: "YWdlbnRyZWdpc3RyeQ==" + + - it: postgresql secret has database component label + template: postgresql-secret.yaml + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/component"] + value: database + + - it: creates postgresql secret even when url is set (bundled still enabled) + template: postgresql-secret.yaml + set: + database.postgres.url: "postgres://user:pass@host:5432/db" + asserts: + - hasDocuments: + count: 1 + + - it: creates postgresql secret even when urlFile is set (bundled still enabled) + template: postgresql-secret.yaml + set: + database.postgres.urlFile: "/run/secrets/db-url" + asserts: + - hasDocuments: + count: 1 + + - it: does not create postgresql secret when bundled is disabled + template: postgresql-secret.yaml + set: + database.postgres.bundled.enabled: false + database.postgres.url: "postgres://user:pass@host:5432/db" + asserts: + - hasDocuments: + count: 0 + diff --git a/charts/agentregistry/tests/secrets_test.yaml b/charts/agentregistry/tests/secrets_test.yaml index ee779df4..e7ca61f6 100644 --- a/charts/agentregistry/tests/secrets_test.yaml +++ b/charts/agentregistry/tests/secrets_test.yaml @@ -5,12 +5,12 @@ templates: tests: # ── Default rendering ─────────────────────────────────────────────────────── - - it: renders one Secret by default (credentials) + - it: renders one Secret by default asserts: - hasDocuments: count: 1 - - it: is the credentials Secret + - it: is an Opaque Secret with standard name and labels asserts: - isKind: of: Secret @@ -20,119 +20,37 @@ tests: - equal: path: type value: Opaque - - - it: credentials Secret has standard labels - asserts: - isSubset: path: metadata.labels content: app.kubernetes.io/name: agentregistry app.kubernetes.io/instance: RELEASE-NAME - - it: credentials Secret contains POSTGRES_PASSWORD key + - it: contains AGENT_REGISTRY_JWT_PRIVATE_KEY asserts: - isNotNull: - path: data["POSTGRES_PASSWORD"] + path: data["AGENT_REGISTRY_JWT_PRIVATE_KEY"] - - it: credentials Secret contains JWT key + - it: does not contain POSTGRES_PASSWORD (now in postgresql-secret.yaml) asserts: - - isNotNull: - path: data["AGENT_REGISTRY_JWT_PRIVATE_KEY"] + - notExists: + path: data["POSTGRES_PASSWORD"] - - it: uses provided jwtPrivateKey value (base64-encoded) + - it: base64-encodes the provided jwtPrivateKey set: config.jwtPrivateKey: deadbeef1234567890abcdef - documentIndex: 0 asserts: - equal: path: data["AGENT_REGISTRY_JWT_PRIVATE_KEY"] # base64("deadbeef1234567890abcdef") value: "ZGVhZGJlZWYxMjM0NTY3ODkwYWJjZGVm" - - it: uses bundled auth password for POSTGRES_PASSWORD when bundled is enabled (default) - set: - database.bundled.auth.password: mybundledpass - documentIndex: 0 - asserts: - - equal: - path: data["POSTGRES_PASSWORD"] - # base64("mybundledpass") - value: "bXlidW5kbGVkcGFzcw==" - - - it: uses database.external.password for POSTGRES_PASSWORD when bundled is disabled - set: - database.bundled.enabled: false - database.external.password: mypassword - documentIndex: 0 - asserts: - - equal: - path: data["POSTGRES_PASSWORD"] - # base64("mypassword") - value: "bXlwYXNzd29yZA==" - - # ── config.existingSecret omits AGENT_REGISTRY_JWT_PRIVATE_KEY ───────────────────────────── - - - it: omits AGENT_REGISTRY_JWT_PRIVATE_KEY when config.existingSecret is set - set: - config.existingSecret: my-creds - asserts: - - hasDocuments: - count: 1 - - notExists: - path: data["AGENT_REGISTRY_JWT_PRIVATE_KEY"] - - isNotNull: - path: data["POSTGRES_PASSWORD"] - - # ── database.external.existingSecret omits POSTGRES_PASSWORD (bundled disabled) ────────── + # ── config.existingSecret suppresses the JWT secret ────────────────────── - - it: omits POSTGRES_PASSWORD when database.external.existingSecret is set (bundled disabled) + - it: renders no Secret when config.existingSecret is set set: - database.bundled.enabled: false - database.external.existingSecret: ext-db-secret - asserts: - - hasDocuments: - count: 1 - - notExists: - path: data["POSTGRES_PASSWORD"] - - isNotNull: - path: data["AGENT_REGISTRY_JWT_PRIVATE_KEY"] - - # ── Both existingSecrets suppress the Secret entirely (bundled disabled) ──── - - - it: renders no Secret when both existingSecrets are set (bundled disabled) - set: - database.bundled.enabled: false config.existingSecret: my-creds - database.external.existingSecret: ext-db-secret asserts: - hasDocuments: count: 0 - # ── global.existingSecret overrides both ───────────────────────────────────── - - - it: renders no Secret when global.existingSecret is set - set: - global.existingSecret: global-secret - asserts: - - hasDocuments: - count: 0 - - - it: global.existingSecret overrides config.existingSecret and database.external.existingSecret - set: - global.existingSecret: global-secret - config.existingSecret: my-creds - database.external.existingSecret: ext-db-secret - asserts: - - hasDocuments: - count: 0 - - # ── Bundled enabled: Secret is always created (unless global.existingSecret) ─ - - - it: renders Secret even when database.external.existingSecret is set if bundled is enabled - set: - database.external.existingSecret: ext-db-secret - asserts: - - hasDocuments: - count: 1 - - isNotNull: - path: data["POSTGRES_PASSWORD"] diff --git a/charts/agentregistry/tests/validation_test.yaml b/charts/agentregistry/tests/validation_test.yaml index 2779a641..16cb584d 100644 --- a/charts/agentregistry/tests/validation_test.yaml +++ b/charts/agentregistry/tests/validation_test.yaml @@ -12,12 +12,11 @@ tests: - hasDocuments: count: 0 - - it: renders nothing when jwtPrivateKey and database.external.password are set (bundled disabled) + - it: renders nothing when jwtPrivateKey and database.postgres.url are set (bundled disabled) set: config.jwtPrivateKey: deadbeef1234567890abcdef - database.bundled.enabled: false - database.external.password: changeme - database.external.host: mydb.example.com + database.postgres.bundled.enabled: false + database.postgres.url: "postgres://user:pass@host:5432/db" asserts: - hasDocuments: count: 0 @@ -25,23 +24,15 @@ tests: - it: renders nothing when config.existingSecret is set (bundled disabled) set: config.existingSecret: my-existing-secret - database.bundled.enabled: false - database.external.password: changeme - database.external.host: mydb.example.com - asserts: - - hasDocuments: - count: 0 - - - it: renders nothing when global.existingSecret is set - set: - global.existingSecret: global-secret + database.postgres.bundled.enabled: false + database.postgres.url: "postgres://user:pass@host:5432/db" asserts: - hasDocuments: count: 0 - it: fails when jwtPrivateKey is empty and no existingSecret is provided set: - database.bundled.enabled: true + database.postgres.bundled.enabled: true asserts: - failedTemplate: errorPattern: "CHART CONFIGURATION ERROR" @@ -55,50 +46,37 @@ tests: # ── Bundled PostgreSQL skips external validation ────────────────────────── - - it: renders nothing with bundled enabled even when no external host or password + - it: renders nothing with bundled enabled even when no url or urlFile set: config.jwtPrivateKey: deadbeef1234567890abcdef asserts: - hasDocuments: count: 0 - # ── Database password validation (bundled disabled) ─────────────────────── + # ── Database URL validation (bundled disabled) ──────────────────────────── - - it: fails when database.external.password is empty and no existingSecret (bundled disabled) + - it: fails when bundled is disabled and neither url nor urlFile is set set: config.jwtPrivateKey: deadbeef1234567890abcdef - database.bundled.enabled: false - database.external.host: mydb.example.com + database.postgres.bundled.enabled: false asserts: - failedTemplate: errorPattern: "CHART CONFIGURATION ERROR" - - it: renders nothing when database.external.existingSecret is set (bundled disabled) + - it: renders nothing when database.postgres.url is set (bundled disabled) set: config.jwtPrivateKey: deadbeef1234567890abcdef - database.bundled.enabled: false - database.external.existingSecret: pg-secret - database.external.host: mydb.example.com + database.postgres.bundled.enabled: false + database.postgres.url: "postgres://user:pass@host:5432/db" asserts: - hasDocuments: count: 0 - - it: renders nothing when database.external.url is set (bundled disabled) + - it: renders nothing when database.postgres.urlFile is set (bundled disabled) set: config.jwtPrivateKey: deadbeef1234567890abcdef - database.bundled.enabled: false - database.external.url: "postgres://user:pass@host:5432/db" + database.postgres.bundled.enabled: false + database.postgres.urlFile: "/run/secrets/db-url" asserts: - hasDocuments: count: 0 - - # ── Database host validation (bundled disabled) ─────────────────────────── - - - it: fails when database.external.host and database.external.url are both unset (bundled disabled) - set: - config.jwtPrivateKey: deadbeef1234567890abcdef - database.bundled.enabled: false - database.external.password: changeme - asserts: - - failedTemplate: - errorPattern: "CHART CONFIGURATION ERROR" diff --git a/charts/agentregistry/values.yaml b/charts/agentregistry/values.yaml index e6178f88..9259c387 100644 --- a/charts/agentregistry/values.yaml +++ b/charts/agentregistry/values.yaml @@ -5,8 +5,6 @@ global: imageRegistry: "" # -- Global Docker registry secret names imagePullSecrets: [] - # -- Name of an existing Secret containing all credentials (POSTGRES_PASSWORD and AGENT_REGISTRY_JWT_PRIVATE_KEY). Overrides config.existingSecret and database.external.existingSecret when set. When database.bundled.enabled=true, this secret must contain POSTGRES_PASSWORD — the chart will not create its own secret and the bundled PostgreSQL pod will reference it directly. - existingSecret: "" # @section Common parameters @@ -24,8 +22,10 @@ commonAnnotations: {} image: # -- Agent Registry image registry registry: ghcr.io - # -- Agent Registry image repository - repository: agentregistry-dev/agentregistry/server + # -- Agent Registry image repository (org/path, excluding the image name) + repository: agentregistry-dev/agentregistry + # -- Agent Registry image name + name: server # -- Agent Registry image tag (immutable tags recommended) tag: "0.2.1" # -- Agent Registry image digest (overrides tag if set) @@ -53,7 +53,7 @@ config: disableBuiltinSeed: "true" # -- Path or URL to external seed data file (leave empty to disable external seeding) seedFrom: "" - # -- Name of an existing Secret containing AGENT_REGISTRY_JWT_PRIVATE_KEY. Use global.existingSecret if you want a single secret for both AGENT_REGISTRY_JWT_PRIVATE_KEY and POSTGRES_PASSWORD. + # -- Name of an existing Secret containing AGENT_REGISTRY_JWT_PRIVATE_KEY. When set, the chart will not create its own JWT secret. existingSecret: "" # -- Hex-encoded HMAC key for signing JWT tokens (must be valid hex, e.g. "a3f4b2c1..."). Ignored when existingSecret is set. jwtPrivateKey: "" @@ -267,59 +267,36 @@ rbac: # @section Database database: - # -- Bundled PostgreSQL (dev/test). Enabled by default; set bundled.enabled=false to bring your own. - bundled: - # -- Deploy a PostgreSQL instance alongside Agent Registry - enabled: true - image: - # -- Bundled PostgreSQL image registry - registry: docker.io - # -- Bundled PostgreSQL image repository - repository: pgvector/pgvector - # -- Bundled PostgreSQL image tag - tag: "pg16" - # -- Bundled PostgreSQL image pull policy - pullPolicy: IfNotPresent - auth: - # -- Database name to create - database: "agent-registry" - # -- Database user to create - username: "agentregistry" - # -- Database password. This default is intentionally weak — always override for any non-local deployment. - password: "agentregistry" - # -- SSL mode for the bundled PostgreSQL connection - sslMode: "disable" - service: - # -- Service type for bundled PostgreSQL - type: ClusterIP - # -- Service port for bundled PostgreSQL - port: 5432 - # -- PersistentVolumeClaim size for the bundled PostgreSQL data directory - storage: 5Gi - # -- Resource requests/limits for the bundled PostgreSQL container - resources: - requests: - cpu: 250m - memory: 256Mi - limits: - cpu: "1" - memory: 1Gi - - # -- External PostgreSQL settings (used when bundled.enabled=false) - external: - # -- Full PostgreSQL connection string (overrides all other external fields when set) + postgres: + # -- External PostgreSQL connection string. Used if set, regardless of bundled.enabled. url: "" - # -- External database host - host: "" - # -- External database port - port: 5432 - # -- External database name - database: "agent-registry" - # -- External database user - username: "agentregistry" - # -- External database password (ignored when existingSecret is set) - password: "" - # -- Name of an existing Secret containing the database password (key: POSTGRES_PASSWORD) - existingSecret: "" - # -- External database SSL mode (require, verify-ca, verify-full, disable) - sslMode: "require" + # -- Path to a file containing the database URL. Takes precedence over url. + urlFile: "" + # -- Enable pgvector extension migration. Set true if your DB has pgvector. + vectorEnabled: false + # -- Bundled PostgreSQL — dev/eval only. + bundled: + # -- Deploy a PostgreSQL instance alongside Agent Registry + enabled: true + image: + # -- Bundled PostgreSQL image registry + registry: docker.io + # -- Bundled PostgreSQL image repository (org/namespace) + repository: library + # -- Bundled PostgreSQL image name + name: postgres + # -- Bundled PostgreSQL image tag + tag: "16" + # -- Bundled PostgreSQL image pull policy + pullPolicy: IfNotPresent + # -- DB name, user, and password are hardcoded ("agentregistry") for the bundled instance. + # -- PersistentVolumeClaim size for the bundled PostgreSQL data directory + storage: 5Gi + # -- Resource requests/limits for the bundled PostgreSQL container + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi From b12a33c2f60c0793b736543a508acd197004bf04 Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Thu, 19 Mar 2026 16:45:37 -0700 Subject: [PATCH 07/15] fixes --- Makefile | 2 +- charts/agentregistry/README.md.gotmpl | 104 +++++++++++------- .../agentregistry/templates/deployment.yaml | 4 + scripts/kind/setup-kind.sh | 2 +- 4 files changed, 70 insertions(+), 42 deletions(-) diff --git a/Makefile b/Makefile index 1f1a3f96..29077f51 100644 --- a/Makefile +++ b/Makefile @@ -351,7 +351,7 @@ endif --create-namespace \ --set image.pullPolicy=Always \ --set image.registry=$(DOCKER_REGISTRY) \ - --set image.repository=$(DOCKER_REPO)/server \ + --set image.repository=$(DOCKER_REPO) \ --set image.tag=$(VERSION) \ --set config.jwtPrivateKey="$$JWT_KEY" \ --set config.enableAnonymousAuth="true" \ diff --git a/charts/agentregistry/README.md.gotmpl b/charts/agentregistry/README.md.gotmpl index e03290a4..49cd4e09 100644 --- a/charts/agentregistry/README.md.gotmpl +++ b/charts/agentregistry/README.md.gotmpl @@ -9,31 +9,39 @@ ## TL;DR ```console -helm install my-agentregistry oci://ghcr.io/agentregistry-dev/helm/agentregistry \ - --set config.jwtPrivateKey=$(openssl rand -hex 32) \ - --set database.host=my-postgres.example.com \ - --set database.password=changeme +helm install my-agentregistry oci://ghcr.io/agentregistry-dev/agentregistry/charts/agentregistry \ + --set config.jwtPrivateKey=$(openssl rand -hex 32) ``` +A PostgreSQL instance is bundled and started automatically — no external database is required to get started. + ## Introduction This chart bootstraps an [Agent Registry](https://github.com/agentregistry-dev/agentregistry) deployment on a [Kubernetes](https://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. -It exposes both an HTTP REST API and an Agent Gateway gRPC endpoint. An external PostgreSQL database with the [pgvector](https://github.com/pgvector/pgvector) extension is required. +It exposes both an HTTP REST API and an Agent Gateway gRPC endpoint. A PostgreSQL instance is bundled by default for development and evaluation. For production, disable the bundled database and supply a connection string to your own PostgreSQL service. ## Prerequisites - Kubernetes 1.19+ - Helm 3.8+ -- PostgreSQL 14+ with pgvector extension ## Installing the Chart +### Development / evaluation (bundled database) + +```console +helm install my-agentregistry oci://ghcr.io/agentregistry-dev/agentregistry/charts/agentregistry \ + --set config.jwtPrivateKey=$(openssl rand -hex 32) +``` + +### Production (external database) + ```console helm install my-agentregistry oci://ghcr.io/agentregistry-dev/agentregistry/charts/agentregistry \ --set config.jwtPrivateKey=$(openssl rand -hex 32) \ - --set database.host=my-postgres.example.com \ - --set database.password=changeme + --set database.postgres.bundled.enabled=false \ + --set database.postgres.url=postgres://:@:5432/ ``` The command deploys Agent Registry on the Kubernetes cluster using the default configuration. The [Parameters](#parameters) section lists the values that can be configured during installation. @@ -55,65 +63,78 @@ The chart deploys the following Kubernetes resources: | Deployment | Agent Registry application | | Service | Exposes HTTP (`:12121`) and gRPC (`:21212`) ports | | ConfigMap | Application configuration injected as environment variables | -| Secret | PostgreSQL password and JWT signing key (omitted when both `existingSecret` values are set) | +| Secret | JWT signing key (omitted when `config.existingSecret` is set) | +| Secret (`-postgresql`) | Bundled PostgreSQL password (only when `database.postgres.bundled.enabled=true`) | +| Deployment (`-postgresql`) | Bundled PostgreSQL instance (only when `database.postgres.bundled.enabled=true`) | +| Service (`-postgresql`) | Internal service for bundled PostgreSQL (only when `database.postgres.bundled.enabled=true`) | +| PersistentVolumeClaim (`-postgresql`) | Storage for bundled PostgreSQL data (only when `database.postgres.bundled.enabled=true`) | | ServiceAccount | Dedicated service account for the workload | | ClusterRole / ClusterRoleBinding | RBAC rules for managing cluster-scoped resources and namespace discovery | | Role / RoleBinding | Per-namespace RBAC rules when `rbac.watchedNamespaces` is set | ## Database -Agent Registry requires an external PostgreSQL database with the pgvector extension. Provide connection details via individual fields: +### Bundled PostgreSQL (default) + +By default, the chart deploys a PostgreSQL instance alongside Agent Registry. It is suitable for development and evaluation only — data may be lost if the pod is restarted or rescheduled. ```yaml database: - host: "my-postgres.example.com" - database: "agent-registry" - username: "agentregistry" - password: "changeme" - sslMode: "require" + postgres: + bundled: + enabled: true # default ``` -Or supply a full connection URL: +### External PostgreSQL + +For production, disable the bundled database and provide a connection string: ```yaml database: - url: "postgresql://agentregistry:changeme@my-postgres.example.com:5432/agent-registry?sslmode=require" + postgres: + bundled: + enabled: false + url: "postgres://agentregistry:changeme@my-postgres.example.com:5432/agentregistry?sslmode=require" ``` -To use an existing Secret for the database password (key: `POSTGRES_PASSWORD`): +To supply the connection string from a file (for example, a mounted Secret): ```yaml database: - existingSecret: "pg-secret" + postgres: + bundled: + enabled: false + urlFile: "/etc/secrets/db-url" ``` -## Secrets +### Semantic search -Agent Registry requires a hex-encoded JWT signing key. Provide it directly: +Semantic search requires a vector-enabled PostgreSQL instance. Set `database.postgres.vectorEnabled=true` to enable it: ```yaml -config: - jwtPrivateKey: "$(openssl rand -hex 32)" +database: + postgres: + vectorEnabled: true ``` -Or reference an existing Secret (key: `AGENT_REGISTRY_JWT_PRIVATE_KEY`): +This enables the `AGENT_REGISTRY_EMBEDDINGS_ENABLED` environment variable on the Agent Registry pod. The bundled database does not include vector support — semantic search will not be available when using it. + +## Secrets + +Agent Registry requires a hex-encoded JWT signing key. Provide it directly: ```yaml config: - existingSecret: "my-agentregistry-secret" + jwtPrivateKey: "$(openssl rand -hex 32)" ``` -To use a single Secret for all credentials (`POSTGRES_PASSWORD` and `AGENT_REGISTRY_JWT_PRIVATE_KEY`): +Or reference an existing Secret (must contain key `AGENT_REGISTRY_JWT_PRIVATE_KEY`): ```yaml -global: +config: existingSecret: "my-agentregistry-secret" ``` -`global.existingSecret` takes precedence over both `config.existingSecret` and `database.existingSecret`. When set, no chart-managed Secret is created. - -When `config.existingSecret` and `database.existingSecret` are set independently, only the keys not covered by an external secret are written into the chart-managed Secret. - ## RBAC By default, Agent Registry is granted cluster-wide access via a `ClusterRole`/`ClusterRoleBinding`. To restrict it to specific namespaces, set `rbac.watchedNamespaces`: @@ -137,9 +158,10 @@ This creates a `Role`/`RoleBinding` in each listed namespace (plus the installat ```yaml database: - host: "pg.example.com" - existingSecret: "pg-secret" - sslMode: "require" + postgres: + bundled: + enabled: false + url: "postgres://agentregistry:changeme@pg.example.com:5432/agentregistry?sslmode=require" config: existingSecret: "agentregistry-secrets" @@ -153,15 +175,17 @@ resources: memory: 512Mi ``` -### Single secret for all credentials +### Production with connection string from a mounted file ```yaml -global: - existingSecret: "agentregistry-all-secrets" - database: - host: "pg.example.com" - sslMode: "require" + postgres: + bundled: + enabled: false + urlFile: "/etc/secrets/db-url" + +config: + existingSecret: "agentregistry-secrets" ``` ### Namespace-scoped RBAC diff --git a/charts/agentregistry/templates/deployment.yaml b/charts/agentregistry/templates/deployment.yaml index 0bf9ad98..6f630a2e 100644 --- a/charts/agentregistry/templates/deployment.yaml +++ b/charts/agentregistry/templates/deployment.yaml @@ -148,6 +148,10 @@ spec: {{- else }} {{ fail "No database connection configured. Set database.postgres.url, database.postgres.urlFile, or enable database.postgres.bundled." }} {{- end }} + {{- if .Values.database.postgres.vectorEnabled }} + - name: AGENT_REGISTRY_EMBEDDINGS_ENABLED + value: "true" + {{- end }} {{- if .Values.extraEnvVars }} {{- tpl (toYaml .Values.extraEnvVars) . | nindent 12 }} {{- end }} diff --git a/scripts/kind/setup-kind.sh b/scripts/kind/setup-kind.sh index 7acca701..4e7499d8 100755 --- a/scripts/kind/setup-kind.sh +++ b/scripts/kind/setup-kind.sh @@ -35,7 +35,7 @@ REG_PORT=${REG_PORT:-} # # We copy kind-config.yaml to a temp file before patching so we never mutate # the tracked source file. -TMP_CONFIG=$(mktemp --suffix=.yaml) +TMP_CONFIG=$(mktemp /tmp/kind-config.XXXXXX.yaml) trap 'rm -f "${TMP_CONFIG}"' EXIT cp "${SCRIPT_DIR}/kind-config.yaml" "${TMP_CONFIG}" From 973f054ed2220092deb5f41c2d1e06421f9cefe8 Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Thu, 19 Mar 2026 17:06:08 -0700 Subject: [PATCH 08/15] update postgres 16 -> 18 --- charts/agentregistry/Chart-template.yaml | 2 +- charts/agentregistry/tests/postgresql_test.yaml | 2 +- charts/agentregistry/values.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/agentregistry/Chart-template.yaml b/charts/agentregistry/Chart-template.yaml index e5a65aa0..9840eabf 100644 --- a/charts/agentregistry/Chart-template.yaml +++ b/charts/agentregistry/Chart-template.yaml @@ -27,4 +27,4 @@ annotations: - name: agentregistry image: ghcr.io/agentregistry-dev/agentregistry/server:${CHART_VERSION} - name: postgresql - image: docker.io/library/postgres:16 + image: docker.io/library/postgres:18 diff --git a/charts/agentregistry/tests/postgresql_test.yaml b/charts/agentregistry/tests/postgresql_test.yaml index 39aa2e10..fc7a8568 100644 --- a/charts/agentregistry/tests/postgresql_test.yaml +++ b/charts/agentregistry/tests/postgresql_test.yaml @@ -47,7 +47,7 @@ tests: asserts: - equal: path: spec.template.spec.containers[0].image - value: 'docker.io/library/postgres:16' + value: 'docker.io/library/postgres:18' - it: Deployment image can be overridden to pgvector via component fields template: postgresql.yaml diff --git a/charts/agentregistry/values.yaml b/charts/agentregistry/values.yaml index 9259c387..98add5ff 100644 --- a/charts/agentregistry/values.yaml +++ b/charts/agentregistry/values.yaml @@ -286,7 +286,7 @@ database: # -- Bundled PostgreSQL image name name: postgres # -- Bundled PostgreSQL image tag - tag: "16" + tag: "18" # -- Bundled PostgreSQL image pull policy pullPolicy: IfNotPresent # -- DB name, user, and password are hardcoded ("agentregistry") for the bundled instance. From 1f3221ea808d9e3f630e07230a1fe2d34990db4e Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Thu, 19 Mar 2026 17:26:45 -0700 Subject: [PATCH 09/15] nits --- charts/agentregistry/templates/_helpers.tpl | 26 +++++++-------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/charts/agentregistry/templates/_helpers.tpl b/charts/agentregistry/templates/_helpers.tpl index c8a6f074..6a99670d 100644 --- a/charts/agentregistry/templates/_helpers.tpl +++ b/charts/agentregistry/templates/_helpers.tpl @@ -75,20 +75,15 @@ Usage: include "agentregistry.annotations" (dict "annotations" .Values.someAnnot {{/* Return the proper Agent Registry image name. -Uses global.imageRegistry as override if set. -Digest takes precedence over tag. +global.imageRegistry overrides image.registry. Digest takes precedence over tag. */}} {{- define "agentregistry.image" -}} -{{- $registry := .Values.image.registry -}} -{{- if .Values.global }} - {{- if .Values.global.imageRegistry }} - {{- $registry = .Values.global.imageRegistry -}} - {{- end }} -{{- end }} +{{- $registry := coalesce (.Values.global).imageRegistry .Values.image.registry }} +{{- $tag := coalesce .Values.image.tag .Chart.AppVersion }} {{- if .Values.image.digest }} {{- printf "%s/%s/%s@%s" $registry .Values.image.repository .Values.image.name .Values.image.digest }} {{- else }} -{{- printf "%s/%s/%s:%s" $registry .Values.image.repository .Values.image.name (.Values.image.tag | default .Chart.AppVersion) }} +{{- printf "%s/%s/%s:%s" $registry .Values.image.repository .Values.image.name $tag }} {{- end }} {{- end }} @@ -290,17 +285,12 @@ app.kubernetes.io/component: database {{/* Return the bundled PostgreSQL image string. -Respects global.imageRegistry override. +global.imageRegistry overrides image.registry. */}} {{- define "agentregistry.postgresql.image" -}} -{{- $pg := .Values.database.postgres.bundled -}} -{{- $registry := $pg.image.registry -}} -{{- if .Values.global }} - {{- if .Values.global.imageRegistry }} - {{- $registry = .Values.global.imageRegistry -}} - {{- end }} -{{- end }} -{{- printf "%s/%s/%s:%s" $registry $pg.image.repository $pg.image.name $pg.image.tag }} +{{- $pg := .Values.database.postgres.bundled.image }} +{{- $registry := coalesce (.Values.global).imageRegistry $pg.registry }} +{{- printf "%s/%s/%s:%s" $registry $pg.repository $pg.name $pg.tag }} {{- end }} {{/* ====================================================================== From ca8d895b2b9b886db619f688b9801c0b8bec73e1 Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Thu, 19 Mar 2026 17:50:12 -0700 Subject: [PATCH 10/15] copilot feedback --- charts/agentregistry/templates/NOTES.txt | 6 +-- .../agentregistry/templates/deployment.yaml | 6 ++- .../agentregistry/tests/deployment_test.yaml | 11 ++--- charts/agentregistry/values.yaml | 2 +- internal/cli/export.go | 2 +- internal/cli/import.go | 2 +- internal/registry/config/config.go | 27 ++++++++++++ internal/registry/database/migrate.go | 14 +++++++ .../migrations/001_initial_schema.sql | 41 ++++--------------- .../migrations_vector/001_vector_support.sql | 24 +++++++++++ internal/registry/database/postgres.go | 9 +++- internal/registry/database/testutil.go | 4 +- internal/registry/registry_app.go | 2 +- 13 files changed, 98 insertions(+), 52 deletions(-) create mode 100644 internal/registry/database/migrations_vector/001_vector_support.sql diff --git a/charts/agentregistry/templates/NOTES.txt b/charts/agentregistry/templates/NOTES.txt index 480262d1..faf98ace 100644 --- a/charts/agentregistry/templates/NOTES.txt +++ b/charts/agentregistry/templates/NOTES.txt @@ -38,12 +38,12 @@ gRPC endpoint (Agent Gateway): To use an external database, set: database.postgres.url= or database.postgres.urlFile= {{- else }} -# NOTE: BUNDLED DATABASE DEPLOYED BUT NOT IN USE BY CONTROLLER # +# NOTE: BUNDLED DATABASE DEPLOYED BUT NOT IN USE # ################################################################################ - The bundled PostgreSQL pod is running, but the controller is connected to an + The bundled PostgreSQL pod is running, but Agent Registry is connected to an external database (database.postgres.url or database.postgres.urlFile is set). - To connect the controller to the bundled instance instead, unset url/urlFile: + To connect Agent Registry to the bundled instance instead, unset url/urlFile: database.postgres.url="" To stop deploying the bundled pod entirely, set: database.postgres.bundled.enabled=false diff --git a/charts/agentregistry/templates/deployment.yaml b/charts/agentregistry/templates/deployment.yaml index 6f630a2e..c2154210 100644 --- a/charts/agentregistry/templates/deployment.yaml +++ b/charts/agentregistry/templates/deployment.yaml @@ -85,7 +85,7 @@ spec: topologySpreadConstraints: {{- toYaml .Values.topologySpreadConstraints | nindent 8 }} {{- end }} - {{- if .Values.database.postgres.bundled.enabled }} + {{- if and .Values.database.postgres.bundled.enabled (eq .Values.database.postgres.url "") (eq .Values.database.postgres.urlFile "") }} initContainers: - name: wait-for-postgres image: {{ include "agentregistry.postgresql.image" . }} @@ -132,7 +132,7 @@ spec: name: {{ .Values.config.existingSecret | default (include "agentregistry.fullname" .) }} key: AGENT_REGISTRY_JWT_PRIVATE_KEY {{- if .Values.database.postgres.urlFile }} - - name: POSTGRES_DATABASE_URL_FILE + - name: AGENT_REGISTRY_DATABASE_URL_FILE value: {{ .Values.database.postgres.urlFile | quote }} {{- else if .Values.database.postgres.url }} - name: AGENT_REGISTRY_DATABASE_URL @@ -151,6 +151,8 @@ spec: {{- if .Values.database.postgres.vectorEnabled }} - name: AGENT_REGISTRY_EMBEDDINGS_ENABLED value: "true" + - name: AGENT_REGISTRY_DATABASE_VECTOR_ENABLED + value: "true" {{- end }} {{- if .Values.extraEnvVars }} {{- tpl (toYaml .Values.extraEnvVars) . | nindent 12 }} diff --git a/charts/agentregistry/tests/deployment_test.yaml b/charts/agentregistry/tests/deployment_test.yaml index b63a789a..34a6ded4 100644 --- a/charts/agentregistry/tests/deployment_test.yaml +++ b/charts/agentregistry/tests/deployment_test.yaml @@ -110,7 +110,7 @@ tests: name: AGENT_REGISTRY_DATABASE_URL value: "postgres://user:pass@host:5432/db?sslmode=disable" - - it: sets POSTGRES_DATABASE_URL_FILE when database.postgres.urlFile is set + - it: sets AGENT_REGISTRY_DATABASE_URL_FILE when database.postgres.urlFile is set template: deployment.yaml set: database.postgres.bundled.enabled: false @@ -119,7 +119,7 @@ tests: - contains: path: spec.template.spec.containers[0].env content: - name: POSTGRES_DATABASE_URL_FILE + name: AGENT_REGISTRY_DATABASE_URL_FILE value: "/run/secrets/db-url" - it: does not inject POSTGRES_PASSWORD when url is set (bundled enabled or disabled) @@ -258,13 +258,10 @@ tests: - isNull: path: spec.template.spec.initContainers - - it: still includes init container when bundled is enabled and url is also set + - it: does not include init container when bundled is enabled but url is also set template: deployment.yaml set: database.postgres.url: "postgres://user:pass@host:5432/db" asserts: - - isNotNull: + - isNull: path: spec.template.spec.initContainers - - equal: - path: spec.template.spec.initContainers[0].name - value: wait-for-postgres diff --git a/charts/agentregistry/values.yaml b/charts/agentregistry/values.yaml index 98add5ff..06969b32 100644 --- a/charts/agentregistry/values.yaml +++ b/charts/agentregistry/values.yaml @@ -272,7 +272,7 @@ database: url: "" # -- Path to a file containing the database URL. Takes precedence over url. urlFile: "" - # -- Enable pgvector extension migration. Set true if your DB has pgvector. + # -- Enable vector schema migrations and embeddings/semantic search (sets AGENT_REGISTRY_DATABASE_VECTOR_ENABLED and AGENT_REGISTRY_EMBEDDINGS_ENABLED). Requires a pgvector-capable PostgreSQL instance. vectorEnabled: false # -- Bundled PostgreSQL — dev/eval only. bundled: diff --git a/internal/cli/export.go b/internal/cli/export.go index 25fdb6a4..089b8eb3 100644 --- a/internal/cli/export.go +++ b/internal/cli/export.go @@ -41,7 +41,7 @@ var ExportCmd = &cobra.Command{ // so that the authn middleware extracts the session and stores in the context. (which the db can use to authorize queries) authz := auth.Authorizer{Authz: nil} - db, err := database.NewPostgreSQL(ctx, cfg.DatabaseURL, authz) + db, err := database.NewPostgreSQL(ctx, cfg.DatabaseURL, authz, cfg.DatabaseVectorEnabled) if err != nil { return fmt.Errorf("failed to connect to database: %w", err) } diff --git a/internal/cli/import.go b/internal/cli/import.go index 08c6fa70..2515113c 100644 --- a/internal/cli/import.go +++ b/internal/cli/import.go @@ -55,7 +55,7 @@ var ImportCmd = &cobra.Command{ // so that the authn middleware extracts the session and stores in the context. (which the db can use to authorize queries) authz := auth.Authorizer{Authz: nil} - db, err := database.NewPostgreSQL(ctx, cfg.DatabaseURL, authz) + db, err := database.NewPostgreSQL(ctx, cfg.DatabaseURL, authz, cfg.DatabaseVectorEnabled) if err != nil { return fmt.Errorf("failed to connect to database: %w", err) } diff --git a/internal/registry/config/config.go b/internal/registry/config/config.go index ce0e71e1..bffa5c0e 100644 --- a/internal/registry/config/config.go +++ b/internal/registry/config/config.go @@ -3,8 +3,10 @@ package config import ( "crypto/rand" "encoding/hex" + "fmt" "log/slog" "os" + "strings" env "github.com/caarlos0/env/v11" "github.com/joho/godotenv" @@ -16,6 +18,8 @@ type Config struct { ServerAddress string `env:"SERVER_ADDRESS" envDefault:":8080"` MCPPort uint16 `env:"MCP_PORT" envDefault:"0"` DatabaseURL string `env:"DATABASE_URL" envDefault:"postgres://agentregistry:agentregistry@localhost:5432/agent-registry?sslmode=disable"` + DatabaseURLFile string `env:"DATABASE_URL_FILE" envDefault:""` + DatabaseVectorEnabled bool `env:"DATABASE_VECTOR_ENABLED" envDefault:"false"` SeedFrom string `env:"SEED_FROM" envDefault:""` EnrichServerData bool `env:"ENRICH_SERVER_DATA" envDefault:"false"` DisableBuiltinSeed bool `env:"DISABLE_BUILTIN_SEED" envDefault:"true"` @@ -82,6 +86,16 @@ func NewConfig() *Config { os.Exit(1) } + // If a database URL file is specified, read it and override DatabaseURL. + if cfg.DatabaseURLFile != "" { + url, err := readDatabaseURLFile(cfg.DatabaseURLFile) + if err != nil { + slog.Error("failed to read database URL file", "path", cfg.DatabaseURLFile, "error", err) + os.Exit(1) + } + cfg.DatabaseURL = url + } + // Append a random suffix to RuntimeDir when the user has not set an // explicit override via the AGENT_REGISTRY_RUNTIME_DIR env var. This // prevents concurrent runs from sharing the same directory. @@ -97,6 +111,19 @@ func NewConfig() *Config { return &cfg } +// readDatabaseURLFile reads a database connection URL from a file and returns the trimmed contents. +func readDatabaseURLFile(path string) (string, error) { + content, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("reading database URL file: %w", err) + } + url := strings.TrimSpace(string(content)) + if url == "" { + return "", fmt.Errorf("database URL file %s is empty", path) + } + return url, nil +} + // randomHex returns a hex-encoded string of n random bytes. func randomHex(n int) (string, error) { b := make([]byte, n) diff --git a/internal/registry/database/migrate.go b/internal/registry/database/migrate.go index 36a1e8bb..3cb50e21 100644 --- a/internal/registry/database/migrate.go +++ b/internal/registry/database/migrate.go @@ -9,6 +9,9 @@ import ( //go:embed migrations/*.sql var migrationFiles embed.FS +//go:embed migrations_vector/*.sql +var vectorMigrationFiles embed.FS + // DefaultMigratorConfig returns the default configuration for OSS migrations. func DefaultMigratorConfig() database.MigratorConfig { return database.MigratorConfig{ @@ -17,3 +20,14 @@ func DefaultMigratorConfig() database.MigratorConfig { EnsureTable: true, } } + +// VectorMigratorConfig returns the configuration for vector/pgvector migrations. +// These are applied separately, only when vector support is enabled. +// VersionOffset 100 keeps vector migrations in a separate namespace from base migrations. +func VectorMigratorConfig() database.MigratorConfig { + return database.MigratorConfig{ + MigrationFiles: vectorMigrationFiles, + VersionOffset: 100, + EnsureTable: false, + } +} diff --git a/internal/registry/database/migrations/001_initial_schema.sql b/internal/registry/database/migrations/001_initial_schema.sql index d150e0b0..1f2081fa 100644 --- a/internal/registry/database/migrations/001_initial_schema.sql +++ b/internal/registry/database/migrations/001_initial_schema.sql @@ -11,9 +11,6 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Trigram support for text search CREATE EXTENSION IF NOT EXISTS "pg_trgm"; --- pgvector for semantic embeddings -CREATE EXTENSION IF NOT EXISTS vector; - -- ============================================================================= -- SERVERS TABLE -- ============================================================================= @@ -22,29 +19,21 @@ CREATE TABLE servers ( -- Primary identifiers server_name VARCHAR(255) NOT NULL, version VARCHAR(255) NOT NULL, - + -- Status and timestamps status VARCHAR(50) NOT NULL DEFAULT 'active', published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), is_latest BOOLEAN NOT NULL DEFAULT true, - + -- Complete ServerJSON payload as JSONB value JSONB NOT NULL, - + -- Publishing state published BOOLEAN NOT NULL DEFAULT false, published_date TIMESTAMP WITH TIME ZONE, unpublished_date TIMESTAMP WITH TIME ZONE, - - -- Semantic embedding columns for vector search - semantic_embedding vector(1536), - semantic_embedding_provider TEXT, - semantic_embedding_model TEXT, - semantic_embedding_dimensions INTEGER, - semantic_embedding_checksum TEXT, - semantic_embedding_generated_at TIMESTAMPTZ, - + -- Primary key CONSTRAINT servers_pkey PRIMARY KEY (server_name, version) ); @@ -65,9 +54,6 @@ CREATE UNIQUE INDEX idx_unique_latest_per_server ON servers (server_name) WHERE CREATE INDEX idx_servers_json_remotes ON servers USING GIN((value->'remotes')); CREATE INDEX idx_servers_json_packages ON servers USING GIN((value->'packages')); --- HNSW index for semantic embedding similarity search -CREATE INDEX idx_servers_semantic_embedding_hnsw ON servers USING hnsw (semantic_embedding vector_cosine_ops); - -- Check constraints for servers ALTER TABLE servers ADD CONSTRAINT check_status_valid CHECK (status IN ('active', 'deprecated', 'deleted')); @@ -166,29 +152,21 @@ CREATE TABLE agents ( -- Primary identifiers agent_name VARCHAR(255) NOT NULL, version VARCHAR(255) NOT NULL, - + -- Status and timestamps status VARCHAR(50) NOT NULL DEFAULT 'active', published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), is_latest BOOLEAN NOT NULL DEFAULT true, - + -- Complete AgentJSON payload as JSONB value JSONB NOT NULL, - + -- Publishing state published BOOLEAN NOT NULL DEFAULT false, published_date TIMESTAMP WITH TIME ZONE, unpublished_date TIMESTAMP WITH TIME ZONE, - - -- Semantic embedding columns for vector search - semantic_embedding vector(1536), - semantic_embedding_provider TEXT, - semantic_embedding_model TEXT, - semantic_embedding_dimensions INTEGER, - semantic_embedding_checksum TEXT, - semantic_embedding_generated_at TIMESTAMPTZ, - + -- Primary key CONSTRAINT agents_pkey PRIMARY KEY (agent_name, version) ); @@ -205,9 +183,6 @@ CREATE INDEX idx_agents_published ON agents (published); -- Ensure only one version per agent is marked as latest CREATE UNIQUE INDEX idx_unique_latest_per_agent ON agents (agent_name) WHERE is_latest = true; --- HNSW index for semantic embedding similarity search -CREATE INDEX idx_agents_semantic_embedding_hnsw ON agents USING hnsw (semantic_embedding vector_cosine_ops); - -- Trigger function to auto-update updated_at CREATE OR REPLACE FUNCTION update_agents_updated_at() RETURNS TRIGGER AS $$ diff --git a/internal/registry/database/migrations_vector/001_vector_support.sql b/internal/registry/database/migrations_vector/001_vector_support.sql new file mode 100644 index 00000000..9bece218 --- /dev/null +++ b/internal/registry/database/migrations_vector/001_vector_support.sql @@ -0,0 +1,24 @@ +-- Vector extension and schema additions for semantic search. +-- Applied only when database.postgres.vectorEnabled=true (AGENT_REGISTRY_DATABASE_VECTOR_ENABLED=true). + +CREATE EXTENSION IF NOT EXISTS vector; + +ALTER TABLE servers + ADD COLUMN IF NOT EXISTS semantic_embedding vector(1536), + ADD COLUMN IF NOT EXISTS semantic_embedding_provider TEXT, + ADD COLUMN IF NOT EXISTS semantic_embedding_model TEXT, + ADD COLUMN IF NOT EXISTS semantic_embedding_dimensions INTEGER, + ADD COLUMN IF NOT EXISTS semantic_embedding_checksum TEXT, + ADD COLUMN IF NOT EXISTS semantic_embedding_generated_at TIMESTAMPTZ; + +CREATE INDEX IF NOT EXISTS idx_servers_semantic_embedding_hnsw ON servers USING hnsw (semantic_embedding vector_cosine_ops); + +ALTER TABLE agents + ADD COLUMN IF NOT EXISTS semantic_embedding vector(1536), + ADD COLUMN IF NOT EXISTS semantic_embedding_provider TEXT, + ADD COLUMN IF NOT EXISTS semantic_embedding_model TEXT, + ADD COLUMN IF NOT EXISTS semantic_embedding_dimensions INTEGER, + ADD COLUMN IF NOT EXISTS semantic_embedding_checksum TEXT, + ADD COLUMN IF NOT EXISTS semantic_embedding_generated_at TIMESTAMPTZ; + +CREATE INDEX IF NOT EXISTS idx_agents_semantic_embedding_hnsw ON agents USING hnsw (semantic_embedding vector_cosine_ops); diff --git a/internal/registry/database/postgres.go b/internal/registry/database/postgres.go index aa8963b1..56d6e4b7 100644 --- a/internal/registry/database/postgres.go +++ b/internal/registry/database/postgres.go @@ -45,7 +45,7 @@ func (db *PostgreSQL) getExecutor(tx pgx.Tx) Executor { } // NewPostgreSQL creates a new instance of the PostgreSQL database -func NewPostgreSQL(ctx context.Context, connectionURI string, authz auth.Authorizer) (*PostgreSQL, error) { +func NewPostgreSQL(ctx context.Context, connectionURI string, authz auth.Authorizer, vectorEnabled bool) (*PostgreSQL, error) { // Parse connection config for pool settings config, err := pgxpool.ParseConfig(connectionURI) if err != nil { @@ -81,6 +81,13 @@ func NewPostgreSQL(ctx context.Context, connectionURI string, authz auth.Authori return nil, fmt.Errorf("failed to run database migrations: %w", err) } + if vectorEnabled { + vectorMigrator := database.NewMigrator(conn.Conn(), VectorMigratorConfig()) + if err := vectorMigrator.Migrate(ctx); err != nil { + return nil, fmt.Errorf("failed to run vector database migrations: %w", err) + } + } + return &PostgreSQL{ pool: pool, authz: authz, diff --git a/internal/registry/database/testutil.go b/internal/registry/database/testutil.go index 0a253a8f..500326ae 100644 --- a/internal/registry/database/testutil.go +++ b/internal/registry/database/testutil.go @@ -106,7 +106,7 @@ func ensureTemplateDB(ctx context.Context, adminConn *pgx.Conn) error { // Connect to template and run migrations (always) to keep it up-to-date // Create a permissive authz for tests testAuthz := createTestAuthz() - templateDB, err := NewPostgreSQL(ctx, templateURI, testAuthz) + templateDB, err := NewPostgreSQL(ctx, templateURI, testAuthz, false) if err != nil { return fmt.Errorf("failed to connect to template database: %w", err) } @@ -180,7 +180,7 @@ func NewTestDB(t *testing.T) database.Database { // Create a permissive authz for tests testAuthz := createTestAuthz() - db, err := NewPostgreSQL(ctx, testURI, testAuthz) + db, err := NewPostgreSQL(ctx, testURI, testAuthz, false) require.NoError(t, err, "Failed to connect to test database") // Register cleanup to close connection diff --git a/internal/registry/registry_app.go b/internal/registry/registry_app.go index 1bee94e2..08ef74f2 100644 --- a/internal/registry/registry_app.go +++ b/internal/registry/registry_app.go @@ -88,7 +88,7 @@ func App(_ context.Context, opts ...types.AppOptions) error { return fmt.Errorf("failed to create database via factory: %w", err) } } else { - baseDB, err := internaldb.NewPostgreSQL(ctx, cfg.DatabaseURL, authz) + baseDB, err := internaldb.NewPostgreSQL(ctx, cfg.DatabaseURL, authz, cfg.DatabaseVectorEnabled) if err != nil { return fmt.Errorf("failed to connect to PostgreSQL: %w", err) } From 85b3ecce43f8a87bcd64d03fb04776c9a21d9287 Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Thu, 19 Mar 2026 18:26:12 -0700 Subject: [PATCH 11/15] fix integration tests that depend on vector DB --- .../registry/api/handlers/v0/servers_test.go | 13 +--------- internal/registry/database/testutil.go | 24 +++++++++++++++---- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/internal/registry/api/handlers/v0/servers_test.go b/internal/registry/api/handlers/v0/servers_test.go index 4b35f11f..b070128b 100644 --- a/internal/registry/api/handlers/v0/servers_test.go +++ b/internal/registry/api/handlers/v0/servers_test.go @@ -22,7 +22,6 @@ import ( "github.com/agentregistry-dev/agentregistry/pkg/registry/database" "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/adapters/humago" - "github.com/jackc/pgx/v5" apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" "github.com/modelcontextprotocol/registry/pkg/model" "github.com/stretchr/testify/assert" @@ -136,8 +135,7 @@ func TestListServersEndpoint(t *testing.T) { func TestListServersSemanticSearch(t *testing.T) { ctx := context.Background() - db := internaldb.NewTestDB(t) - ensureVectorExtension(t, db) + db := internaldb.NewTestDB(t, internaldb.WithVector()) cfg := config.NewConfig() cfg.Embeddings.Enabled = true @@ -436,15 +434,6 @@ func TestGetServerVersionEndpoint(t *testing.T) { } } -func ensureVectorExtension(t *testing.T, db database.Database) { - t.Helper() - err := db.InTransaction(context.Background(), func(ctx context.Context, tx pgx.Tx) error { - _, execErr := tx.Exec(ctx, "CREATE EXTENSION IF NOT EXISTS vector") - return execErr - }) - require.NoError(t, err, "failed to ensure pgvector extension for tests") -} - type stubEmbeddingProvider struct { mu sync.Mutex vectors map[string][]float32 diff --git a/internal/registry/database/testutil.go b/internal/registry/database/testutil.go index 500326ae..23eb0cf4 100644 --- a/internal/registry/database/testutil.go +++ b/internal/registry/database/testutil.go @@ -128,12 +128,30 @@ func ensureVectorExtension(ctx context.Context, uri string) error { return nil } +type testDBOption struct { + vectorEnabled bool +} + +// WithVector enables vector migrations (adds semantic_embedding columns) on the test database. +// Use for tests that exercise pgvector/embeddings functionality. +func WithVector() testDBOption { + return testDBOption{vectorEnabled: true} +} + // NewTestDB creates an isolated PostgreSQL database for each test by copying a template. // The template database has migrations pre-applied, so each test is fast. // Requires PostgreSQL to be running on localhost:5432 (e.g., via docker-compose). -func NewTestDB(t *testing.T) database.Database { +// Pass WithVector() to also apply vector migrations. +func NewTestDB(t *testing.T, opts ...testDBOption) database.Database { t.Helper() + vectorEnabled := false + for _, o := range opts { + if o.vectorEnabled { + vectorEnabled = true + } + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -175,15 +193,13 @@ func NewTestDB(t *testing.T) database.Database { _, _ = adminConn.Exec(cleanupCtx, fmt.Sprintf("DROP DATABASE IF EXISTS %s", dbName)) }) - // Connect to test database (no migrations needed - copied from template) testURI := fmt.Sprintf("postgres://agentregistry:agentregistry@localhost:5432/%s?sslmode=disable", dbName) // Create a permissive authz for tests testAuthz := createTestAuthz() - db, err := NewPostgreSQL(ctx, testURI, testAuthz, false) + db, err := NewPostgreSQL(ctx, testURI, testAuthz, vectorEnabled) require.NoError(t, err, "Failed to connect to test database") - // Register cleanup to close connection t.Cleanup(func() { if err := db.Close(); err != nil { t.Logf("Warning: failed to close test database connection: %v", err) From d3e5604425847fdcc8c71c02925f56ae9759f555 Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Thu, 19 Mar 2026 18:54:15 -0700 Subject: [PATCH 12/15] migration dir fix --- Makefile | 7 ++++++- internal/registry/database/migrate.go | 1 + pkg/registry/database/migrate.go | 10 +++++++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 6611faf1..1a73b91b 100644 --- a/Makefile +++ b/Makefile @@ -379,7 +379,12 @@ dump-kind-state: ## Dump Kind cluster state for debugging (pods, events, kagent @kubectl get pods -A --context $(KIND_CLUSTER_CONTEXT) 2>/dev/null || true @echo "" @echo "=== Pod describe ===" - @kubectl describe pods --context $(KIND_CLUSTER_CONTEXT) 2>/dev/null || true + @kubectl describe pods -A --context $(KIND_CLUSTER_CONTEXT) 2>/dev/null || true + @echo "" + @echo "=== AgentRegistry logs ===" + @kubectl logs deployment/agentregistry -n $(KIND_NAMESPACE) --context $(KIND_CLUSTER_CONTEXT) --tail=100 2>/dev/null || true + @echo "=== AgentRegistry previous logs ===" + @kubectl logs deployment/agentregistry -n $(KIND_NAMESPACE) --context $(KIND_CLUSTER_CONTEXT) --tail=100 --previous 2>/dev/null || true @echo "" @echo "=== Events ===" @kubectl get events -A --sort-by='.lastTimestamp' --context $(KIND_CLUSTER_CONTEXT) 2>/dev/null | tail -50 || true diff --git a/internal/registry/database/migrate.go b/internal/registry/database/migrate.go index 3cb50e21..0027570b 100644 --- a/internal/registry/database/migrate.go +++ b/internal/registry/database/migrate.go @@ -27,6 +27,7 @@ func DefaultMigratorConfig() database.MigratorConfig { func VectorMigratorConfig() database.MigratorConfig { return database.MigratorConfig{ MigrationFiles: vectorMigrationFiles, + MigrationDir: "migrations_vector", VersionOffset: 100, EnsureTable: false, } diff --git a/pkg/registry/database/migrate.go b/pkg/registry/database/migrate.go index daafd50b..6b30e490 100644 --- a/pkg/registry/database/migrate.go +++ b/pkg/registry/database/migrate.go @@ -27,9 +27,12 @@ type Migration struct { // their own migrations while sharing the same schema_migrations table. type MigratorConfig struct { // MigrationFiles is the embedded filesystem containing migration files. - // The filesystem should contain a "migrations" directory with .sql files + // The filesystem should contain a directory (named by MigrationDir) with .sql files // named using the pattern "NNN_description.sql" (e.g., "001_initial_schema.sql"). MigrationFiles embed.FS + // MigrationDir is the directory within MigrationFiles to read migrations from. + // Defaults to "migrations" when empty. + MigrationDir string // VersionOffset is added to all migration versions to avoid conflicts. // Set to 0 for OSS migrations, 500+ for extensions. // This allows multiple migration sources to avoid collisions. @@ -97,7 +100,8 @@ func (m *Migrator) getAppliedMigrations(ctx context.Context) (map[int]struct{}, // loadMigrations loads all migration files from the embedded filesystem func (m *Migrator) loadMigrations() ([]Migration, error) { - entries, err := m.config.MigrationFiles.ReadDir("migrations") + dir := cmp.Or(m.config.MigrationDir, "migrations") + entries, err := m.config.MigrationFiles.ReadDir(dir) if err != nil { return nil, fmt.Errorf("failed to read migrations directory: %w", err) } @@ -126,7 +130,7 @@ func (m *Migrator) loadMigrations() ([]Migration, error) { offsetVersion := version + m.config.VersionOffset // Read the migration SQL - content, err := m.config.MigrationFiles.ReadFile(path.Join("migrations", name)) + content, err := m.config.MigrationFiles.ReadFile(path.Join(dir, name)) if err != nil { return nil, fmt.Errorf("failed to read migration file %s: %w", name, err) } From c29857dec6a18603285c7dae37239202a65ab671 Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Fri, 20 Mar 2026 12:21:21 -0700 Subject: [PATCH 13/15] remove urlFile option, will introduce secure option in followup work --- charts/agentregistry/templates/NOTES.txt | 8 ++--- charts/agentregistry/templates/_helpers.tpl | 4 +-- .../agentregistry/templates/deployment.yaml | 9 ++---- .../agentregistry/tests/deployment_test.yaml | 22 -------------- .../agentregistry/tests/postgresql_test.yaml | 16 ---------- .../agentregistry/tests/validation_test.yaml | 12 ++------ charts/agentregistry/values.yaml | 2 -- internal/registry/config/config.go | 30 ++----------------- 8 files changed, 13 insertions(+), 90 deletions(-) diff --git a/charts/agentregistry/templates/NOTES.txt b/charts/agentregistry/templates/NOTES.txt index faf98ace..f9808333 100644 --- a/charts/agentregistry/templates/NOTES.txt +++ b/charts/agentregistry/templates/NOTES.txt @@ -28,7 +28,7 @@ gRPC endpoint (Agent Gateway): {{ if .Values.database.postgres.bundled.enabled -}} ################################################################################ -{{- if and (eq .Values.database.postgres.url "") (eq .Values.database.postgres.urlFile "") }} +{{- if eq .Values.database.postgres.url "" }} # WARNING: BUNDLED DATABASE IN USE # ################################################################################ The bundled PostgreSQL instance is enabled. It is intended for development and @@ -36,14 +36,14 @@ gRPC endpoint (Agent Gateway): pod is restarted or rescheduled. To use an external database, set: - database.postgres.url= or database.postgres.urlFile= + database.postgres.url= {{- else }} # NOTE: BUNDLED DATABASE DEPLOYED BUT NOT IN USE # ################################################################################ The bundled PostgreSQL pod is running, but Agent Registry is connected to an - external database (database.postgres.url or database.postgres.urlFile is set). + external database (database.postgres.url is set). - To connect Agent Registry to the bundled instance instead, unset url/urlFile: + To connect Agent Registry to the bundled instance instead, unset url: database.postgres.url="" To stop deploying the bundled pod entirely, set: database.postgres.bundled.enabled=false diff --git a/charts/agentregistry/templates/_helpers.tpl b/charts/agentregistry/templates/_helpers.tpl index 6a99670d..7d045bc0 100644 --- a/charts/agentregistry/templates/_helpers.tpl +++ b/charts/agentregistry/templates/_helpers.tpl @@ -309,8 +309,8 @@ Called from templates/validate.yaml so it fires during helm template/install. {{- $errors = append $errors "config.jwtPrivateKey must be a valid hex string (e.g. generated with: openssl rand -hex 32)." }} {{- end }} {{- if not .Values.database.postgres.bundled.enabled }} -{{- if and (not .Values.database.postgres.url) (not .Values.database.postgres.urlFile) }} -{{- $errors = append $errors "database.postgres.url or database.postgres.urlFile must be set when database.postgres.bundled.enabled=false. An external PostgreSQL instance with pgvector is required." }} +{{- if not .Values.database.postgres.url }} +{{- $errors = append $errors "database.postgres.url must be set when database.postgres.bundled.enabled=false." }} {{- end }} {{- end }} {{- range $errors }} diff --git a/charts/agentregistry/templates/deployment.yaml b/charts/agentregistry/templates/deployment.yaml index 39cd8f99..d25b91eb 100644 --- a/charts/agentregistry/templates/deployment.yaml +++ b/charts/agentregistry/templates/deployment.yaml @@ -85,7 +85,7 @@ spec: topologySpreadConstraints: {{- toYaml .Values.topologySpreadConstraints | nindent 8 }} {{- end }} - {{- if and .Values.database.postgres.bundled.enabled (eq .Values.database.postgres.url "") (eq .Values.database.postgres.urlFile "") }} + {{- if and .Values.database.postgres.bundled.enabled (eq .Values.database.postgres.url "") }} initContainers: - name: wait-for-postgres image: {{ include "agentregistry.postgresql.image" . }} @@ -134,10 +134,7 @@ spec: secretKeyRef: name: {{ .Values.config.existingSecret | default (include "agentregistry.fullname" .) }} key: AGENT_REGISTRY_JWT_PRIVATE_KEY - {{- if .Values.database.postgres.urlFile }} - - name: AGENT_REGISTRY_DATABASE_URL_FILE - value: {{ .Values.database.postgres.urlFile | quote }} - {{- else if .Values.database.postgres.url }} + {{- if .Values.database.postgres.url }} - name: AGENT_REGISTRY_DATABASE_URL value: {{ .Values.database.postgres.url | quote }} {{- else if .Values.database.postgres.bundled.enabled }} @@ -149,7 +146,7 @@ spec: - name: AGENT_REGISTRY_DATABASE_URL value: {{ printf "postgres://agentregistry:$(POSTGRES_PASSWORD)@%s:5432/agentregistry?sslmode=disable" (include "agentregistry.postgresql.fullname" .) | quote }} {{- else }} - {{ fail "No database connection configured. Set database.postgres.url, database.postgres.urlFile, or enable database.postgres.bundled." }} + {{ fail "No database connection configured. Set database.postgres.url or enable database.postgres.bundled." }} {{- end }} {{- if .Values.database.postgres.vectorEnabled }} - name: AGENT_REGISTRY_EMBEDDINGS_ENABLED diff --git a/charts/agentregistry/tests/deployment_test.yaml b/charts/agentregistry/tests/deployment_test.yaml index 34a6ded4..eeb6a6e8 100644 --- a/charts/agentregistry/tests/deployment_test.yaml +++ b/charts/agentregistry/tests/deployment_test.yaml @@ -110,18 +110,6 @@ tests: name: AGENT_REGISTRY_DATABASE_URL value: "postgres://user:pass@host:5432/db?sslmode=disable" - - it: sets AGENT_REGISTRY_DATABASE_URL_FILE when database.postgres.urlFile is set - template: deployment.yaml - set: - database.postgres.bundled.enabled: false - database.postgres.urlFile: "/run/secrets/db-url" - asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: AGENT_REGISTRY_DATABASE_URL_FILE - value: "/run/secrets/db-url" - - it: does not inject POSTGRES_PASSWORD when url is set (bundled enabled or disabled) template: deployment.yaml set: @@ -132,16 +120,6 @@ tests: content: name: POSTGRES_PASSWORD - - it: does not inject POSTGRES_PASSWORD when urlFile is set - template: deployment.yaml - set: - database.postgres.urlFile: "/run/secrets/db-url" - asserts: - - notContains: - path: spec.template.spec.containers[0].env - content: - name: POSTGRES_PASSWORD - - it: applies pod security context by default template: deployment.yaml asserts: diff --git a/charts/agentregistry/tests/postgresql_test.yaml b/charts/agentregistry/tests/postgresql_test.yaml index fc7a8568..790bf75f 100644 --- a/charts/agentregistry/tests/postgresql_test.yaml +++ b/charts/agentregistry/tests/postgresql_test.yaml @@ -193,14 +193,6 @@ tests: - hasDocuments: count: 3 - - it: still renders when bundled is enabled and urlFile is also set - template: postgresql.yaml - set: - database.postgres.urlFile: "/run/secrets/db-url" - asserts: - - hasDocuments: - count: 3 - # ── Custom storage size ─────────────────────────────────────────────────── - it: uses custom storage size @@ -253,14 +245,6 @@ tests: - hasDocuments: count: 1 - - it: creates postgresql secret even when urlFile is set (bundled still enabled) - template: postgresql-secret.yaml - set: - database.postgres.urlFile: "/run/secrets/db-url" - asserts: - - hasDocuments: - count: 1 - - it: does not create postgresql secret when bundled is disabled template: postgresql-secret.yaml set: diff --git a/charts/agentregistry/tests/validation_test.yaml b/charts/agentregistry/tests/validation_test.yaml index 16cb584d..0b1b770d 100644 --- a/charts/agentregistry/tests/validation_test.yaml +++ b/charts/agentregistry/tests/validation_test.yaml @@ -46,7 +46,7 @@ tests: # ── Bundled PostgreSQL skips external validation ────────────────────────── - - it: renders nothing with bundled enabled even when no url or urlFile + - it: renders nothing with bundled enabled even when no url set: config.jwtPrivateKey: deadbeef1234567890abcdef asserts: @@ -55,7 +55,7 @@ tests: # ── Database URL validation (bundled disabled) ──────────────────────────── - - it: fails when bundled is disabled and neither url nor urlFile is set + - it: fails when bundled is disabled and url is not set set: config.jwtPrivateKey: deadbeef1234567890abcdef database.postgres.bundled.enabled: false @@ -72,11 +72,3 @@ tests: - hasDocuments: count: 0 - - it: renders nothing when database.postgres.urlFile is set (bundled disabled) - set: - config.jwtPrivateKey: deadbeef1234567890abcdef - database.postgres.bundled.enabled: false - database.postgres.urlFile: "/run/secrets/db-url" - asserts: - - hasDocuments: - count: 0 diff --git a/charts/agentregistry/values.yaml b/charts/agentregistry/values.yaml index 2b2dd18d..b84f6999 100644 --- a/charts/agentregistry/values.yaml +++ b/charts/agentregistry/values.yaml @@ -278,8 +278,6 @@ database: postgres: # -- External PostgreSQL connection string. Used if set, regardless of bundled.enabled. url: "" - # -- Path to a file containing the database URL. Takes precedence over url. - urlFile: "" # -- Enable vector schema migrations and embeddings/semantic search (sets AGENT_REGISTRY_DATABASE_VECTOR_ENABLED and AGENT_REGISTRY_EMBEDDINGS_ENABLED). Requires a pgvector-capable PostgreSQL instance. vectorEnabled: false # -- Bundled PostgreSQL — dev/eval only. diff --git a/internal/registry/config/config.go b/internal/registry/config/config.go index a70a8516..09c10882 100644 --- a/internal/registry/config/config.go +++ b/internal/registry/config/config.go @@ -3,10 +3,8 @@ package config import ( "crypto/rand" "encoding/hex" - "fmt" "log/slog" "os" - "strings" env "github.com/caarlos0/env/v11" "github.com/joho/godotenv" @@ -17,9 +15,8 @@ import ( type Config struct { ServerAddress string `env:"SERVER_ADDRESS" envDefault:":8080"` MCPPort uint16 `env:"MCP_PORT" envDefault:"0"` - DatabaseURL string `env:"DATABASE_URL" envDefault:"postgres://agentregistry:agentregistry@localhost:5432/agent-registry?sslmode=disable"` - DatabaseURLFile string `env:"DATABASE_URL_FILE" envDefault:""` - DatabaseVectorEnabled bool `env:"DATABASE_VECTOR_ENABLED" envDefault:"false"` + DatabaseURL string `env:"DATABASE_URL" envDefault:"postgres://agentregistry:agentregistry@localhost:5432/agent-registry?sslmode=disable"` + DatabaseVectorEnabled bool `env:"DATABASE_VECTOR_ENABLED" envDefault:"false"` SeedFrom string `env:"SEED_FROM" envDefault:""` EnrichServerData bool `env:"ENRICH_SERVER_DATA" envDefault:"false"` DisableBuiltinSeed bool `env:"DISABLE_BUILTIN_SEED" envDefault:"true"` @@ -87,16 +84,6 @@ func NewConfig() *Config { os.Exit(1) } - // If a database URL file is specified, read it and override DatabaseURL. - if cfg.DatabaseURLFile != "" { - url, err := readDatabaseURLFile(cfg.DatabaseURLFile) - if err != nil { - slog.Error("failed to read database URL file", "path", cfg.DatabaseURLFile, "error", err) - os.Exit(1) - } - cfg.DatabaseURL = url - } - // Append a random suffix to RuntimeDir when the user has not set an // explicit override via the AGENT_REGISTRY_RUNTIME_DIR env var. This // prevents concurrent runs from sharing the same directory. @@ -112,19 +99,6 @@ func NewConfig() *Config { return &cfg } -// readDatabaseURLFile reads a database connection URL from a file and returns the trimmed contents. -func readDatabaseURLFile(path string) (string, error) { - content, err := os.ReadFile(path) - if err != nil { - return "", fmt.Errorf("reading database URL file: %w", err) - } - url := strings.TrimSpace(string(content)) - if url == "" { - return "", fmt.Errorf("database URL file %s is empty", path) - } - return url, nil -} - // randomHex returns a hex-encoded string of n random bytes. func randomHex(n int) (string, error) { b := make([]byte, n) From 4dd5872cdbea77c51c7b2e7a3693cac35795c1e9 Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Fri, 20 Mar 2026 12:53:25 -0700 Subject: [PATCH 14/15] address feedback --- .env.example | 2 +- README.md | 4 ++-- charts/agentregistry/values.yaml | 5 +++-- e2e/deploy_test.go | 2 +- internal/daemon/docker-compose.yml | 6 +++--- internal/registry/config/config.go | 4 ++-- internal/registry/database/testutil.go | 15 +++++++++------ scripts/kind/README.md | 12 +++++++----- 8 files changed, 28 insertions(+), 22 deletions(-) diff --git a/.env.example b/.env.example index 3f38468a..0c3bebaf 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ AGENT_REGISTRY_SERVER_ADDRESS=:8080 # Database Configuration # PostgreSQL connection string -AGENT_REGISTRY_DATABASE_URL=postgres://localhost:5432/agent-registry?sslmode=disable +AGENT_REGISTRY_DATABASE_URL=postgres://localhost:5432/agentregistry?sslmode=disable # Seed Configuration # Path to seed data file (optional) diff --git a/README.md b/README.md index 704739ca..bc82237f 100644 --- a/README.md +++ b/README.md @@ -130,8 +130,6 @@ helm install agentregistry oci://ghcr.io/agentregistry-dev/agentregistry/charts/ --set config.jwtPrivateKey=$(openssl rand -hex 32) ``` -> **Semantic search** requires a vector-enabled PostgreSQL instance. Add `--set database.postgres.vectorEnabled=true` when your database has vector support. The bundled database does not include vector support — semantic search will not be available when using it. - Then port-forward to access the UI: ```bash @@ -140,6 +138,8 @@ kubectl port-forward -n agentregistry svc/agentregistry 12121:12121 **Get started:** [Helm chart details](charts/agentregistry/README.md.gotmpl), [Local Kind cluster](scripts/kind/README.md) +> **Semantic search** requires a PostgreSQL instance with the pgvector extension. It is disabled by default. To enable it, ensure your database has pgvector support and set `AGENT_REGISTRY_DATABASE_VECTOR_ENABLED=true` (docker-compose / `.env`) or `--set database.postgres.vectorEnabled=true` (Helm). + --- ## 🎬 See It In Action diff --git a/charts/agentregistry/values.yaml b/charts/agentregistry/values.yaml index b84f6999..fb19d8d6 100644 --- a/charts/agentregistry/values.yaml +++ b/charts/agentregistry/values.yaml @@ -276,9 +276,10 @@ rbac: database: postgres: - # -- External PostgreSQL connection string. Used if set, regardless of bundled.enabled. + # -- External PostgreSQL connection string. + # -- When set, Agent Registry connects here instead of the bundled instance. The bundled pod (if enabled) still deploys but is unused; the init container is skipped. url: "" - # -- Enable vector schema migrations and embeddings/semantic search (sets AGENT_REGISTRY_DATABASE_VECTOR_ENABLED and AGENT_REGISTRY_EMBEDDINGS_ENABLED). Requires a pgvector-capable PostgreSQL instance. + # -- Enable features that require a pgvector-capable PostgreSQL instance. e.g. vector schema migrations, embeddings and semantic search. vectorEnabled: false # -- Bundled PostgreSQL — dev/eval only. bundled: diff --git a/e2e/deploy_test.go b/e2e/deploy_test.go index bc7c4fec..188e29d0 100644 --- a/e2e/deploy_test.go +++ b/e2e/deploy_test.go @@ -632,7 +632,7 @@ func deleteAgentDeploymentsDirectlyInDB(t *testing.T, agentName, providerID stri defer cancel() cmd := exec.CommandContext(ctx, "docker", "exec", "agent-registry-postgres", - "psql", "-U", "agentregistry", "-d", "agent-registry", "-v", "ON_ERROR_STOP=1", "-c", sql) + "psql", "-U", "agentregistry", "-d", "agentregistry", "-v", "ON_ERROR_STOP=1", "-c", sql) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("failed to delete deployments directly in db: %v\n%s", err, string(out)) diff --git a/internal/daemon/docker-compose.yml b/internal/daemon/docker-compose.yml index 1a7e4371..107928b7 100644 --- a/internal/daemon/docker-compose.yml +++ b/internal/daemon/docker-compose.yml @@ -3,7 +3,7 @@ services: image: pgvector/pgvector:pg16 container_name: agent-registry-postgres environment: - POSTGRES_DB: agent-registry + POSTGRES_DB: agentregistry POSTGRES_USER: agentregistry POSTGRES_PASSWORD: agentregistry ports: @@ -11,7 +11,7 @@ services: volumes: - postgres_data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U agentregistry -d agent-registry"] + test: ["CMD-SHELL", "pg_isready -U agentregistry -d agentregistry"] interval: 5s timeout: 5s retries: 5 @@ -38,7 +38,7 @@ services: fi exec /app/bin/arctl-server environment: - AGENT_REGISTRY_DATABASE_URL: "postgres://agentregistry:agentregistry@postgres:5432/agent-registry?sslmode=disable" + AGENT_REGISTRY_DATABASE_URL: "postgres://agentregistry:agentregistry@postgres:5432/agentregistry?sslmode=disable" AGENT_REGISTRY_SERVER_ADDRESS: ":8080" AGENT_REGISTRY_AGENT_GATEWAY_PORT: "21212" AGENT_REGISTRY_JWT_PRIVATE_KEY: "0000000000000000000000000000000000000000000000000000000000000000" diff --git a/internal/registry/config/config.go b/internal/registry/config/config.go index 09c10882..390a4904 100644 --- a/internal/registry/config/config.go +++ b/internal/registry/config/config.go @@ -15,8 +15,8 @@ import ( type Config struct { ServerAddress string `env:"SERVER_ADDRESS" envDefault:":8080"` MCPPort uint16 `env:"MCP_PORT" envDefault:"0"` - DatabaseURL string `env:"DATABASE_URL" envDefault:"postgres://agentregistry:agentregistry@localhost:5432/agent-registry?sslmode=disable"` - DatabaseVectorEnabled bool `env:"DATABASE_VECTOR_ENABLED" envDefault:"false"` + DatabaseURL string `env:"DATABASE_URL" envDefault:"postgres://agentregistry:agentregistry@localhost:5432/agentregistry?sslmode=disable"` + DatabaseVectorEnabled bool `env:"DATABASE_VECTOR_ENABLED" envDefault:"false"` SeedFrom string `env:"SEED_FROM" envDefault:""` EnrichServerData bool `env:"ENRICH_SERVER_DATA" envDefault:"false"` DisableBuiltinSeed bool `env:"DISABLE_BUILTIN_SEED" envDefault:"true"` diff --git a/internal/registry/database/testutil.go b/internal/registry/database/testutil.go index 23eb0cf4..3f727991 100644 --- a/internal/registry/database/testutil.go +++ b/internal/registry/database/testutil.go @@ -128,14 +128,18 @@ func ensureVectorExtension(ctx context.Context, uri string) error { return nil } -type testDBOption struct { +type testDBOption func(*testDBConfig) + +type testDBConfig struct { vectorEnabled bool } // WithVector enables vector migrations (adds semantic_embedding columns) on the test database. // Use for tests that exercise pgvector/embeddings functionality. func WithVector() testDBOption { - return testDBOption{vectorEnabled: true} + return func(cfg *testDBConfig) { + cfg.vectorEnabled = true + } } // NewTestDB creates an isolated PostgreSQL database for each test by copying a template. @@ -145,12 +149,11 @@ func WithVector() testDBOption { func NewTestDB(t *testing.T, opts ...testDBOption) database.Database { t.Helper() - vectorEnabled := false + var cfg testDBConfig for _, o := range opts { - if o.vectorEnabled { - vectorEnabled = true - } + o(&cfg) } + vectorEnabled := cfg.vectorEnabled ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() diff --git a/scripts/kind/README.md b/scripts/kind/README.md index d9161cef..c36d14ed 100644 --- a/scripts/kind/README.md +++ b/scripts/kind/README.md @@ -20,25 +20,27 @@ This single command sets up the full local environment. ## What It Does -`make setup-kind-cluster` runs two sub-targets in order: +`make setup-kind-cluster` runs the following sub-targets in order: 1. **`make create-kind-cluster`** — creates a Kind cluster named `agentregistry` with a local container registry on `localhost:5001` and MetalLB for LoadBalancer support -2. **`make install-agentregistry`** — builds server images, pushes them to the local registry, and Helm installs AgentRegistry with a bundled PostgreSQL/pgvector instance +2. **`make install-agentregistry`** — builds server images, pushes them to the local registry, and Helm installs AgentRegistry with a bundled database instance You can also run any sub-target individually, e.g. `make install-agentregistry` to redeploy after a code change. ## Database Details -PostgreSQL/pgvector is bundled in the Helm chart and deployed automatically. The default configuration is: +PostgreSQL is bundled in the Helm chart and deployed automatically. The default configuration is: | Setting | Value | |----------|----------------------------------| | Host | `agentregistry-postgresql.agentregistry.svc.cluster.local` (in-cluster) | | Port | `5432` | -| Database | `agent-registry` | +| Database | `agentregistry` | | Username | `agentregistry` | | Password | `agentregistry` | +Local setup uses `pgvector` for development of vector dependent capabilities. + ### Connecting Directly Port-forward to access PostgreSQL from your local machine: @@ -50,7 +52,7 @@ kubectl --context kind-agentregistry port-forward -n agentregistry svc/agentregi Then connect with psql: ```bash -psql -h localhost -U agentregistry -d agent-registry +psql -h localhost -U agentregistry -d agentregistry ``` ### pgvector Extension From 2ae0e6cad4aec13fd871bcd5620090e6e692a569 Mon Sep 17 00:00:00 2001 From: Nik Matthiopoulos Date: Mon, 23 Mar 2026 14:33:30 -0700 Subject: [PATCH 15/15] fixes --- charts/agentregistry/templates/_helpers.tpl | 13 ++++++++++++- .../agentregistry/templates/postgresql-secret.yaml | 3 +-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/charts/agentregistry/templates/_helpers.tpl b/charts/agentregistry/templates/_helpers.tpl index 7d045bc0..16775d84 100644 --- a/charts/agentregistry/templates/_helpers.tpl +++ b/charts/agentregistry/templates/_helpers.tpl @@ -53,6 +53,7 @@ Selector labels — stable subset used in matchLabels. {{- define "agentregistry.selectorLabels" -}} app.kubernetes.io/name: {{ include "agentregistry.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/component: server {{- end }} {{/* @@ -270,8 +271,18 @@ Full name for the bundled PostgreSQL resources. Standard labels for bundled PostgreSQL resources. */}} {{- define "agentregistry.postgresql.labels" -}} -{{ include "agentregistry.labels" . }} +helm.sh/chart: {{ include "agentregistry.chart" . }} +app.kubernetes.io/name: {{ include "agentregistry.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/component: database +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: {{ include "agentregistry.name" . }} +{{- if .Values.commonLabels }} +{{ toYaml .Values.commonLabels }} +{{- end }} {{- end }} {{/* diff --git a/charts/agentregistry/templates/postgresql-secret.yaml b/charts/agentregistry/templates/postgresql-secret.yaml index 8e36abe9..d114ebeb 100644 --- a/charts/agentregistry/templates/postgresql-secret.yaml +++ b/charts/agentregistry/templates/postgresql-secret.yaml @@ -4,8 +4,7 @@ kind: Secret metadata: name: {{ include "agentregistry.fullname" . }}-postgresql labels: - {{- include "agentregistry.labels" . | nindent 4 }} - app.kubernetes.io/component: database + {{- include "agentregistry.postgresql.labels" . | nindent 4 }} {{- $annotations := include "agentregistry.annotations" (dict "annotations" dict "context" $) }} {{- if $annotations }} annotations: