initial commit
This commit is contained in:
commit
31e2a32e01
|
@ -0,0 +1,4 @@
|
||||||
|
target
|
||||||
|
Tiltfile
|
||||||
|
manifests
|
||||||
|
orig-compose
|
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
Tiltfile
|
||||||
|
manifests/
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "neon-operator"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.37.0", features = ["full"] }
|
||||||
|
kube = { version = "0.91.0", features = ["runtime", "derive"] }
|
||||||
|
k8s-openapi = { version = "0.22.0", features = ["latest"] }
|
||||||
|
clap = { version = "4.5.4", features = ["derive", "env"] }
|
||||||
|
futures = "0.3.30"
|
||||||
|
futures-executor = { version = "0.3.30" }
|
||||||
|
schemars = "0.8.21"
|
||||||
|
serde = { version = "1.0.202", features = ["derive"] }
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tracing-subscriber = "0.3.18"
|
||||||
|
serde_json = "1.0.117"
|
||||||
|
anyhow = "1.0.86"
|
||||||
|
thiserror = "1.0.61"
|
|
@ -0,0 +1,7 @@
|
||||||
|
FROM rust:1.78
|
||||||
|
WORKDIR /build
|
||||||
|
COPY . .
|
||||||
|
RUN --mount=type=cache,target=/usr/local/cargo/registry --mount=type=cache,target=/build/target cargo build
|
||||||
|
WORKDIR /app
|
||||||
|
RUN --mount=type=cache,target=/build/target cp /build/target/debug/neon-operator /app/
|
||||||
|
CMD ["/app/neon-operator"]
|
|
@ -0,0 +1,69 @@
|
||||||
|
apiVersion: v1
|
||||||
|
items:
|
||||||
|
- apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: "2024-05-24T17:05:01Z"
|
||||||
|
generation: 1
|
||||||
|
name: neondatabases.melenion.com
|
||||||
|
resourceVersion: "32345"
|
||||||
|
uid: c72c3bea-c002-4a74-a91a-f6d1a36f683d
|
||||||
|
spec:
|
||||||
|
conversion:
|
||||||
|
strategy: None
|
||||||
|
group: melenion.com
|
||||||
|
names:
|
||||||
|
kind: NeonDatabase
|
||||||
|
listKind: NeonDatabaseList
|
||||||
|
plural: neondatabases
|
||||||
|
shortNames:
|
||||||
|
- nd
|
||||||
|
singular: neondatabase
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- name: v1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: Auto-generated derived type for NeondatabaseSpec via `CustomResource`
|
||||||
|
properties:
|
||||||
|
spec:
|
||||||
|
properties:
|
||||||
|
neon_image_ref:
|
||||||
|
type: string
|
||||||
|
postgres_version:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- neon_image_ref
|
||||||
|
- postgres_version
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- spec
|
||||||
|
title: NeonDatabase
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources: {}
|
||||||
|
status:
|
||||||
|
acceptedNames:
|
||||||
|
kind: NeonDatabase
|
||||||
|
listKind: NeonDatabaseList
|
||||||
|
plural: neondatabases
|
||||||
|
shortNames:
|
||||||
|
- nd
|
||||||
|
singular: neondatabase
|
||||||
|
conditions:
|
||||||
|
- lastTransitionTime: "2024-05-24T17:05:01Z"
|
||||||
|
message: no conflicts found
|
||||||
|
reason: NoConflicts
|
||||||
|
status: "True"
|
||||||
|
type: NamesAccepted
|
||||||
|
- lastTransitionTime: "2024-05-24T17:05:01Z"
|
||||||
|
message: the initial names have been accepted
|
||||||
|
reason: InitialNamesAccepted
|
||||||
|
status: "True"
|
||||||
|
type: Established
|
||||||
|
storedVersions:
|
||||||
|
- v1
|
||||||
|
kind: List
|
||||||
|
metadata:
|
||||||
|
resourceVersion: ""
|
|
@ -0,0 +1,16 @@
|
||||||
|
ARG REPOSITORY=neondatabase
|
||||||
|
ARG COMPUTE_IMAGE=compute-node-v14
|
||||||
|
ARG TAG=latest
|
||||||
|
|
||||||
|
FROM $REPOSITORY/${COMPUTE_IMAGE}:$TAG
|
||||||
|
|
||||||
|
USER root
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y curl \
|
||||||
|
jq \
|
||||||
|
netcat
|
||||||
|
|
||||||
|
ADD ./var/db/postgres/specs /var/db/postgres/specs
|
||||||
|
ADD ./shell/ /shell
|
||||||
|
|
||||||
|
USER postgres
|
|
@ -0,0 +1,56 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -eux
|
||||||
|
|
||||||
|
# Generate a random tenant or timeline ID
|
||||||
|
#
|
||||||
|
# Takes a variable name as argument. The result is stored in that variable.
|
||||||
|
generate_id() {
|
||||||
|
local -n resvar=$1
|
||||||
|
printf -v resvar '%08x%08x%08x%08x' $SRANDOM $SRANDOM $SRANDOM $SRANDOM
|
||||||
|
}
|
||||||
|
|
||||||
|
PG_VERSION=${PG_VERSION:-14}
|
||||||
|
|
||||||
|
SPEC_FILE_ORG=/var/db/postgres/specs/spec.json
|
||||||
|
SPEC_FILE=/tmp/spec.json
|
||||||
|
|
||||||
|
echo "Waiting pageserver become ready."
|
||||||
|
while ! nc -z pageserver 6400; do
|
||||||
|
sleep 1;
|
||||||
|
done
|
||||||
|
echo "Page server is ready."
|
||||||
|
|
||||||
|
echo "Create a tenant and timeline"
|
||||||
|
generate_id tenant_id
|
||||||
|
PARAMS=(
|
||||||
|
-sb
|
||||||
|
-X POST
|
||||||
|
-H "Content-Type: application/json"
|
||||||
|
-d "{\"new_tenant_id\": \"${tenant_id}\"}"
|
||||||
|
http://pageserver:9898/v1/tenant/
|
||||||
|
)
|
||||||
|
result=$(curl "${PARAMS[@]}")
|
||||||
|
echo $result | jq .
|
||||||
|
|
||||||
|
generate_id timeline_id
|
||||||
|
PARAMS=(
|
||||||
|
-sb
|
||||||
|
-X POST
|
||||||
|
-H "Content-Type: application/json"
|
||||||
|
-d "{\"new_timeline_id\": \"${timeline_id}\", \"pg_version\": ${PG_VERSION}}"
|
||||||
|
"http://pageserver:9898/v1/tenant/${tenant_id}/timeline/"
|
||||||
|
)
|
||||||
|
result=$(curl "${PARAMS[@]}")
|
||||||
|
echo $result | jq .
|
||||||
|
|
||||||
|
echo "Overwrite tenant id and timeline id in spec file"
|
||||||
|
sed "s/TENANT_ID/${tenant_id}/" ${SPEC_FILE_ORG} > ${SPEC_FILE}
|
||||||
|
sed -i "s/TIMELINE_ID/${timeline_id}/" ${SPEC_FILE}
|
||||||
|
|
||||||
|
cat ${SPEC_FILE}
|
||||||
|
|
||||||
|
echo "Start compute node"
|
||||||
|
/usr/local/bin/compute_ctl --pgdata /var/db/postgres/compute \
|
||||||
|
-C "postgresql://cloud_admin@localhost:55433/postgres" \
|
||||||
|
-b /usr/local/bin/postgres \
|
||||||
|
-S ${SPEC_FILE}
|
|
@ -0,0 +1,134 @@
|
||||||
|
{
|
||||||
|
"format_version": 1.0,
|
||||||
|
|
||||||
|
"timestamp": "2022-10-12T18:00:00.000Z",
|
||||||
|
"operation_uuid": "0f657b36-4b0f-4a2d-9c2e-1dcd615e7d8c",
|
||||||
|
|
||||||
|
"cluster": {
|
||||||
|
"cluster_id": "docker_compose",
|
||||||
|
"name": "docker_compose_test",
|
||||||
|
"state": "restarted",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"name": "cloud_admin",
|
||||||
|
"encrypted_password": "b093c0d3b281ba6da1eacc608620abd8",
|
||||||
|
"options": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"databases": [],
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"name": "fsync",
|
||||||
|
"value": "off",
|
||||||
|
"vartype": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "wal_level",
|
||||||
|
"value": "logical",
|
||||||
|
"vartype": "enum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "wal_log_hints",
|
||||||
|
"value": "on",
|
||||||
|
"vartype": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_connections",
|
||||||
|
"value": "on",
|
||||||
|
"vartype": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "port",
|
||||||
|
"value": "55433",
|
||||||
|
"vartype": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shared_buffers",
|
||||||
|
"value": "1MB",
|
||||||
|
"vartype": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "max_connections",
|
||||||
|
"value": "100",
|
||||||
|
"vartype": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "listen_addresses",
|
||||||
|
"value": "0.0.0.0",
|
||||||
|
"vartype": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "max_wal_senders",
|
||||||
|
"value": "10",
|
||||||
|
"vartype": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "max_replication_slots",
|
||||||
|
"value": "10",
|
||||||
|
"vartype": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "wal_sender_timeout",
|
||||||
|
"value": "5s",
|
||||||
|
"vartype": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "wal_keep_size",
|
||||||
|
"value": "0",
|
||||||
|
"vartype": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password_encryption",
|
||||||
|
"value": "md5",
|
||||||
|
"vartype": "enum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "restart_after_crash",
|
||||||
|
"value": "off",
|
||||||
|
"vartype": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "synchronous_standby_names",
|
||||||
|
"value": "walproposer",
|
||||||
|
"vartype": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shared_preload_libraries",
|
||||||
|
"value": "neon",
|
||||||
|
"vartype": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "neon.safekeepers",
|
||||||
|
"value": "safe-keeper-0.safe-keeper:5454,safe-keeper-1.safe-keeper:5454,safe-keeper-2.safe-keeper:5454",
|
||||||
|
"vartype": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "neon.timeline_id",
|
||||||
|
"value": "TIMELINE_ID",
|
||||||
|
"vartype": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "neon.tenant_id",
|
||||||
|
"value": "TENANT_ID",
|
||||||
|
"vartype": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "neon.pageserver_connstring",
|
||||||
|
"value": "host=pageserver port=6400",
|
||||||
|
"vartype": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "max_replication_write_lag",
|
||||||
|
"value": "500MB",
|
||||||
|
"vartype": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "max_replication_flush_lag",
|
||||||
|
"value": "10GB",
|
||||||
|
"vartype": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"delta_operations": []
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
minio:
|
||||||
|
restart: always
|
||||||
|
image: quay.io/minio/minio:RELEASE.2022-10-20T00-55-09Z
|
||||||
|
ports:
|
||||||
|
- 9000:9000
|
||||||
|
- 9001:9001
|
||||||
|
environment:
|
||||||
|
- MINIO_ROOT_USER=minio
|
||||||
|
- MINIO_ROOT_PASSWORD=password
|
||||||
|
command: server /data --address :9000 --console-address ":9001"
|
||||||
|
|
||||||
|
minio_create_buckets:
|
||||||
|
image: minio/mc
|
||||||
|
environment:
|
||||||
|
- MINIO_ROOT_USER=minio
|
||||||
|
- MINIO_ROOT_PASSWORD=password
|
||||||
|
entrypoint:
|
||||||
|
- "/bin/sh"
|
||||||
|
- "-c"
|
||||||
|
command:
|
||||||
|
- "until (/usr/bin/mc alias set minio http://minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD) do
|
||||||
|
echo 'Waiting to start minio...' && sleep 1;
|
||||||
|
done;
|
||||||
|
/usr/bin/mc mb minio/neon --region=eu-north-1;
|
||||||
|
exit 0;"
|
||||||
|
depends_on:
|
||||||
|
- minio
|
||||||
|
|
||||||
|
pageserver:
|
||||||
|
restart: always
|
||||||
|
image: ${REPOSITORY:-neondatabase}/neon:${TAG:-latest}
|
||||||
|
environment:
|
||||||
|
- BROKER_ENDPOINT='http://storage_broker:50051'
|
||||||
|
- AWS_ACCESS_KEY_ID=minio
|
||||||
|
- AWS_SECRET_ACCESS_KEY=password
|
||||||
|
#- RUST_BACKTRACE=1
|
||||||
|
ports:
|
||||||
|
#- 6400:6400 # pg protocol handler
|
||||||
|
- 9898:9898 # http endpoints
|
||||||
|
entrypoint:
|
||||||
|
- "/bin/sh"
|
||||||
|
- "-c"
|
||||||
|
command:
|
||||||
|
- "/usr/local/bin/pageserver -D /data/.neon/
|
||||||
|
-c \"broker_endpoint=$$BROKER_ENDPOINT\"
|
||||||
|
-c \"listen_pg_addr='0.0.0.0:6400'\"
|
||||||
|
-c \"listen_http_addr='0.0.0.0:9898'\"
|
||||||
|
-c \"remote_storage={endpoint='http://minio:9000',
|
||||||
|
bucket_name='neon',
|
||||||
|
bucket_region='eu-north-1',
|
||||||
|
prefix_in_bucket='/pageserver/'}\""
|
||||||
|
depends_on:
|
||||||
|
- storage_broker
|
||||||
|
- minio_create_buckets
|
||||||
|
|
||||||
|
safekeeper1:
|
||||||
|
restart: always
|
||||||
|
image: ${REPOSITORY:-neondatabase}/neon:${TAG:-latest}
|
||||||
|
environment:
|
||||||
|
- SAFEKEEPER_ADVERTISE_URL=safekeeper1:5454
|
||||||
|
- SAFEKEEPER_ID=1
|
||||||
|
- BROKER_ENDPOINT=http://storage_broker:50051
|
||||||
|
- AWS_ACCESS_KEY_ID=minio
|
||||||
|
- AWS_SECRET_ACCESS_KEY=password
|
||||||
|
#- RUST_BACKTRACE=1
|
||||||
|
ports:
|
||||||
|
#- 5454:5454 # pg protocol handler
|
||||||
|
- 7676:7676 # http endpoints
|
||||||
|
entrypoint:
|
||||||
|
- "/bin/sh"
|
||||||
|
- "-c"
|
||||||
|
command:
|
||||||
|
- "safekeeper --listen-pg=$$SAFEKEEPER_ADVERTISE_URL
|
||||||
|
--listen-http='0.0.0.0:7676'
|
||||||
|
--id=$$SAFEKEEPER_ID
|
||||||
|
--broker-endpoint=$$BROKER_ENDPOINT
|
||||||
|
-D /data
|
||||||
|
--remote-storage=\"{endpoint='http://minio:9000',
|
||||||
|
bucket_name='neon',
|
||||||
|
bucket_region='eu-north-1',
|
||||||
|
prefix_in_bucket='/safekeeper/'}\""
|
||||||
|
depends_on:
|
||||||
|
- storage_broker
|
||||||
|
- minio_create_buckets
|
||||||
|
|
||||||
|
safekeeper2:
|
||||||
|
restart: always
|
||||||
|
image: ${REPOSITORY:-neondatabase}/neon:${TAG:-latest}
|
||||||
|
environment:
|
||||||
|
- SAFEKEEPER_ADVERTISE_URL=safekeeper2:5454
|
||||||
|
- SAFEKEEPER_ID=2
|
||||||
|
- BROKER_ENDPOINT=http://storage_broker:50051
|
||||||
|
- AWS_ACCESS_KEY_ID=minio
|
||||||
|
- AWS_SECRET_ACCESS_KEY=password
|
||||||
|
#- RUST_BACKTRACE=1
|
||||||
|
ports:
|
||||||
|
#- 5454:5454 # pg protocol handler
|
||||||
|
- 7677:7676 # http endpoints
|
||||||
|
entrypoint:
|
||||||
|
- "/bin/sh"
|
||||||
|
- "-c"
|
||||||
|
command:
|
||||||
|
- "safekeeper --listen-pg=$$SAFEKEEPER_ADVERTISE_URL
|
||||||
|
--listen-http='0.0.0.0:7676'
|
||||||
|
--id=$$SAFEKEEPER_ID
|
||||||
|
--broker-endpoint=$$BROKER_ENDPOINT
|
||||||
|
-D /data
|
||||||
|
--remote-storage=\"{endpoint='http://minio:9000',
|
||||||
|
bucket_name='neon',
|
||||||
|
bucket_region='eu-north-1',
|
||||||
|
prefix_in_bucket='/safekeeper/'}\""
|
||||||
|
depends_on:
|
||||||
|
- storage_broker
|
||||||
|
- minio_create_buckets
|
||||||
|
|
||||||
|
safekeeper3:
|
||||||
|
restart: always
|
||||||
|
image: ${REPOSITORY:-neondatabase}/neon:${TAG:-latest}
|
||||||
|
environment:
|
||||||
|
- SAFEKEEPER_ADVERTISE_URL=safekeeper3:5454
|
||||||
|
- SAFEKEEPER_ID=3
|
||||||
|
- BROKER_ENDPOINT=http://storage_broker:50051
|
||||||
|
- AWS_ACCESS_KEY_ID=minio
|
||||||
|
- AWS_SECRET_ACCESS_KEY=password
|
||||||
|
#- RUST_BACKTRACE=1
|
||||||
|
ports:
|
||||||
|
#- 5454:5454 # pg protocol handler
|
||||||
|
- 7678:7676 # http endpoints
|
||||||
|
entrypoint:
|
||||||
|
- "/bin/sh"
|
||||||
|
- "-c"
|
||||||
|
command:
|
||||||
|
- "safekeeper --listen-pg=$$SAFEKEEPER_ADVERTISE_URL
|
||||||
|
--listen-http='0.0.0.0:7676'
|
||||||
|
--id=$$SAFEKEEPER_ID
|
||||||
|
--broker-endpoint=$$BROKER_ENDPOINT
|
||||||
|
-D /data
|
||||||
|
--remote-storage=\"{endpoint='http://minio:9000',
|
||||||
|
bucket_name='neon',
|
||||||
|
bucket_region='eu-north-1',
|
||||||
|
prefix_in_bucket='/safekeeper/'}\""
|
||||||
|
depends_on:
|
||||||
|
- storage_broker
|
||||||
|
- minio_create_buckets
|
||||||
|
|
||||||
|
storage_broker:
|
||||||
|
restart: always
|
||||||
|
image: ${REPOSITORY:-neondatabase}/neon:${TAG:-latest}
|
||||||
|
ports:
|
||||||
|
- 50051:50051
|
||||||
|
command:
|
||||||
|
- "storage_broker"
|
||||||
|
- "--listen-addr=0.0.0.0:50051"
|
||||||
|
|
||||||
|
compute:
|
||||||
|
restart: always
|
||||||
|
build:
|
||||||
|
context: ./compute_wrapper/
|
||||||
|
args:
|
||||||
|
- REPOSITORY=${REPOSITORY:-neondatabase}
|
||||||
|
- COMPUTE_IMAGE=compute-node-v${PG_VERSION:-14}
|
||||||
|
- TAG=${TAG:-latest}
|
||||||
|
- http_proxy=$http_proxy
|
||||||
|
- https_proxy=$https_proxy
|
||||||
|
environment:
|
||||||
|
- PG_VERSION=${PG_VERSION:-14}
|
||||||
|
#- RUST_BACKTRACE=1
|
||||||
|
# Mount the test files directly, for faster editing cycle.
|
||||||
|
volumes:
|
||||||
|
- ./compute_wrapper/var/db/postgres/specs/:/var/db/postgres/specs/
|
||||||
|
- ./compute_wrapper/shell/:/shell/
|
||||||
|
ports:
|
||||||
|
- 55433:55433 # pg protocol handler
|
||||||
|
- 3080:3080 # http endpoints
|
||||||
|
entrypoint:
|
||||||
|
- "/shell/compute.sh"
|
||||||
|
depends_on:
|
||||||
|
- safekeeper1
|
||||||
|
- safekeeper2
|
||||||
|
- safekeeper3
|
||||||
|
- pageserver
|
||||||
|
|
||||||
|
compute_is_ready:
|
||||||
|
image: postgres:latest
|
||||||
|
entrypoint:
|
||||||
|
- "/bin/bash"
|
||||||
|
- "-c"
|
||||||
|
command:
|
||||||
|
- "until pg_isready -h compute -p 55433 -U cloud_admin ; do
|
||||||
|
echo 'Waiting to start compute...' && sleep 1;
|
||||||
|
done"
|
||||||
|
depends_on:
|
||||||
|
- compute
|
|
@ -0,0 +1,58 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# A basic test to ensure Docker images are built correctly.
|
||||||
|
# Build a wrapper around the compute, start all services and runs a simple SQL query.
|
||||||
|
# Repeats the process for all currenly supported Postgres versions.
|
||||||
|
|
||||||
|
# Implicitly accepts `REPOSITORY` and `TAG` env vars that are passed into the compose file
|
||||||
|
# Their defaults point at DockerHub `neondatabase/neon:latest` image.`,
|
||||||
|
# to verify custom image builds (e.g pre-published ones).
|
||||||
|
|
||||||
|
set -eux -o pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
COMPOSE_FILE=$SCRIPT_DIR/docker-compose.yml
|
||||||
|
|
||||||
|
COMPUTE_CONTAINER_NAME=docker-compose-compute-1
|
||||||
|
SQL="CREATE TABLE t(key int primary key, value text); insert into t values(1,1); select * from t;"
|
||||||
|
PSQL_OPTION="-h localhost -U cloud_admin -p 55433 -c '$SQL' postgres"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
echo "show container information"
|
||||||
|
docker ps
|
||||||
|
docker compose -f $COMPOSE_FILE logs
|
||||||
|
echo "stop containers..."
|
||||||
|
docker compose -f $COMPOSE_FILE down
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "clean up containers if exists"
|
||||||
|
cleanup
|
||||||
|
|
||||||
|
for pg_version in 14 15 16; do
|
||||||
|
echo "start containers (pg_version=$pg_version)."
|
||||||
|
PG_VERSION=$pg_version docker compose -f $COMPOSE_FILE up --build -d
|
||||||
|
|
||||||
|
echo "wait until the compute is ready. timeout after 60s. "
|
||||||
|
cnt=0
|
||||||
|
while sleep 1; do
|
||||||
|
# check timeout
|
||||||
|
cnt=`expr $cnt + 1`
|
||||||
|
if [ $cnt -gt 60 ]; then
|
||||||
|
echo "timeout before the compute is ready."
|
||||||
|
cleanup
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# check if the compute is ready
|
||||||
|
set +o pipefail
|
||||||
|
result=`docker compose -f $COMPOSE_FILE logs "compute_is_ready" | grep "accepting connections" | wc -l`
|
||||||
|
set -o pipefail
|
||||||
|
if [ $result -eq 1 ]; then
|
||||||
|
echo "OK. The compute is ready to connect."
|
||||||
|
echo "execute simple queries."
|
||||||
|
docker exec $COMPUTE_CONTAINER_NAME /bin/bash -c "psql $PSQL_OPTION"
|
||||||
|
cleanup
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
|
@ -0,0 +1,20 @@
|
||||||
|
use kube::CustomResource;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||||
|
#[kube(
|
||||||
|
group = "melenion.com",
|
||||||
|
version = "v1",
|
||||||
|
kind = "NeonDatabase",
|
||||||
|
namespaced,
|
||||||
|
shortname = "nd",
|
||||||
|
// status = "NeonDatabaseStatus"
|
||||||
|
)]
|
||||||
|
pub struct NeondatabaseSpec {
|
||||||
|
pub compute_image_ref: String,
|
||||||
|
pub neon_image_ref: String,
|
||||||
|
pub postgres_version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NeonDatabaseStatus {}
|
|
@ -0,0 +1,119 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crd::NeonDatabase;
|
||||||
|
use futures::stream::StreamExt;
|
||||||
|
use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition;
|
||||||
|
use kube::api::Api;
|
||||||
|
use kube::api::Patch;
|
||||||
|
use kube::api::PatchParams;
|
||||||
|
use kube::runtime::conditions;
|
||||||
|
use kube::runtime::wait::await_condition;
|
||||||
|
use kube::runtime::watcher::Config;
|
||||||
|
use kube::Client;
|
||||||
|
use kube::CustomResourceExt;
|
||||||
|
use kube::ResourceExt;
|
||||||
|
use kube::{runtime::controller::Action, runtime::Controller};
|
||||||
|
use tracing::error;
|
||||||
|
use tracing::info;
|
||||||
|
use tracing::instrument;
|
||||||
|
pub mod crd;
|
||||||
|
pub mod minio;
|
||||||
|
pub mod neon;
|
||||||
|
|
||||||
|
struct ContextData {
|
||||||
|
pub kube_client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextData {
|
||||||
|
pub fn new(kube_client: Client) -> Self {
|
||||||
|
Self { kube_client }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All errors possible to occur during reconciliation
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Any error originating from the `kube-rs` crate
|
||||||
|
#[error("Kubernetes reported error: {source}")]
|
||||||
|
KubeError {
|
||||||
|
#[from]
|
||||||
|
source: kube::Error,
|
||||||
|
},
|
||||||
|
/// Error in user input or Echo resource definition, typically missing fields.
|
||||||
|
#[error("Invalid Neon CRD: {0}")]
|
||||||
|
UserInputError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
let kube_client = Client::try_default()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create client");
|
||||||
|
|
||||||
|
let crd_api: Api<NeonDatabase> = Api::all(kube_client.clone());
|
||||||
|
let context: Arc<ContextData> = Arc::new(ContextData::new(kube_client.clone()));
|
||||||
|
|
||||||
|
let ssapply = PatchParams::apply("crd_apply_example").force();
|
||||||
|
let crds: Api<CustomResourceDefinition> = Api::all(kube_client.clone());
|
||||||
|
info!(
|
||||||
|
"Creating crd: {}",
|
||||||
|
serde_json::to_string(&NeonDatabase::crd()).unwrap()
|
||||||
|
);
|
||||||
|
crds.patch(
|
||||||
|
"neondatabases.melenion.com",
|
||||||
|
&ssapply,
|
||||||
|
&Patch::Apply(NeonDatabase::crd()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Patching the CRD failed");
|
||||||
|
|
||||||
|
info!("Waiting for the api-server to accept the CRD");
|
||||||
|
let establish = await_condition(
|
||||||
|
crds,
|
||||||
|
"neondatabases.melenion.com",
|
||||||
|
conditions::is_crd_established(),
|
||||||
|
);
|
||||||
|
let _ = tokio::time::timeout(std::time::Duration::from_secs(10), establish)
|
||||||
|
.await
|
||||||
|
.expect("Waiting failed");
|
||||||
|
|
||||||
|
info!("CRD established, starting controller");
|
||||||
|
Controller::new(crd_api.clone(), Config::default())
|
||||||
|
.run(reconcile, on_error, context)
|
||||||
|
.for_each(|rec_result| async move {
|
||||||
|
match rec_result {
|
||||||
|
Ok(o) => {
|
||||||
|
println!("Went OK {o:?}")
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Reconciliation error:\n{:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(err, fields(database_name = ?neon.metadata.name), skip(neon, context))]
|
||||||
|
async fn reconcile(neon: Arc<NeonDatabase>, context: Arc<ContextData>) -> Result<Action, Error> {
|
||||||
|
let client = context.kube_client.clone();
|
||||||
|
|
||||||
|
let namespace: String = match neon.namespace() {
|
||||||
|
Some(ns) => ns.to_string(),
|
||||||
|
None => return Err(Error::UserInputError("Missing namespace".to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
minio::create_deployment(client.clone(), &namespace).await?;
|
||||||
|
minio::create_service(client.clone(), &namespace).await?;
|
||||||
|
neon::reconcile_storage_broker(client.clone(), &namespace).await?;
|
||||||
|
neon::reconcile_page_server(client.clone(), &namespace).await?;
|
||||||
|
neon::reconcile_safe_keepers(client.clone(), &namespace).await?;
|
||||||
|
neon::reconcile_compute(client.clone(), &namespace, &neon.spec.neon_image_ref).await?;
|
||||||
|
Ok(Action::requeue(Duration::from_secs(5)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_error(db: Arc<NeonDatabase>, error: &Error, _context: Arc<ContextData>) -> Action {
|
||||||
|
error!("Reconciliation error:\n{:?}.\n{:?}", error, db);
|
||||||
|
Action::requeue(Duration::from_secs(5))
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
use k8s_openapi::api::apps::v1::{Deployment, DeploymentSpec};
|
||||||
|
use k8s_openapi::api::core::v1::{
|
||||||
|
Container, ContainerPort, EnvVar, PodSpec, PodTemplateSpec, Service, ServicePort, ServiceSpec,
|
||||||
|
};
|
||||||
|
use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
|
||||||
|
use kube::api::{ListParams, ObjectMeta, Patch, PatchParams};
|
||||||
|
use kube::{Api, Client, Error};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use tracing::{info, instrument};
|
||||||
|
|
||||||
|
const NAME: &str = "minio";
|
||||||
|
|
||||||
|
pub async fn create_deployment(client: Client, namespace: &str) -> Result<(), Error> {
|
||||||
|
let minio_container = Container {
|
||||||
|
name: NAME.to_string(),
|
||||||
|
env: Some(vec![
|
||||||
|
EnvVar {
|
||||||
|
name: "MINIO_ROOT_USER".to_string(),
|
||||||
|
value: Some("minio".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnvVar {
|
||||||
|
name: "MINIO_ROOT_PASSWORD".to_string(),
|
||||||
|
value: Some("password".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
image: Some("quay.io/minio/minio:RELEASE.2022-10-20T00-55-09Z".to_string()),
|
||||||
|
ports: Some(vec![
|
||||||
|
ContainerPort {
|
||||||
|
container_port: 9000,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
ContainerPort {
|
||||||
|
container_port: 9001,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
args: Some(
|
||||||
|
vec![
|
||||||
|
"server",
|
||||||
|
"/data",
|
||||||
|
"--address",
|
||||||
|
":9000",
|
||||||
|
"--console-address",
|
||||||
|
":9001",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect::<Vec<String>>(),
|
||||||
|
),
|
||||||
|
liveness_probe: Some(k8s_openapi::api::core::v1::Probe {
|
||||||
|
http_get: Some(k8s_openapi::api::core::v1::HTTPGetAction {
|
||||||
|
path: Some("/minio/health/live".to_string()),
|
||||||
|
port: k8s_openapi::apimachinery::pkg::util::intstr::IntOrString::Int(9000),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
initial_delay_seconds: Some(10),
|
||||||
|
period_seconds: Some(30),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
readiness_probe: Some(k8s_openapi::api::core::v1::Probe {
|
||||||
|
http_get: Some(k8s_openapi::api::core::v1::HTTPGetAction {
|
||||||
|
path: Some("/minio/health/ready".to_string()),
|
||||||
|
port: k8s_openapi::apimachinery::pkg::util::intstr::IntOrString::Int(9000),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
initial_delay_seconds: Some(10),
|
||||||
|
period_seconds: Some(30),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mc_container = Container {
|
||||||
|
name: "mc".to_string(),
|
||||||
|
env: Some(vec![
|
||||||
|
EnvVar {
|
||||||
|
name: "MINIO_ROOT_USER".to_string(),
|
||||||
|
value: Some("minio".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnvVar {
|
||||||
|
name: "MINIO_ROOT_PASSWORD".to_string(),
|
||||||
|
value: Some("password".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
image: Some("minio/mc".to_string()),
|
||||||
|
command: Some(vec!["bash".to_string(), "-c".to_string()]),
|
||||||
|
args: Some(vec!["until (/usr/bin/mc alias set minio http://minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD) do
|
||||||
|
echo 'Waiting to start minio...' && sleep 1;
|
||||||
|
done;
|
||||||
|
/usr/bin/mc mb --ignore-existing minio/neon --region=eu-north-1;
|
||||||
|
sleep inf;".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let deployment = Deployment {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some(NAME.to_string()),
|
||||||
|
namespace: Some(namespace.to_string()),
|
||||||
|
labels: Some(BTreeMap::from([("app".to_string(), NAME.to_string())])),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: Some(DeploymentSpec {
|
||||||
|
replicas: Some(1),
|
||||||
|
selector: LabelSelector {
|
||||||
|
match_labels: Some(BTreeMap::from([("app".to_string(), NAME.to_string())])),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
template: PodTemplateSpec {
|
||||||
|
metadata: Some(ObjectMeta {
|
||||||
|
labels: Some(BTreeMap::from([("app".to_string(), NAME.to_string())])),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
spec: Some(PodSpec {
|
||||||
|
containers: vec![minio_container, mc_container],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let deployment_api = Api::<Deployment>::namespaced(client, namespace);
|
||||||
|
|
||||||
|
deployment_api
|
||||||
|
.patch(
|
||||||
|
NAME,
|
||||||
|
&PatchParams::apply("neon-operator"),
|
||||||
|
&Patch::Apply(deployment),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_service(client: Client, namespace: &str) -> Result<(), Error> {
|
||||||
|
let service = Service {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some(NAME.to_string()),
|
||||||
|
namespace: Some(namespace.to_string()),
|
||||||
|
labels: Some(BTreeMap::from([("app".to_string(), NAME.to_string())])),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: Some(ServiceSpec {
|
||||||
|
selector: Some(BTreeMap::from([("app".to_string(), NAME.to_string())])),
|
||||||
|
ports: Some(vec![ServicePort {
|
||||||
|
port: 9000,
|
||||||
|
..Default::default()
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Reconciling service");
|
||||||
|
Api::<Service>::namespaced(client, namespace)
|
||||||
|
.patch(
|
||||||
|
NAME,
|
||||||
|
&PatchParams::apply("neon-operator"),
|
||||||
|
&Patch::Apply(service),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(err, skip(client))]
|
||||||
|
pub async fn deployment_exists(client: Client, namespace: &str) -> Result<bool, Error> {
|
||||||
|
let deployment_api: Api<Deployment> = Api::namespaced(client, namespace);
|
||||||
|
let lp = ListParams {
|
||||||
|
label_selector: Some(format!("app={}", NAME)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let list_result = deployment_api.list(&lp).await?;
|
||||||
|
Ok(list_result.items.len() > 0)
|
||||||
|
}
|
|
@ -0,0 +1,456 @@
|
||||||
|
use k8s_openapi::api::apps::v1::{Deployment, DeploymentSpec, StatefulSet};
|
||||||
|
use k8s_openapi::api::core::v1::{
|
||||||
|
Container, ContainerPort, EnvVar, PodSpec, PodTemplateSpec, Service, ServicePort, ServiceSpec,
|
||||||
|
};
|
||||||
|
use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
|
||||||
|
use kube::api::{ObjectMeta, Patch, PatchParams};
|
||||||
|
use kube::{Api, Client, Error};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
// Workflow:
|
||||||
|
// 1. Create minio deployment
|
||||||
|
// 2. Create minio service
|
||||||
|
// 3. Ensure bucket exists
|
||||||
|
|
||||||
|
pub async fn reconcile_storage_broker(client: Client, namespace: &str) -> Result<(), Error> {
|
||||||
|
let storage_broker = Deployment {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("storage-broker".to_string()),
|
||||||
|
namespace: Some(namespace.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: Some(DeploymentSpec {
|
||||||
|
replicas: Some(1),
|
||||||
|
selector: LabelSelector {
|
||||||
|
match_labels: Some(BTreeMap::from([(
|
||||||
|
"app".to_string(),
|
||||||
|
"storage-broker".to_string(),
|
||||||
|
)])),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
template: PodTemplateSpec {
|
||||||
|
metadata: Some(ObjectMeta {
|
||||||
|
labels: Some(BTreeMap::from([(
|
||||||
|
"app".to_string(),
|
||||||
|
"storage-broker".to_string(),
|
||||||
|
)])),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
spec: Some(PodSpec {
|
||||||
|
containers: vec![Container {
|
||||||
|
name: "storage-broker".to_string(),
|
||||||
|
image: Some("neondatabase/neon".to_string()),
|
||||||
|
ports: Some(vec![ContainerPort {
|
||||||
|
container_port: 50051,
|
||||||
|
..Default::default()
|
||||||
|
}]),
|
||||||
|
command: Some(vec![
|
||||||
|
"storage_broker".to_string(),
|
||||||
|
"--listen-addr=0.0.0.0:50051".to_string(),
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let deployment_api = Api::<Deployment>::namespaced(client.clone(), namespace);
|
||||||
|
deployment_api
|
||||||
|
.patch(
|
||||||
|
"storage-broker",
|
||||||
|
&PatchParams::apply("neon-operator"),
|
||||||
|
&Patch::Apply(storage_broker),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let service = Service {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("storage-broker".to_string()),
|
||||||
|
namespace: Some(namespace.to_string()),
|
||||||
|
labels: Some(BTreeMap::from([(
|
||||||
|
"app".to_string(),
|
||||||
|
"storage-broker".to_string(),
|
||||||
|
)])),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: Some(ServiceSpec {
|
||||||
|
selector: Some(BTreeMap::from([(
|
||||||
|
"app".to_string(),
|
||||||
|
"storage-broker".to_string(),
|
||||||
|
)])),
|
||||||
|
ports: Some(vec![ServicePort {
|
||||||
|
port: 50051,
|
||||||
|
..Default::default()
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let service_api = Api::<Service>::namespaced(client.clone(), namespace);
|
||||||
|
service_api
|
||||||
|
.patch(
|
||||||
|
"storage-broker",
|
||||||
|
&PatchParams::apply("neon-operator"),
|
||||||
|
&Patch::Apply(service),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reconcile_page_server(client: Client, namespace: &str) -> Result<(), Error> {
|
||||||
|
let deployment = Deployment {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("pageserver".to_string()),
|
||||||
|
namespace: Some(namespace.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: Some(DeploymentSpec {
|
||||||
|
replicas: Some(1),
|
||||||
|
selector: LabelSelector {
|
||||||
|
match_labels: Some(BTreeMap::from([(
|
||||||
|
"app".to_string(),
|
||||||
|
"pageserver".to_string(),
|
||||||
|
)])),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
template: PodTemplateSpec {
|
||||||
|
metadata: Some(ObjectMeta {
|
||||||
|
labels: Some(BTreeMap::from([(
|
||||||
|
"app".to_string(),
|
||||||
|
"pageserver".to_string(),
|
||||||
|
)])),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
spec: Some(PodSpec {
|
||||||
|
containers: vec![Container {
|
||||||
|
name: "pageserver".to_string(),
|
||||||
|
image: Some("neondatabase/neon".to_string()),
|
||||||
|
ports: Some(vec![ContainerPort {
|
||||||
|
container_port: 9898,
|
||||||
|
name: Some("http".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}, ContainerPort {
|
||||||
|
container_port: 6400,
|
||||||
|
name: Some("pg".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}]),
|
||||||
|
env: Some(vec![
|
||||||
|
EnvVar {
|
||||||
|
name: "BROKER_ENDPOINT".to_string(),
|
||||||
|
value: Some("storage-broker:50051".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnvVar {
|
||||||
|
name: "AWS_ACCESS_KEY_ID".to_string(),
|
||||||
|
value: Some("minio".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnvVar {
|
||||||
|
name: "AWS_SECRET_ACCESS_KEY".to_string(),
|
||||||
|
value: Some("password".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
command: Some(vec!["/usr/local/bin/pageserver".to_string()]),
|
||||||
|
args: Some(vec![
|
||||||
|
"-D".to_string(),
|
||||||
|
"/data/.neon/".to_string(),
|
||||||
|
"-c".to_string(),
|
||||||
|
r#"broker_endpoint='http://storage-broker:50051'"#.to_string(),
|
||||||
|
"-c".to_string(),
|
||||||
|
r#"listen_pg_addr='0.0.0.0:6400'"#.to_string(),
|
||||||
|
"-c".to_string(),
|
||||||
|
r#"listen_http_addr='0.0.0.0:9898'"#.to_string(),
|
||||||
|
"-c".to_string(),
|
||||||
|
r#"remote_storage={endpoint='http://minio:9000',bucket_name='neon',bucket_region='eu-north-1',prefix_in_bucket='/pageserver/'}"#.to_string(),
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let deployment_api = Api::<Deployment>::namespaced(client.clone(), namespace);
|
||||||
|
deployment_api
|
||||||
|
.patch(
|
||||||
|
"pageserver",
|
||||||
|
&PatchParams::apply("neon_operator"),
|
||||||
|
&Patch::Apply(deployment),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let service = Service {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("pageserver".to_string()),
|
||||||
|
namespace: Some(namespace.to_string()),
|
||||||
|
labels: Some(BTreeMap::from([(
|
||||||
|
"app".to_string(),
|
||||||
|
"pageserver".to_string(),
|
||||||
|
)])),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: Some(ServiceSpec {
|
||||||
|
selector: Some(BTreeMap::from([(
|
||||||
|
"app".to_string(),
|
||||||
|
"pageserver".to_string(),
|
||||||
|
)])),
|
||||||
|
ports: Some(vec![
|
||||||
|
ServicePort {
|
||||||
|
port: 9898,
|
||||||
|
name: Some("http".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
ServicePort {
|
||||||
|
port: 6400,
|
||||||
|
name: Some("pg".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let service_api = Api::<Service>::namespaced(client.clone(), namespace);
|
||||||
|
service_api
|
||||||
|
.patch(
|
||||||
|
"pageserver",
|
||||||
|
&PatchParams::apply("neon_operator"),
|
||||||
|
&Patch::Apply(service),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reconcile_safe_keepers(client: Client, namespace: &str) -> Result<(), Error> {
|
||||||
|
let stateful_set = StatefulSet {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("safe-keeper".to_string()),
|
||||||
|
namespace: Some(namespace.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: Some(k8s_openapi::api::apps::v1::StatefulSetSpec {
|
||||||
|
service_name: "safe-keeper".to_string(),
|
||||||
|
replicas: Some(3),
|
||||||
|
selector: LabelSelector {
|
||||||
|
match_labels: Some(BTreeMap::from([(
|
||||||
|
"app".to_string(),
|
||||||
|
"safe-keeper".to_string(),
|
||||||
|
)])),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
template: PodTemplateSpec {
|
||||||
|
metadata: Some(ObjectMeta {
|
||||||
|
labels: Some(BTreeMap::from([(
|
||||||
|
"app".to_string(),
|
||||||
|
"safe-keeper".to_string(),
|
||||||
|
)])),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
spec: Some(PodSpec {
|
||||||
|
containers: vec![Container {
|
||||||
|
name: "safe-keeper".to_string(),
|
||||||
|
image: Some("neondatabase/neon".to_string()),
|
||||||
|
ports: Some(vec![ContainerPort {
|
||||||
|
container_port: 7676,
|
||||||
|
..Default::default()
|
||||||
|
}]),
|
||||||
|
command: Some(vec![
|
||||||
|
"bash".to_string(),
|
||||||
|
"-c".to_string(),
|
||||||
|
]),
|
||||||
|
args: Some(vec![
|
||||||
|
r#"[[ `hostname` =~ -([0-9]+)$ ]] || exit 1
|
||||||
|
export ordinal=${BASH_REMATCH[1]}
|
||||||
|
safekeeper --listen-pg=$SAFEKEEPER_ADVERTISE_URL \
|
||||||
|
--listen-http='0.0.0.0:7676' \
|
||||||
|
--id=$ordinal \
|
||||||
|
--broker-endpoint=$BROKER_ENDPOINT \
|
||||||
|
-D /data \
|
||||||
|
--remote-storage="{endpoint='http://minio:9000',bucket_name='neon',bucket_region='eu-north-1',prefix_in_bucket='/safekeeper/'}"
|
||||||
|
"#.to_string(),
|
||||||
|
]),
|
||||||
|
env: Some(vec![
|
||||||
|
EnvVar {
|
||||||
|
name: "POD_NAME".to_string(),
|
||||||
|
value_from: Some(k8s_openapi::api::core::v1::EnvVarSource {
|
||||||
|
field_ref: Some(k8s_openapi::api::core::v1::ObjectFieldSelector {
|
||||||
|
field_path: "metadata.name".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnvVar {
|
||||||
|
name: "SAFEKEEPER_ADVERTISE_URL".to_string(),
|
||||||
|
value: Some("$(POD_NAME).safe-keeper.default.svc.cluster.local:5454".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnvVar {
|
||||||
|
name: "BROKER_ENDPOINT".to_string(),
|
||||||
|
value: Some("http://storage-broker:50051".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnvVar {
|
||||||
|
name: "AWS_ACCESS_KEY_ID".to_string(),
|
||||||
|
value: Some("minio".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnvVar {
|
||||||
|
name: "AWS_SECRET_ACCESS_KEY".to_string(),
|
||||||
|
value: Some("password".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let headless_service = Service {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("safe-keeper".to_string()),
|
||||||
|
namespace: Some(namespace.to_string()),
|
||||||
|
labels: Some(BTreeMap::from([(
|
||||||
|
"app".to_string(),
|
||||||
|
"safe-keeper".to_string(),
|
||||||
|
)])),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: Some(ServiceSpec {
|
||||||
|
cluster_ip: Some("None".to_string()),
|
||||||
|
selector: Some(BTreeMap::from([(
|
||||||
|
"app".to_string(),
|
||||||
|
"safe-keeper".to_string(),
|
||||||
|
)])),
|
||||||
|
ports: Some(vec![ServicePort {
|
||||||
|
port: 5454,
|
||||||
|
..Default::default()
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let stateful_set_api: Api<StatefulSet> = Api::namespaced(client.clone(), &namespace);
|
||||||
|
stateful_set_api
|
||||||
|
.patch(
|
||||||
|
"safe-keeper",
|
||||||
|
&PatchParams::apply("neon_operator"),
|
||||||
|
&Patch::Apply(stateful_set),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let service_api = Api::<Service>::namespaced(client.clone(), &namespace);
|
||||||
|
service_api
|
||||||
|
.patch(
|
||||||
|
"safe-keeper",
|
||||||
|
&PatchParams::apply("neon_operator"),
|
||||||
|
&Patch::Apply(headless_service),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reconcile_compute(client: Client, namespace: &str, image: &str) -> Result<(), Error> {
|
||||||
|
let deployment = Deployment {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("compute".to_string()),
|
||||||
|
namespace: Some(namespace.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: Some(DeploymentSpec {
|
||||||
|
replicas: Some(1),
|
||||||
|
selector: LabelSelector {
|
||||||
|
match_labels: Some(BTreeMap::from([("app".to_string(), "compute".to_string())])),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
template: PodTemplateSpec {
|
||||||
|
metadata: Some(ObjectMeta {
|
||||||
|
labels: Some(BTreeMap::from([("app".to_string(), "compute".to_string())])),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
spec: Some(PodSpec {
|
||||||
|
containers: vec![Container {
|
||||||
|
name: "compute".to_string(),
|
||||||
|
image: Some(image.to_string()),
|
||||||
|
ports: Some(vec![ContainerPort {
|
||||||
|
container_port: 9898,
|
||||||
|
..Default::default()
|
||||||
|
}]),
|
||||||
|
env: Some(vec![EnvVar {
|
||||||
|
name: "PG_VERSION".to_string(),
|
||||||
|
value: Some("15".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}]),
|
||||||
|
command: Some(vec!["/shell/compute.sh".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let service = Service {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("compute".to_string()),
|
||||||
|
namespace: Some(namespace.to_string()),
|
||||||
|
labels: Some(BTreeMap::from([("app".to_string(), "compute".to_string())])),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
spec: Some(ServiceSpec {
|
||||||
|
selector: Some(BTreeMap::from([("app".to_string(), "compute".to_string())])),
|
||||||
|
ports: Some(vec![ServicePort {
|
||||||
|
port: 9898,
|
||||||
|
..Default::default()
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let deployment_api = Api::<Deployment>::namespaced(client.clone(), &namespace);
|
||||||
|
deployment_api
|
||||||
|
.patch(
|
||||||
|
"compute",
|
||||||
|
&PatchParams::apply("neon_operator"),
|
||||||
|
&Patch::Apply(deployment),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let service_api = Api::<Service>::namespaced(client.clone(), &namespace);
|
||||||
|
service_api
|
||||||
|
.patch(
|
||||||
|
"compute",
|
||||||
|
&PatchParams::apply("neon_operator"),
|
||||||
|
&Patch::Apply(service),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue