merge with master and resolve conflicts

This commit is contained in:
Felix Kunde 2020-02-25 12:30:47 +01:00
commit b38d72d9a2
41 changed files with 486 additions and 209 deletions

View File

@ -1,7 +1,7 @@
apiVersion: v1 apiVersion: v1
name: postgres-operator-ui name: postgres-operator-ui
version: 0.1.0 version: 1.4.0
appVersion: 1.2.0 appVersion: 1.4.0
home: https://github.com/zalando/postgres-operator home: https://github.com/zalando/postgres-operator
description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience description: Postgres Operator UI provides a graphical interface for a convenient database-as-a-service user experience
keywords: keywords:
@ -12,6 +12,8 @@ keywords:
- patroni - patroni
- spilo - spilo
maintainers: maintainers:
- name: Zalando
email: opensource@zalando.de
- name: siku4 - name: siku4
email: sk@sik-net.de email: sk@sik-net.de
sources: sources:

View File

@ -0,0 +1,29 @@
apiVersion: v1
entries:
postgres-operator-ui:
- apiVersion: v1
appVersion: 1.4.0
created: "2020-02-24T15:32:47.610967635+01:00"
description: Postgres Operator UI provides a graphical interface for a convenient
database-as-a-service user experience
digest: 00e0eff7056d56467cd5c975657fbb76c8d01accd25a4b7aca81bc42aeac961d
home: https://github.com/zalando/postgres-operator
keywords:
- postgres
- operator
- ui
- cloud-native
- patroni
- spilo
maintainers:
- email: opensource@zalando.de
name: Zalando
- email: sk@sik-net.de
name: siku4
name: postgres-operator-ui
sources:
- https://github.com/zalando/postgres-operator
urls:
- postgres-operator-ui-1.4.0.tgz
version: 1.4.0
generated: "2020-02-24T15:32:47.610348278+01:00"

View File

@ -8,7 +8,7 @@ replicaCount: 1
image: image:
registry: registry.opensource.zalan.do registry: registry.opensource.zalan.do
repository: acid/postgres-operator-ui repository: acid/postgres-operator-ui
tag: v1.2.0 tag: v1.4.0
pullPolicy: "IfNotPresent" pullPolicy: "IfNotPresent"
rbac: rbac:

View File

@ -1,7 +1,7 @@
apiVersion: v1 apiVersion: v1
name: postgres-operator name: postgres-operator
version: 1.3.0 version: 1.4.0
appVersion: 1.3.0 appVersion: 1.4.0
home: https://github.com/zalando/postgres-operator home: https://github.com/zalando/postgres-operator
description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes
keywords: keywords:

View File

@ -94,7 +94,7 @@ spec:
s3_secret_access_key: s3_secret_access_key:
type: string type: string
s3_force_path_style: s3_force_path_style:
type: string type: boolean
s3_wal_path: s3_wal_path:
type: string type: string
timestamp: timestamp:

View File

@ -1,9 +1,31 @@
apiVersion: v1 apiVersion: v1
entries: entries:
postgres-operator: postgres-operator:
- apiVersion: v1
appVersion: 1.4.0
created: "2020-02-20T17:39:25.443276193+01:00"
description: Postgres Operator creates and manages PostgreSQL clusters running
in Kubernetes
digest: b93ccde5581deb8ed0857136b8ce74ca3f1b7240438fa4415f705764a1300bed
home: https://github.com/zalando/postgres-operator
keywords:
- postgres
- operator
- cloud-native
- patroni
- spilo
maintainers:
- email: opensource@zalando.de
name: Zalando
name: postgres-operator
sources:
- https://github.com/zalando/postgres-operator
urls:
- postgres-operator-1.4.0.tgz
version: 1.4.0
- apiVersion: v1 - apiVersion: v1
appVersion: 1.3.0 appVersion: 1.3.0
created: "2019-12-17T12:58:49.477140129+01:00" created: "2020-02-20T17:39:25.441532163+01:00"
description: Postgres Operator creates and manages PostgreSQL clusters running description: Postgres Operator creates and manages PostgreSQL clusters running
in Kubernetes in Kubernetes
digest: 7e788fd37daec76a01f6d6f9fe5be5b54f5035e4eba0041e80a760d656537325 digest: 7e788fd37daec76a01f6d6f9fe5be5b54f5035e4eba0041e80a760d656537325
@ -25,7 +47,7 @@ entries:
version: 1.3.0 version: 1.3.0
- apiVersion: v1 - apiVersion: v1
appVersion: 1.2.0 appVersion: 1.2.0
created: "2019-12-17T12:58:49.475844233+01:00" created: "2020-02-20T17:39:25.440278302+01:00"
description: Postgres Operator creates and manages PostgreSQL clusters running description: Postgres Operator creates and manages PostgreSQL clusters running
in Kubernetes in Kubernetes
digest: d10710c7cf19f4e266e7704f5d1e98dcfc61bee3919522326c35c22ca7d2f2bf digest: d10710c7cf19f4e266e7704f5d1e98dcfc61bee3919522326c35c22ca7d2f2bf
@ -47,4 +69,4 @@ entries:
urls: urls:
- postgres-operator-1.2.0.tgz - postgres-operator-1.2.0.tgz
version: 1.2.0 version: 1.2.0
generated: "2019-12-17T12:58:49.474719294+01:00" generated: "2020-02-20T17:39:25.439168098+01:00"

Binary file not shown.

View File

@ -63,9 +63,9 @@ rules:
- secrets - secrets
verbs: verbs:
- create - create
- update
- delete - delete
- get - get
- update
# to check nodes for node readiness label # to check nodes for node readiness label
- apiGroups: - apiGroups:
- "" - ""

View File

@ -8,6 +8,7 @@ metadata:
app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/instance: {{ .Release.Name }}
name: {{ template "postgres-operator.fullname" . }} name: {{ template "postgres-operator.fullname" . }}
spec: spec:
type: ClusterIP
ports: ports:
- port: 8080 - port: 8080
protocol: TCP protocol: TCP
@ -15,7 +16,3 @@ spec:
selector: selector:
app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/name: {{ template "postgres-operator.name" . }} app.kubernetes.io/name: {{ template "postgres-operator.name" . }}
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}

View File

@ -1,7 +1,7 @@
image: image:
registry: registry.opensource.zalan.do registry: registry.opensource.zalan.do
repository: acid/postgres-operator repository: acid/postgres-operator
tag: v1.3.1 tag: v1.4.0
pullPolicy: "IfNotPresent" pullPolicy: "IfNotPresent"
# Optionally specify an array of imagePullSecrets. # Optionally specify an array of imagePullSecrets.
@ -24,7 +24,7 @@ configGeneral:
# etcd connection string for Patroni. Empty uses K8s-native DCS. # etcd connection string for Patroni. Empty uses K8s-native DCS.
etcd_host: "" etcd_host: ""
# Spilo docker image # Spilo docker image
docker_image: registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16 docker_image: registry.opensource.zalan.do/acid/spilo-12:1.6-p2
# max number of instances in Postgres cluster. -1 = no limit # max number of instances in Postgres cluster. -1 = no limit
min_instances: -1 min_instances: -1
# min number of instances in Postgres cluster. -1 = no limit # min number of instances in Postgres cluster. -1 = no limit

View File

@ -1,7 +1,7 @@
image: image:
registry: registry.opensource.zalan.do registry: registry.opensource.zalan.do
repository: acid/postgres-operator repository: acid/postgres-operator
tag: v1.3.1 tag: v1.4.0
pullPolicy: "IfNotPresent" pullPolicy: "IfNotPresent"
# Optionally specify an array of imagePullSecrets. # Optionally specify an array of imagePullSecrets.
@ -24,7 +24,7 @@ configGeneral:
# etcd connection string for Patroni. Empty uses K8s-native DCS. # etcd connection string for Patroni. Empty uses K8s-native DCS.
etcd_host: "" etcd_host: ""
# Spilo docker image # Spilo docker image
docker_image: registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16 docker_image: registry.opensource.zalan.do/acid/spilo-12:1.6-p2
# max number of instances in Postgres cluster. -1 = no limit # max number of instances in Postgres cluster. -1 = no limit
min_instances: "-1" min_instances: "-1"
# min number of instances in Postgres cluster. -1 = no limit # min number of instances in Postgres cluster. -1 = no limit
@ -55,7 +55,7 @@ configKubernetes:
# additional labels assigned to the cluster objects # additional labels assigned to the cluster objects
cluster_labels: application:spilo cluster_labels: application:spilo
# label assigned to Kubernetes objects created by the operator # label assigned to Kubernetes objects created by the operator
cluster_name_label: version cluster_name_label: cluster-name
# annotations attached to each database pod # annotations attached to each database pod
# custom_pod_annotations: "keya:valuea,keyb:valueb" # custom_pod_annotations: "keya:valuea,keyb:valueb"

View File

@ -66,20 +66,13 @@ pipeline:
- desc: 'Build and push Docker image' - desc: 'Build and push Docker image'
cmd: | cmd: |
cd ui cd ui
image_base='registry-write.opensource.zalan.do/acid/postgres-operator-ui' IS_PR_BUILD=${CDP_PULL_REQUEST_NUMBER+"true"}
if [[ "${CDP_TARGET_BRANCH}" == 'master' && -z "${CDP_PULL_REQUEST_NUMBER}" ]] if [[ ${CDP_TARGET_BRANCH} == "master" && ${IS_PR_BUILD} != "true" ]]
then then
image="${image_base}" IMAGE=registry-write.opensource.zalan.do/acid/postgres-operator-ui
else else
image="${image_base}-test" IMAGE=registry-write.opensource.zalan.do/acid/postgres-operator-ui-test
fi fi
image_with_tag="${image}:c${CDP_BUILD_VERSION}" export IMAGE
make docker
if docker pull "${image}" make push
then
docker build --cache-from="${image}" -t "${image_with_tag}" .
else
docker build -t "${image_with_tag}" .
fi
docker push "${image_with_tag}"

View File

@ -11,11 +11,11 @@ switchover (planned failover) of the master to the Pod with new minor version.
The switch should usually take less than 5 seconds, still clients have to The switch should usually take less than 5 seconds, still clients have to
reconnect. reconnect.
Major version upgrades are supported via [cloning](user.md#clone-directly). The Major version upgrades are supported via [cloning](user.md#how-to-clone-an-existing-postgresql-cluster).
new cluster manifest must have a higher `version` string than the source cluster The new cluster manifest must have a higher `version` string than the source
and will be created from a basebackup. Depending of the cluster size, downtime cluster and will be created from a basebackup. Depending of the cluster size,
in this case can be significant as writes to the database should be stopped and downtime in this case can be significant as writes to the database should be
all WAL files should be archived first before cloning is started. stopped and all WAL files should be archived first before cloning is started.
Note, that simply changing the version string in the `postgresql` manifest does Note, that simply changing the version string in the `postgresql` manifest does
not work at present and leads to errors. Neither Patroni nor Postgres Operator not work at present and leads to errors. Neither Patroni nor Postgres Operator
@ -481,37 +481,71 @@ A secret can be pre-provisioned in different ways:
## Setting up the Postgres Operator UI ## Setting up the Postgres Operator UI
With the v1.2 release the Postgres Operator is shipped with a browser-based Since the v1.2 release the Postgres Operator is shipped with a browser-based
configuration user interface (UI) that simplifies managing Postgres clusters configuration user interface (UI) that simplifies managing Postgres clusters
with the operator. The UI runs with Node.js and comes with it's own Docker with the operator.
image.
Run NPM to continuously compile `tags/js` code. Basically, it creates an ### Building the UI image
`app.js` file in: `static/build/app.js`
``` The UI runs with Node.js and comes with it's own Docker
(cd ui/app && npm start) image. However, installing Node.js to build the operator UI is not required. It
``` is handled via Docker containers when running:
To build the Docker image open a shell and change to the `ui` folder. Then run:
```bash ```bash
docker build -t registry.opensource.zalan.do/acid/postgres-operator-ui:v1.2.0 . make docker
``` ```
Apply all manifests for the `ui/manifests` folder to deploy the Postgres ### Configure endpoints and options
Operator UI on K8s. For local tests you don't need the Ingress resource.
The UI talks to the K8s API server as well as the Postgres Operator [REST API](developer.md#debugging-the-operator).
K8s API server URLs are loaded from the machine's kubeconfig environment by
default. Alternatively, a list can also be passed when starting the Python
application with the `--cluster` option.
The Operator API endpoint can be configured via the `OPERATOR_API_URL`
environment variables in the [deployment manifest](../ui/manifests/deployment.yaml#L40).
You can also expose the operator API through a [service](../manifests/api-service.yaml).
Some displayed options can be disabled from UI using simple flags under the
`OPERATOR_UI_CONFIG` field in the deployment.
### Deploy the UI on K8s
Now, apply all manifests from the `ui/manifests` folder to deploy the Postgres
Operator UI on K8s. Replace the image tag in the deployment manifest if you
want to test the image you've built with `make docker`. Make sure the pods for
the operator and the UI are both running.
```bash ```bash
kubectl apply -f ui/manifests sed -e "s/\(image\:.*\:\).*$/\1$TAG/" manifests/deployment.yaml | kubectl apply -f manifests/
kubectl get all -l application=postgres-operator-ui
``` ```
Make sure the pods for the operator and the UI are both running. For local ### Local testing
testing you need to apply proxying and port forwarding so that the UI can talk
to the K8s and Postgres Operator REST API. You can use the provided For local testing you need to apply K8s proxying and operator pod port
`run_local.sh` script for this. Make sure it uses the correct URL to your K8s forwarding so that the UI can talk to the K8s and Postgres Operator REST API.
API server, e.g. for minikube it would be `https://192.168.99.100:8443`. The Ingress resource is not needed. You can use the provided `run_local.sh`
script for this. Make sure that:
* Python dependencies are installed on your machine
* the K8s API server URL is set for kubectl commands, e.g. for minikube it would usually be `https://192.168.99.100:8443`.
* the pod label selectors for port forwarding are correct
When testing with minikube you have to build the image in its docker environment
(running `make docker` doesn't do it for you). From the `ui` directory execute:
```bash ```bash
# compile and build operator UI
make docker
# build in image in minikube docker env
eval $(minikube docker-env)
docker build -t registry.opensource.zalan.do/acid/postgres-operator-ui:v1.3.0 .
# apply UI manifests next to a running Postgres Operator
kubectl apply -f manifests/
# install python dependencies to run UI locally
pip3 install -r requirements
./run_local.sh ./run_local.sh
``` ```

View File

@ -31,9 +31,13 @@ status page.
![pgui-waiting-for-master](diagrams/pgui-waiting-for-master.png "Waiting for master pod") ![pgui-waiting-for-master](diagrams/pgui-waiting-for-master.png "Waiting for master pod")
Usually, the startup should only take up to 1 minute. If you feel the process Usually, the startup should only take up to 1 minute. If you feel the process
got stuck click on the "Logs" button to inspect the operator logs. From the got stuck click on the "Logs" button to inspect the operator logs. If the logs
"Status" field in the top menu you can also retrieve the logs and queue of each look fine, but the UI seems to got stuck, check if you are have configured the
worker the operator is using. The number of concurrent workers can be same [cluster name label](../ui/manifests/deployment.yaml#L45) like for the
[operator](../manifests/configmap.yaml#L13).
From the "Status" field in the top menu you can also retrieve the logs and queue
of each worker the operator is using. The number of concurrent workers can be
[configured](reference/operator_parameters.md#general). [configured](reference/operator_parameters.md#general).
![pgui-operator-logs](diagrams/pgui-operator-logs.png "Checking operator logs") ![pgui-operator-logs](diagrams/pgui-operator-logs.png "Checking operator logs")

View File

@ -52,6 +52,7 @@ cd postgres-operator
kubectl create -f manifests/configmap.yaml # configuration kubectl create -f manifests/configmap.yaml # configuration
kubectl create -f manifests/operator-service-account-rbac.yaml # identity and permissions kubectl create -f manifests/operator-service-account-rbac.yaml # identity and permissions
kubectl create -f manifests/postgres-operator.yaml # deployment kubectl create -f manifests/postgres-operator.yaml # deployment
kubectl create -f manifests/api-service.yaml # operator API to be used by UI
``` ```
There is a [Kustomization](https://github.com/kubernetes-sigs/kustomize) There is a [Kustomization](https://github.com/kubernetes-sigs/kustomize)
@ -104,7 +105,7 @@ kubectl create -f https://operatorhub.io/install/postgres-operator.yaml
This installs the operator in the `operators` namespace. More information can be This installs the operator in the `operators` namespace. More information can be
found on [operatorhub.io](https://operatorhub.io/operator/postgres-operator). found on [operatorhub.io](https://operatorhub.io/operator/postgres-operator).
## Create a Postgres cluster ## Check if Postgres Operator is running
Starting the operator may take a few seconds. Check if the operator pod is Starting the operator may take a few seconds. Check if the operator pod is
running before applying a Postgres cluster manifest. running before applying a Postgres cluster manifest.
@ -115,7 +116,61 @@ kubectl get pod -l name=postgres-operator
# if you've created the operator using helm chart # if you've created the operator using helm chart
kubectl get pod -l app.kubernetes.io/name=postgres-operator kubectl get pod -l app.kubernetes.io/name=postgres-operator
```
If the operator doesn't get into `Running` state, either check the latest K8s
events of the deployment or pod with `kubectl describe` or inspect the operator
logs:
```bash
kubectl logs "$(kubectl get pod -l name=postgres-operator --output='name')"
```
## Deploy the operator UI
In the following paragraphs we describe how to access and manage PostgreSQL
clusters from the command line with kubectl. But it can also be done from the
browser-based [Postgres Operator UI](operator-ui.md). Before deploying the UI
make sure the operator is running and its REST API is reachable through a
[K8s service](../manifests/api-service.yaml). The URL to this API must be
configured in the [deployment manifest](../ui/manifests/deployment.yaml#L43)
of the UI.
To deploy the UI simply apply all its manifests files or use the UI helm chart:
```bash
# manual deployment
kubectl apply -f ui/manifests/
# or helm chart
helm install postgres-operator-ui ./charts/postgres-operator-ui
```
Like with the operator, check if the UI pod gets into `Running` state:
```bash
# if you've created the operator using yaml manifests
kubectl get pod -l name=postgres-operator-ui
# if you've created the operator using helm chart
kubectl get pod -l app.kubernetes.io/name=postgres-operator-ui
```
You can now access the web interface by port forwarding the UI pod (mind the
label selector) and enter `localhost:8081` in your browser:
```bash
kubectl port-forward "$(kubectl get pod -l name=postgres-operator-ui --output='name')" 8081
```
Available option are explained in detail in the [UI docs](operator-ui.md).
## Create a Postgres cluster
If the operator pod is running it listens to new events regarding `postgresql`
resources. Now, it's time to submit your first Postgres cluster manifest.
```bash
# create a Postgres cluster # create a Postgres cluster
kubectl create -f manifests/minimal-postgres-manifest.yaml kubectl create -f manifests/minimal-postgres-manifest.yaml
``` ```

View File

@ -110,8 +110,10 @@ Those are top-level keys, containing both leaf keys and groups.
* **min_instances** * **min_instances**
operator will run at least the number of instances for any given Postgres operator will run at least the number of instances for any given Postgres
cluster equal to the value of this parameter. When `-1` is specified, no cluster equal to the value of this parameter. Standby clusters can still run
limits are applied. The default is `-1`. with `numberOfInstances: 1` as this is the [recommended setup](../user.md#setting-up-a-standby-cluster).
When `-1` is specified for `min_instances`, no limits are applied. The default
is `-1`.
* **resync_period** * **resync_period**
period between consecutive sync requests. The default is `30m`. period between consecutive sync requests. The default is `30m`.

View File

@ -65,7 +65,7 @@ our test cluster.
```bash ```bash
# get name of master pod of acid-minimal-cluster # get name of master pod of acid-minimal-cluster
export PGMASTER=$(kubectl get pods -o jsonpath={.items..metadata.name} -l application=spilo,version=acid-minimal-cluster,spilo-role=master) export PGMASTER=$(kubectl get pods -o jsonpath={.items..metadata.name} -l application=spilo,cluster-name=acid-minimal-cluster,spilo-role=master)
# set up port forward # set up port forward
kubectl port-forward $PGMASTER 6432:5432 kubectl port-forward $PGMASTER 6432:5432
@ -254,29 +254,22 @@ spec:
## How to clone an existing PostgreSQL cluster ## How to clone an existing PostgreSQL cluster
You can spin up a new cluster as a clone of the existing one, using a clone You can spin up a new cluster as a clone of the existing one, using a `clone`
section in the spec. There are two options here: section in the spec. There are two options here:
* Clone directly from a source cluster using `pg_basebackup` * Clone from an S3 bucket (recommended)
* Clone from an S3 bucket * Clone directly from a source cluster
### Clone directly Note, that cloning can also be used for [major version upgrades](administrator.md#minor-and-major-version-upgrade)
of PostgreSQL.
```yaml
spec:
clone:
cluster: "acid-batman"
```
Here `cluster` is a name of a source cluster that is going to be cloned. The
cluster to clone is assumed to be running and the clone procedure invokes
`pg_basebackup` from it. The operator will setup the cluster to be cloned to
connect to the service of the source cluster by name (if the cluster is called
test, then the connection string will look like host=test port=5432), which
means that you can clone only from clusters within the same namespace.
### Clone from S3 ### Clone from S3
Cloning from S3 has the advantage that there is no impact on your production
database. A new Postgres cluster is created by restoring the data of another
source cluster. If you create it in the same Kubernetes environment, use a
different name.
```yaml ```yaml
spec: spec:
clone: clone:
@ -287,7 +280,8 @@ spec:
Here `cluster` is a name of a source cluster that is going to be cloned. A new Here `cluster` is a name of a source cluster that is going to be cloned. A new
cluster will be cloned from S3, using the latest backup before the `timestamp`. cluster will be cloned from S3, using the latest backup before the `timestamp`.
In this case, `uid` field is also mandatory - operator will use it to find a Note, that a time zone is required for `timestamp` in the format of +00:00 which
is UTC. The `uid` field is also mandatory. The operator will use it to find a
correct key inside an S3 bucket. You can find this field in the metadata of the correct key inside an S3 bucket. You can find this field in the metadata of the
source cluster: source cluster:
@ -299,9 +293,6 @@ metadata:
uid: efd12e58-5786-11e8-b5a7-06148230260c uid: efd12e58-5786-11e8-b5a7-06148230260c
``` ```
Note that timezone is required for `timestamp`. Otherwise, offset is relative
to UTC, see [RFC 3339 section 5.6) 3339 section 5.6](https://www.ietf.org/rfc/rfc3339.txt).
For non AWS S3 following settings can be set to support cloning from other S3 For non AWS S3 following settings can be set to support cloning from other S3
implementations: implementations:
@ -317,14 +308,35 @@ spec:
s3_force_path_style: true s3_force_path_style: true
``` ```
### Clone directly
Another way to get a fresh copy of your source DB cluster is via basebackup. To
use this feature simply leave out the timestamp field from the clone section.
The operator will connect to the service of the source cluster by name. If the
cluster is called test, then the connection string will look like host=test
port=5432), which means that you can clone only from clusters within the same
namespace.
```yaml
spec:
clone:
cluster: "acid-batman"
```
Be aware that on a busy source database this can result in an elevated load!
## Setting up a standby cluster ## Setting up a standby cluster
Standby clusters are like normal cluster but they are streaming from a remote Standby cluster is a [Patroni feature](https://github.com/zalando/patroni/blob/master/docs/replica_bootstrap.rst#standby-cluster)
cluster. As the first version of this feature, the only scenario covered by that first clones a database, and keeps replicating changes afterwards. As the
operator is to stream from a WAL archive of the master. Following the more replication is happening by the means of archived WAL files (stored on S3 or
popular infrastructure of using Amazon's S3 buckets, it is mentioned as the equivalent of other cloud providers), the standby cluster can exist in a
`s3_wal_path` here. To start a cluster as standby add the following `standby` different location than its source database. Unlike cloning, the PostgreSQL
section in the YAML file: version between source and target cluster has to be the same.
To start a cluster as standby, add the following `standby` section in the YAML
file and specify the S3 bucket path. An empty path will result in an error and
no statefulset will be created.
```yaml ```yaml
spec: spec:
@ -332,20 +344,65 @@ spec:
s3_wal_path: "s3 bucket path to the master" s3_wal_path: "s3 bucket path to the master"
``` ```
Things to note: At the moment, the operator only allows to stream from the WAL archive of the
master. Thus, it is recommended to deploy standby clusters with only [one pod](../manifests/standby-manifest.yaml#L10).
You can raise the instance count when detaching. Note, that the same pod role
labels like for normal clusters are used: The standby leader is labeled as
`master`.
- An empty string in the `s3_wal_path` field of the standby cluster will result ### Providing credentials of source cluster
in an error and no statefulset will be created.
- Only one pod can be deployed for stand-by cluster. A standby cluster is replicating the data (including users and passwords) from
- To manually promote the standby_cluster, use `patronictl` and remove config the source database and is read-only. The system and application users (like
entry. standby, postgres etc.) all have a password that does not match the credentials
- There is no way to transform a non-standby cluster to a standby cluster stored in secrets which are created by the operator. One solution is to create
through the operator. Adding the standby section to the manifest of a running secrets beforehand and paste in the credentials of the source cluster.
Postgres cluster will have no effect. However, it can be done through Patroni Otherwise, you will see errors in the Postgres logs saying users cannot log in
by adding the [standby_cluster](https://github.com/zalando/patroni/blob/bd2c54581abb42a7d3a3da551edf0b8732eefd27/docs/replica_bootstrap.rst#standby-cluster) and the operator logs will complain about not being able to sync resources.
section using `patronictl edit-config`. Note that the transformed standby
cluster will not be doing any streaming. It will be in standby mode and allow When you only run a standby leader, you can safely ignore this, as it will be
read-only transactions only. sorted out once the cluster is detached from the source. It is also harmless if
you dont plan it. But, when you created a standby replica, too, fix the
credentials right away. WAL files will pile up on the standby leader if no
connection can be established between standby replica(s). You can also edit the
secrets after their creation. Find them by:
```bash
kubectl get secrets --all-namespaces | grep <standby-cluster-name>
```
### Promote the standby
One big advantage of standby clusters is that they can be promoted to a proper
database cluster. This means it will stop replicating changes from the source,
and start accept writes itself. This mechanism makes it possible to move
databases from one place to another with minimal downtime. Currently, the
operator does not support promoting a standby cluster. It has to be done
manually using `patronictl edit-config` inside the postgres container of the
standby leader pod. Remove the following lines from the YAML structure and the
leader promotion happens immediately. Before doing so, make sure that the
standby is not behind the source database.
```yaml
standby_cluster:
create_replica_methods:
- bootstrap_standby_with_wale
- basebackup_fast_xlog
restore_command: envdir "/home/postgres/etc/wal-e.d/env-standby" /scripts/restore_command.sh
"%f" "%p"
```
Finally, remove the `standby` section from the postgres cluster manifest.
### Turn a normal cluster into a standby
There is no way to transform a non-standby cluster to a standby cluster through
the operator. Adding the `standby` section to the manifest of a running
Postgres cluster will have no effect. But, as explained in the previous
paragraph it can be done manually through `patronictl edit-config`. This time,
by adding the `standby_cluster` section to the Patroni configuration. However,
the transformed standby cluster will not be doing any streaming. It will be in
standby mode and allow read-only transactions only.
## Sidecar Support ## Sidecar Support

View File

@ -65,7 +65,7 @@ class EndToEndTestCase(unittest.TestCase):
''' '''
k8s = self.k8s k8s = self.k8s
cluster_label = 'version=acid-minimal-cluster' cluster_label = 'cluster-name=acid-minimal-cluster'
# enable load balancer services # enable load balancer services
pg_patch_enable_lbs = { pg_patch_enable_lbs = {
@ -113,7 +113,7 @@ class EndToEndTestCase(unittest.TestCase):
Lower resource limits below configured minimum and let operator fix it Lower resource limits below configured minimum and let operator fix it
''' '''
k8s = self.k8s k8s = self.k8s
cluster_label = 'version=acid-minimal-cluster' cluster_label = 'cluster-name=acid-minimal-cluster'
_, failover_targets = k8s.get_pg_nodes(cluster_label) _, failover_targets = k8s.get_pg_nodes(cluster_label)
# configure minimum boundaries for CPU and memory limits # configure minimum boundaries for CPU and memory limits
@ -172,7 +172,7 @@ class EndToEndTestCase(unittest.TestCase):
k8s.create_with_kubectl("manifests/complete-postgres-manifest.yaml") k8s.create_with_kubectl("manifests/complete-postgres-manifest.yaml")
k8s.wait_for_pod_start("spilo-role=master", self.namespace) k8s.wait_for_pod_start("spilo-role=master", self.namespace)
self.assert_master_is_unique(self.namespace, version="acid-test-cluster") self.assert_master_is_unique(self.namespace, "acid-test-cluster")
@timeout_decorator.timeout(TEST_TIMEOUT_SEC) @timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_scaling(self): def test_scaling(self):
@ -180,7 +180,7 @@ class EndToEndTestCase(unittest.TestCase):
Scale up from 2 to 3 and back to 2 pods by updating the Postgres manifest at runtime. Scale up from 2 to 3 and back to 2 pods by updating the Postgres manifest at runtime.
''' '''
k8s = self.k8s k8s = self.k8s
labels = "version=acid-minimal-cluster" labels = "cluster-name=acid-minimal-cluster"
k8s.wait_for_pg_to_scale(3) k8s.wait_for_pg_to_scale(3)
self.assertEqual(3, k8s.count_pods_with_label(labels)) self.assertEqual(3, k8s.count_pods_with_label(labels))
@ -196,7 +196,7 @@ class EndToEndTestCase(unittest.TestCase):
Add taint "postgres=:NoExecute" to node with master. This must cause a failover. Add taint "postgres=:NoExecute" to node with master. This must cause a failover.
''' '''
k8s = self.k8s k8s = self.k8s
cluster_label = 'version=acid-minimal-cluster' cluster_label = 'cluster-name=acid-minimal-cluster'
# get nodes of master and replica(s) (expected target of new master) # get nodes of master and replica(s) (expected target of new master)
current_master_node, failover_targets = k8s.get_pg_nodes(cluster_label) current_master_node, failover_targets = k8s.get_pg_nodes(cluster_label)
@ -334,9 +334,9 @@ class EndToEndTestCase(unittest.TestCase):
"foo": "bar", "foo": "bar",
} }
self.assertTrue(k8s.check_service_annotations( self.assertTrue(k8s.check_service_annotations(
"version=acid-service-annotations,spilo-role=master", annotations)) "cluster-name=acid-service-annotations,spilo-role=master", annotations))
self.assertTrue(k8s.check_service_annotations( self.assertTrue(k8s.check_service_annotations(
"version=acid-service-annotations,spilo-role=replica", annotations)) "cluster-name=acid-service-annotations,spilo-role=replica", annotations))
# clean up # clean up
unpatch_custom_service_annotations = { unpatch_custom_service_annotations = {
@ -346,14 +346,14 @@ class EndToEndTestCase(unittest.TestCase):
} }
k8s.update_config(unpatch_custom_service_annotations) k8s.update_config(unpatch_custom_service_annotations)
def assert_master_is_unique(self, namespace='default', version="acid-minimal-cluster"): def assert_master_is_unique(self, namespace='default', clusterName="acid-minimal-cluster"):
''' '''
Check that there is a single pod in the k8s cluster with the label "spilo-role=master" Check that there is a single pod in the k8s cluster with the label "spilo-role=master"
To be called manually after operations that affect pods To be called manually after operations that affect pods
''' '''
k8s = self.k8s k8s = self.k8s
labels = 'spilo-role=master,version=' + version labels = 'spilo-role=master,cluster-name=' + clusterName
num_of_master_pods = k8s.count_pods_with_label(labels, namespace) num_of_master_pods = k8s.count_pods_with_label(labels, namespace)
self.assertEqual(num_of_master_pods, 1, "Expected 1 master pod, found {}".format(num_of_master_pods)) self.assertEqual(num_of_master_pods, 1, "Expected 1 master pod, found {}".format(num_of_master_pods))
@ -438,7 +438,7 @@ class K8s:
_ = self.api.custom_objects_api.patch_namespaced_custom_object( _ = self.api.custom_objects_api.patch_namespaced_custom_object(
"acid.zalan.do", "v1", namespace, "postgresqls", "acid-minimal-cluster", body) "acid.zalan.do", "v1", namespace, "postgresqls", "acid-minimal-cluster", body)
labels = 'version=acid-minimal-cluster' labels = 'cluster-name=acid-minimal-cluster'
while self.count_pods_with_label(labels) != number_of_instances: while self.count_pods_with_label(labels) != number_of_instances:
time.sleep(self.RETRY_TIMEOUT_SEC) time.sleep(self.RETRY_TIMEOUT_SEC)
@ -448,7 +448,7 @@ class K8s:
def wait_for_master_failover(self, expected_master_nodes, namespace='default'): def wait_for_master_failover(self, expected_master_nodes, namespace='default'):
pod_phase = 'Failing over' pod_phase = 'Failing over'
new_master_node = '' new_master_node = ''
labels = 'spilo-role=master,version=acid-minimal-cluster' labels = 'spilo-role=master,cluster-name=acid-minimal-cluster'
while (pod_phase != 'Running') or (new_master_node not in expected_master_nodes): while (pod_phase != 'Running') or (new_master_node not in expected_master_nodes):
pods = self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items pods = self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items

View File

@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: postgres-operator
spec:
type: ClusterIP
ports:
- port: 8080
protocol: TCP
targetPort: 8080
selector:
name: postgres-operator

View File

@ -5,7 +5,7 @@ metadata:
# labels: # labels:
# environment: demo # environment: demo
spec: spec:
dockerImage: registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16 dockerImage: registry.opensource.zalan.do/acid/spilo-12:1.6-p2
teamId: "acid" teamId: "acid"
volume: volume:
size: 1Gi size: 1Gi

View File

@ -10,7 +10,7 @@ data:
cluster_domain: cluster.local cluster_domain: cluster.local
cluster_history_entries: "1000" cluster_history_entries: "1000"
cluster_labels: application:spilo cluster_labels: application:spilo
cluster_name_label: version cluster_name_label: cluster-name
# custom_service_annotations: "keyx:valuez,keya:valuea" # custom_service_annotations: "keyx:valuez,keya:valuea"
# custom_pod_annotations: "keya:valuea,keyb:valueb" # custom_pod_annotations: "keya:valuea,keyb:valueb"
db_hosted_zone: db.example.com db_hosted_zone: db.example.com
@ -19,7 +19,7 @@ data:
# default_cpu_request: 100m # default_cpu_request: 100m
# default_memory_limit: 500Mi # default_memory_limit: 500Mi
# default_memory_request: 100Mi # default_memory_request: 100Mi
docker_image: registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16 docker_image: registry.opensource.zalan.do/acid/spilo-12:1.6-p2
# enable_admin_role_for_users: "true" # enable_admin_role_for_users: "true"
# enable_crd_validation: "true" # enable_crd_validation: "true"
# enable_database_access: "true" # enable_database_access: "true"

View File

@ -4,3 +4,4 @@ resources:
- configmap.yaml - configmap.yaml
- operator-service-account-rbac.yaml - operator-service-account-rbac.yaml
- postgres-operator.yaml - postgres-operator.yaml
- api-service.yaml

View File

@ -64,9 +64,9 @@ rules:
- secrets - secrets
verbs: verbs:
- create - create
- update
- delete - delete
- get - get
- update
# to check nodes for node readiness label # to check nodes for node readiness label
- apiGroups: - apiGroups:
- "" - ""

View File

@ -15,7 +15,7 @@ spec:
serviceAccountName: postgres-operator serviceAccountName: postgres-operator
containers: containers:
- name: postgres-operator - name: postgres-operator
image: registry.opensource.zalan.do/acid/postgres-operator:v1.3.1 image: registry.opensource.zalan.do/acid/postgres-operator:v1.4.0
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
resources: resources:
requests: requests:

View File

@ -5,7 +5,7 @@ metadata:
configuration: configuration:
# enable_crd_validation: true # enable_crd_validation: true
etcd_host: "" etcd_host: ""
docker_image: registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16 docker_image: registry.opensource.zalan.do/acid/spilo-12:1.6-p2
# enable_shm_volume: true # enable_shm_volume: true
max_instances: -1 max_instances: -1
min_instances: -1 min_instances: -1

View File

@ -58,7 +58,7 @@ spec:
s3_secret_access_key: s3_secret_access_key:
type: string type: string
s3_force_path_style: s3_force_path_style:
type: string type: boolean
s3_wal_path: s3_wal_path:
type: string type: string
timestamp: timestamp:

View File

@ -160,7 +160,7 @@ var PostgresCRDResourceValidation = apiextv1beta1.CustomResourceValidation{
Type: "string", Type: "string",
}, },
"s3_force_path_style": { "s3_force_path_style": {
Type: "string", Type: "boolean",
}, },
"s3_wal_path": { "s3_wal_path": {
Type: "string", Type: "string",

View File

@ -1048,11 +1048,13 @@ func (c *Cluster) getNumberOfInstances(spec *acidv1.PostgresSpec) int32 {
cur := spec.NumberOfInstances cur := spec.NumberOfInstances
newcur := cur newcur := cur
/* Limit the max number of pods to one, if this is standby-cluster */
if spec.StandbyCluster != nil { if spec.StandbyCluster != nil {
c.logger.Info("Standby cluster can have maximum of 1 pod") if newcur == 1 {
min = 1 min = newcur
max = 1 max = newcur
} else {
c.logger.Warningf("operator only supports standby clusters with 1 pod")
}
} }
if max >= 0 && newcur > max { if max >= 0 && newcur > max {
newcur = max newcur = max
@ -1498,7 +1500,7 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1beta1.CronJob, error) {
) )
labels := map[string]string{ labels := map[string]string{
"version": c.Name, c.OpConfig.ClusterNameLabel: c.Name,
"application": "spilo-logical-backup", "application": "spilo-logical-backup",
} }
podAffinityTerm := v1.PodAffinityTerm{ podAffinityTerm := v1.PodAffinityTerm{

View File

@ -508,14 +508,6 @@ func (c *Controller) submitRBACCredentials(event ClusterEvent) error {
return fmt.Errorf("could not create pod service account %q : %v", c.opConfig.PodServiceAccountName, err) return fmt.Errorf("could not create pod service account %q : %v", c.opConfig.PodServiceAccountName, err)
} }
// create role only if binding references a role
// if not role is empty and we rely on an existing cluster role
if c.PodServiceAccountRole != nil {
if err := c.createRole(namespace); err != nil {
return fmt.Errorf("could not create role %q : %v", c.PodServiceAccountRole.Name, err)
}
}
if err := c.createRoleBindings(namespace); err != nil { if err := c.createRoleBindings(namespace); err != nil {
return fmt.Errorf("could not create role binding %q : %v", c.PodServiceAccountRoleBinding.Name, err) return fmt.Errorf("could not create role binding %q : %v", c.PodServiceAccountRoleBinding.Name, err)
} }
@ -525,13 +517,14 @@ func (c *Controller) submitRBACCredentials(event ClusterEvent) error {
func (c *Controller) createPodServiceAccount(namespace string) error { func (c *Controller) createPodServiceAccount(namespace string) error {
podServiceAccountName := c.opConfig.PodServiceAccountName podServiceAccountName := c.opConfig.PodServiceAccountName
// get a separate copy of service account
// to prevent a race condition when setting a namespace for many clusters
sa := *c.PodServiceAccount
_, err := c.KubeClient.ServiceAccounts(namespace).Get(podServiceAccountName, metav1.GetOptions{}) _, err := c.KubeClient.ServiceAccounts(namespace).Get(podServiceAccountName, metav1.GetOptions{})
if k8sutil.ResourceNotFound(err) { if k8sutil.ResourceNotFound(err) {
c.logger.Infof(fmt.Sprintf("creating pod service account %q in the %q namespace", podServiceAccountName, namespace)) c.logger.Infof(fmt.Sprintf("creating pod service account %q in the %q namespace", podServiceAccountName, namespace))
// get a separate copy of service account
// to prevent a race condition when setting a namespace for many clusters
sa := *c.PodServiceAccount
if _, err = c.KubeClient.ServiceAccounts(namespace).Create(&sa); err != nil { if _, err = c.KubeClient.ServiceAccounts(namespace).Create(&sa); err != nil {
return fmt.Errorf("cannot deploy the pod service account %q defined in the configuration to the %q namespace: %v", podServiceAccountName, namespace, err) return fmt.Errorf("cannot deploy the pod service account %q defined in the configuration to the %q namespace: %v", podServiceAccountName, namespace, err)
} }
@ -546,13 +539,14 @@ func (c *Controller) createPodServiceAccount(namespace string) error {
func (c *Controller) createRole(namespace string) error { func (c *Controller) createRole(namespace string) error {
podServiceAccountRoleName := c.PodServiceAccountRole.Name podServiceAccountRoleName := c.PodServiceAccountRole.Name
// get a separate copy of the role
// to prevent a race condition when setting a namespace for many clusters
role := *c.PodServiceAccountRole
_, err := c.KubeClient.Roles(namespace).Get(podServiceAccountRoleName, metav1.GetOptions{}) _, err := c.KubeClient.Roles(namespace).Get(podServiceAccountRoleName, metav1.GetOptions{})
if k8sutil.ResourceNotFound(err) { if k8sutil.ResourceNotFound(err) {
c.logger.Infof("creating role %q in the %q namespace", podServiceAccountRoleName, namespace) c.logger.Infof("creating role %q in the %q namespace", podServiceAccountRoleName, namespace)
// get a separate copy of the role
// to prevent a race condition when setting a namespace for many clusters
role := *c.PodServiceAccountRole
_, err = c.KubeClient.Roles(namespace).Create(&role) _, err = c.KubeClient.Roles(namespace).Create(&role)
if err != nil { if err != nil {
return fmt.Errorf("cannot create role %q in the %q namespace: %v", podServiceAccountRoleName, namespace, err) return fmt.Errorf("cannot create role %q in the %q namespace: %v", podServiceAccountRoleName, namespace, err)
@ -569,13 +563,14 @@ func (c *Controller) createRoleBindings(namespace string) error {
podServiceAccountName := c.opConfig.PodServiceAccountName podServiceAccountName := c.opConfig.PodServiceAccountName
podServiceAccountRoleBindingName := c.PodServiceAccountRoleBinding.Name podServiceAccountRoleBindingName := c.PodServiceAccountRoleBinding.Name
// get a separate copy of role binding
// to prevent a race condition when setting a namespace for many clusters
rb := *c.PodServiceAccountRoleBinding
_, err := c.KubeClient.RoleBindings(namespace).Get(podServiceAccountRoleBindingName, metav1.GetOptions{}) _, err := c.KubeClient.RoleBindings(namespace).Get(podServiceAccountRoleBindingName, metav1.GetOptions{})
if k8sutil.ResourceNotFound(err) { if k8sutil.ResourceNotFound(err) {
c.logger.Infof("creating the role binding %q in the %q namespace", podServiceAccountRoleBindingName, namespace) c.logger.Infof("Creating the role binding %q in the %q namespace", podServiceAccountRoleBindingName, namespace)
// get a separate copy of role binding
// to prevent a race condition when setting a namespace for many clusters
rb := *c.PodServiceAccountRoleBinding
_, err = c.KubeClient.RoleBindings(namespace).Create(&rb) _, err = c.KubeClient.RoleBindings(namespace).Create(&rb)
if err != nil { if err != nil {
return fmt.Errorf("cannot bind the pod service account %q defined in the configuration to the cluster role in the %q namespace: %v", podServiceAccountName, namespace, err) return fmt.Errorf("cannot bind the pod service account %q defined in the configuration to the cluster role in the %q namespace: %v", podServiceAccountName, namespace, err)

View File

@ -93,7 +93,7 @@ type Config struct {
WatchedNamespace string `name:"watched_namespace"` // special values: "*" means 'watch all namespaces', the empty string "" means 'watch a namespace where operator is deployed to' WatchedNamespace string `name:"watched_namespace"` // special values: "*" means 'watch all namespaces', the empty string "" means 'watch a namespace where operator is deployed to'
EtcdHost string `name:"etcd_host" default:""` // special values: the empty string "" means Patroni will use K8s as a DCS EtcdHost string `name:"etcd_host" default:""` // special values: the empty string "" means Patroni will use K8s as a DCS
DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16"` DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spilo-12:1.6-p2"`
Sidecars map[string]string `name:"sidecar_docker_images"` Sidecars map[string]string `name:"sidecar_docker_images"`
PodServiceAccountName string `name:"pod_service_account_name" default:"postgres-pod"` PodServiceAccountName string `name:"pod_service_account_name" default:"postgres-pod"`
// value of this string must be valid JSON or YAML; see initPodServiceAccount // value of this string must be valid JSON or YAML; see initPodServiceAccount

View File

@ -1,24 +1,17 @@
.PHONY: clean test appjs docker push mock .PHONY: clean test appjs docker push mock
BINARY ?= postgres-operator-ui IMAGE ?= registry.opensource.zalan.do/acid/postgres-operator-ui
BUILD_FLAGS ?= -v
CGO_ENABLED ?= 0
ifeq ($(RACE),1)
BUILD_FLAGS += -race -a
CGO_ENABLED=1
endif
LOCAL_BUILD_FLAGS ?= $(BUILD_FLAGS)
LDFLAGS ?= -X=main.version=$(VERSION)
IMAGE ?= registry.opensource.zalan.do/acid/$(BINARY)
VERSION ?= $(shell git describe --tags --always --dirty) VERSION ?= $(shell git describe --tags --always --dirty)
TAG ?= $(VERSION) TAG ?= $(VERSION)
GITHEAD = $(shell git rev-parse --short HEAD) GITHEAD = $(shell git rev-parse --short HEAD)
GITURL = $(shell git config --get remote.origin.url) GITURL = $(shell git config --get remote.origin.url)
GITSTATU = $(shell git status --porcelain || echo 'no changes') GITSTATUS = $(shell git status --porcelain || echo 'no changes')
TTYFLAGS = $(shell test -t 0 && echo '-it') TTYFLAGS = $(shell test -t 0 && echo '-it')
ifdef CDP_PULL_REQUEST_NUMBER
CDP_TAG := -${CDP_BUILD_VERSION}
endif
default: docker default: docker
clean: clean:
@ -32,11 +25,15 @@ appjs:
docker run $(TTYFLAGS) -u $$(id -u) -v $$(pwd):/workdir -w /workdir/app node:10.1.0-alpine npm run build docker run $(TTYFLAGS) -u $$(id -u) -v $$(pwd):/workdir -w /workdir/app node:10.1.0-alpine npm run build
docker: appjs docker: appjs
docker build --build-arg "VERSION=$(VERSION)" -t "$(IMAGE):$(TAG)" . echo `(env)`
@echo 'Docker image $(IMAGE):$(TAG) can now be used.' echo "Tag ${TAG}"
echo "Version ${VERSION}"
echo "CDP tag ${CDP_TAG}"
echo "git describe $(shell git describe --tags --always --dirty)"
docker build --rm -t "$(IMAGE):$(TAG)$(CDP_TAG)" -f Dockerfile .
push: docker push:
docker push "$(IMAGE):$(TAG)" docker push "$(IMAGE):$(TAG)$(CDP_TAG)"
mock: mock:
docker run -it -p 8080:8080 "$(IMAGE):$(TAG)" --mock docker run -it -p 8080:8080 "$(IMAGE):$(TAG)" --mock

View File

@ -1,6 +1,6 @@
{ {
"name": "postgres-operator-ui", "name": "postgres-operator-ui",
"version": "1.0.0", "version": "1.3.0",
"description": "PostgreSQL Operator UI", "description": "PostgreSQL Operator UI",
"main": "src/app.js", "main": "src/app.js",
"config": { "config": {

View File

@ -408,7 +408,7 @@ new
ref='cpuLimit' ref='cpuLimit'
type='number' type='number'
placeholder='{ cpu.state.limit.initialValue }' placeholder='{ cpu.state.limit.initialValue }'
min='1' min='250'
required required
value='{ cpu.state.limit.state }' value='{ cpu.state.limit.state }'
onchange='{ cpu.state.limit.edit }' onchange='{ cpu.state.limit.edit }'
@ -434,7 +434,7 @@ new
onkeyup='{ memory.state.request.edit }' onkeyup='{ memory.state.request.edit }'
) )
.input-group-addon .input-group-addon
.input-units Gi .input-units Mi
.input-group .input-group
.input-group-addon.resource-type Limit .input-group-addon.resource-type Limit
@ -442,14 +442,14 @@ new
ref='memoryLimit' ref='memoryLimit'
type='number' type='number'
placeholder='{ memory.state.limit.initialValue }' placeholder='{ memory.state.limit.initialValue }'
min='1' min='250'
required required
value='{ memory.state.limit.state }' value='{ memory.state.limit.state }'
onchange='{ memory.state.limit.edit }' onchange='{ memory.state.limit.edit }'
onkeyup='{ memory.state.limit.edit }' onkeyup='{ memory.state.limit.edit }'
) )
.input-group-addon .input-group-addon
.input-units Gi .input-units Mi
.col-lg-3 .col-lg-3
help-general(config='{ opts.config }') help-general(config='{ opts.config }')
@ -519,10 +519,10 @@ new
resources: resources:
requests: requests:
cpu: {{ cpu.state.request.state }}m cpu: {{ cpu.state.request.state }}m
memory: {{ memory.state.request.state }}Gi memory: {{ memory.state.request.state }}Mi
limits: limits:
cpu: {{ cpu.state.limit.state }}m cpu: {{ cpu.state.limit.state }}m
memory: {{ memory.state.limit.state }}Gi{{#if restoring}} memory: {{ memory.state.limit.state }}Mi{{#if restoring}}
clone: clone:
cluster: "{{ backup.state.name.state }}" cluster: "{{ backup.state.name.state }}"
@ -786,8 +786,8 @@ new
return instance return instance
} }
this.cpu = DynamicResource({ request: 100, limit: 1000 }) this.cpu = DynamicResource({ request: 100, limit: 500 })
this.memory = DynamicResource({ request: 1, limit: 1 }) this.memory = DynamicResource({ request: 100, limit: 500 })
this.backup = DynamicSet({ this.backup = DynamicSet({
type: () => 'empty', type: () => 'empty',

View File

@ -76,6 +76,9 @@ postgresql
.alert.alert-danger(if='{ progress.requestStatus !== "OK" }') Create request failed .alert.alert-danger(if='{ progress.requestStatus !== "OK" }') Create request failed
.alert.alert-success(if='{ progress.requestStatus === "OK" }') Create request successful ({ new Date(progress.createdTimestamp).toLocaleString() }) .alert.alert-success(if='{ progress.requestStatus === "OK" }') Create request successful ({ new Date(progress.createdTimestamp).toLocaleString() })
.alert.alert-info(if='{ !progress.postgresql }') PostgreSQL cluster manifest pending
.alert.alert-success(if='{ progress.postgresql }') PostgreSQL cluster manifest created
.alert.alert-info(if='{ !progress.statefulSet }') StatefulSet pending .alert.alert-info(if='{ !progress.statefulSet }') StatefulSet pending
.alert.alert-success(if='{ progress.statefulSet }') StatefulSet created .alert.alert-success(if='{ progress.statefulSet }') StatefulSet created

View File

@ -45,12 +45,14 @@ postgresqls
thead thead
tr tr
th(style='width: 120px') Team th(style='width: 120px') Team
th(style='width: 130px') Namespace
th Name
th(style='width: 50px') Pods th(style='width: 50px') Pods
th(style='width: 140px') CPU th(style='width: 140px') CPU
th(style='width: 130px') Memory th(style='width: 130px') Memory
th(style='width: 100px') Size th(style='width: 100px') Size
th(style='width: 130px') Namespace th(style='width: 120px') Cost/Month
th Name th(stlye='width: 120px')
tbody tbody
tr( tr(
@ -58,19 +60,21 @@ postgresqls
hidden='{ !namespaced_name.toLowerCase().includes(filter.state.toLowerCase()) }' hidden='{ !namespaced_name.toLowerCase().includes(filter.state.toLowerCase()) }'
) )
td { team } td { team }
td { nodes }
td { cpu } / { cpu_limit }
td { memory } / { memory_limit }
td { volume_size }
td(style='white-space: pre') td(style='white-space: pre')
| { namespace } | { namespace }
td td
a( a(
href='/#/status/{ cluster_path(this) }' href='/#/status/{ cluster_path(this) }'
) )
| { name } | { name }
td { nodes }
td { cpu } / { cpu_limit }
td { memory } / { memory_limit }
td { volume_size }
td { calcCosts(nodes, cpu, memory, volume_size) }$
td
.btn-group.pull-right( .btn-group.pull-right(
aria-label='Cluster { qname } actions' aria-label='Cluster { qname } actions'
@ -124,12 +128,14 @@ postgresqls
thead thead
tr tr
th(style='width: 120px') Team th(style='width: 120px') Team
th(style='width: 130px') Namespace
th Name
th(style='width: 50px') Pods th(style='width: 50px') Pods
th(style='width: 140px') CPU th(style='width: 140px') CPU
th(style='width: 130px') Memory th(style='width: 130px') Memory
th(style='width: 100px') Size th(style='width: 100px') Size
th(style='width: 130px') Namespace th(style='width: 120px') Cost/Month
th Name th(stlye='width: 120px')
tbody tbody
tr( tr(
@ -137,20 +143,20 @@ postgresqls
hidden='{ !namespaced_name.toLowerCase().includes(filter.state.toLowerCase()) }' hidden='{ !namespaced_name.toLowerCase().includes(filter.state.toLowerCase()) }'
) )
td { team } td { team }
td { nodes }
td { cpu } / { cpu_limit }
td { memory } / { memory_limit }
td { volume_size }
td(style='white-space: pre') td(style='white-space: pre')
| { namespace } | { namespace }
td td
a( a(
href='/#/status/{ cluster_path(this) }' href='/#/status/{ cluster_path(this) }'
) )
| { name } | { name }
td { nodes }
td { cpu } / { cpu_limit }
td { memory } / { memory_limit }
td { volume_size }
td { calcCosts(nodes, cpu, memory, volume_size) }$
td
.btn-group.pull-right( .btn-group.pull-right(
aria-label='Cluster { qname } actions' aria-label='Cluster { qname } actions'
@ -223,6 +229,45 @@ postgresqls
+ '/' + encodeURI(cluster.name) + '/' + encodeURI(cluster.name)
) )
const calcCosts = this.calcCosts = (nodes, cpu, memory, disk) => {
costs = nodes * (toCores(cpu) * opts.config.cost_core + toMemory(memory) * opts.config.cost_memory + toDisk(disk) * opts.config.cost_ebs)
return costs.toFixed(2)
}
const toDisk = this.toDisk = value => {
if(value.endsWith("Gi")) {
value = value.substring(0, value.length-2)
value = Number(value)
return value
}
return value
}
const toMemory = this.toMemory = value => {
if (value.endsWith("Mi")) {
value = value.substring(0, value.length-2)
value = Number(value) / 1000.
return value
}
else if(value.endsWith("Gi")) {
value = value.substring(0, value.length-2)
value = Number(value)
return value
}
return value
}
const toCores = this.toCores = value => {
if (value.endsWith("m")) {
value = value.substring(0, value.length-1)
value = Number(value) / 1000.
return value
}
return value
}
this.on('mount', () => this.on('mount', () =>
jQuery jQuery
.get('/postgresqls') .get('/postgresqls')

View File

@ -4,23 +4,23 @@ metadata:
name: "postgres-operator-ui" name: "postgres-operator-ui"
namespace: "default" namespace: "default"
labels: labels:
application: "postgres-operator-ui" name: "postgres-operator-ui"
team: "acid" team: "acid"
spec: spec:
replicas: 1 replicas: 1
selector: selector:
matchLabels: matchLabels:
application: "postgres-operator-ui" name: "postgres-operator-ui"
template: template:
metadata: metadata:
labels: labels:
application: "postgres-operator-ui" name: "postgres-operator-ui"
team: "acid" team: "acid"
spec: spec:
serviceAccountName: postgres-operator-ui serviceAccountName: postgres-operator-ui
containers: containers:
- name: "service" - name: "service"
image: registry.opensource.zalan.do/acid/postgres-operator-ui:v1.2.0 image: registry.opensource.zalan.do/acid/postgres-operator-ui:v1.3.0
ports: ports:
- containerPort: 8081 - containerPort: 8081
protocol: "TCP" protocol: "TCP"
@ -32,8 +32,8 @@ spec:
timeoutSeconds: 1 timeoutSeconds: 1
resources: resources:
limits: limits:
cpu: "300m" cpu: "200m"
memory: "3000Mi" memory: "200Mi"
requests: requests:
cpu: "100m" cpu: "100m"
memory: "100Mi" memory: "100Mi"
@ -41,7 +41,9 @@ spec:
- name: "APP_URL" - name: "APP_URL"
value: "http://localhost:8081" value: "http://localhost:8081"
- name: "OPERATOR_API_URL" - name: "OPERATOR_API_URL"
value: "http://localhost:8080" value: "http://postgres-operator:8080"
- name: "OPERATOR_CLUSTER_NAME_LABEL"
value: "cluster-name"
- name: "TARGET_NAMESPACE" - name: "TARGET_NAMESPACE"
value: "default" value: "default"
- name: "TEAMS" - name: "TEAMS"
@ -60,9 +62,14 @@ spec:
"replica_load_balancer_visible": true, "replica_load_balancer_visible": true,
"resources_visible": true, "resources_visible": true,
"users_visible": true, "users_visible": true,
"cost_ebs": 0.119,
"cost_core": 0.0575,
"cost_memory": 0.014375,
"postgresql_versions": [ "postgresql_versions": [
"12",
"11", "11",
"10", "10",
"9.6" "9.6",
"9.5"
] ]
} }

View File

@ -76,6 +76,7 @@ ACCESS_TOKEN_URL = getenv('ACCESS_TOKEN_URL')
TOKENINFO_URL = getenv('OAUTH2_TOKEN_INFO_URL') TOKENINFO_URL = getenv('OAUTH2_TOKEN_INFO_URL')
OPERATOR_API_URL = getenv('OPERATOR_API_URL', 'http://postgres-operator') OPERATOR_API_URL = getenv('OPERATOR_API_URL', 'http://postgres-operator')
OPERATOR_CLUSTER_NAME_LABEL = getenv('OPERATOR_CLUSTER_NAME_LABEL', 'cluster-name')
OPERATOR_UI_CONFIG = getenv('OPERATOR_UI_CONFIG', '{}') OPERATOR_UI_CONFIG = getenv('OPERATOR_UI_CONFIG', '{}')
OPERATOR_UI_MAINTENANCE_CHECK = getenv('OPERATOR_UI_MAINTENANCE_CHECK', '{}') OPERATOR_UI_MAINTENANCE_CHECK = getenv('OPERATOR_UI_MAINTENANCE_CHECK', '{}')
READ_ONLY_MODE = getenv('READ_ONLY_MODE', False) in [True, 'true'] READ_ONLY_MODE = getenv('READ_ONLY_MODE', False) in [True, 'true']
@ -84,6 +85,13 @@ SUPERUSER_TEAM = getenv('SUPERUSER_TEAM', 'acid')
TARGET_NAMESPACE = getenv('TARGET_NAMESPACE') TARGET_NAMESPACE = getenv('TARGET_NAMESPACE')
GOOGLE_ANALYTICS = getenv('GOOGLE_ANALYTICS', False) GOOGLE_ANALYTICS = getenv('GOOGLE_ANALYTICS', False)
# storage pricing, i.e. https://aws.amazon.com/ebs/pricing/
COST_EBS = float(getenv('COST_EBS', 0.119)) # GB per month
# compute costs, i.e. https://www.ec2instances.info/?region=eu-central-1&selected=m5.2xlarge
COST_CORE = 30.5 * 24 * float(getenv('COST_CORE', 0.0575)) # Core per hour m5.2xlarge / 8.
COST_MEMORY = 30.5 * 24 * float(getenv('COST_MEMORY', 0.014375)) # Memory GB m5.2xlarge / 32.
WALE_S3_ENDPOINT = getenv( WALE_S3_ENDPOINT = getenv(
'WALE_S3_ENDPOINT', 'WALE_S3_ENDPOINT',
'https+path://s3-eu-central-1.amazonaws.com:443', 'https+path://s3-eu-central-1.amazonaws.com:443',
@ -293,6 +301,9 @@ DEFAULT_UI_CONFIG = {
'dns_format_string': '{0}.{1}.{2}', 'dns_format_string': '{0}.{1}.{2}',
'pgui_link': '', 'pgui_link': '',
'static_network_whitelist': {}, 'static_network_whitelist': {},
'cost_ebs': COST_EBS,
'cost_core': COST_CORE,
'cost_memory': COST_MEMORY
} }
@ -1003,6 +1014,7 @@ def main(port, secret_key, debug, clusters: list):
logger.info(f'App URL: {APP_URL}') logger.info(f'App URL: {APP_URL}')
logger.info(f'Authorize URL: {AUTHORIZE_URL}') logger.info(f'Authorize URL: {AUTHORIZE_URL}')
logger.info(f'Operator API URL: {OPERATOR_API_URL}') logger.info(f'Operator API URL: {OPERATOR_API_URL}')
logger.info(f'Operator cluster name label: {OPERATOR_CLUSTER_NAME_LABEL}')
logger.info(f'Readonly mode: {"enabled" if READ_ONLY_MODE else "disabled"}') # noqa logger.info(f'Readonly mode: {"enabled" if READ_ONLY_MODE else "disabled"}') # noqa
logger.info(f'Spilo S3 backup bucket: {SPILO_S3_BACKUP_BUCKET}') logger.info(f'Spilo S3 backup bucket: {SPILO_S3_BACKUP_BUCKET}')
logger.info(f'Spilo S3 backup prefix: {SPILO_S3_BACKUP_PREFIX}') logger.info(f'Spilo S3 backup prefix: {SPILO_S3_BACKUP_PREFIX}')

View File

@ -3,7 +3,7 @@ from datetime import datetime, timezone
from furl import furl from furl import furl
from json import dumps from json import dumps
from logging import getLogger from logging import getLogger
from os import environ from os import environ, getenv
from requests import Session from requests import Session
from urllib.parse import urljoin from urllib.parse import urljoin
from uuid import UUID from uuid import UUID
@ -16,6 +16,8 @@ logger = getLogger(__name__)
session = Session() session = Session()
OPERATOR_CLUSTER_NAME_LABEL = getenv('OPERATOR_CLUSTER_NAME_LABEL', 'cluster-name')
def request(cluster, path, **kwargs): def request(cluster, path, **kwargs):
if 'timeout' not in kwargs: if 'timeout' not in kwargs:
@ -137,7 +139,7 @@ def read_pods(cluster, namespace, spilo_cluster):
cluster=cluster, cluster=cluster,
resource_type='pods', resource_type='pods',
namespace=namespace, namespace=namespace,
label_selector={'version': spilo_cluster}, label_selector={OPERATOR_CLUSTER_NAME_LABEL: spilo_cluster},
) )

View File

@ -1,14 +1,15 @@
Flask-OAuthlib==0.9.5 Flask-OAuthlib==0.9.5
Flask==1.0.2 Flask==1.1.1
backoff==1.5.0 backoff==1.8.1
boto3==1.5.14 boto3==1.10.4
boto==2.48.0 boto==2.49.0
click==6.7 click==6.7
furl==1.0.1 furl==1.0.2
gevent==1.2.2 gevent==1.2.2
jq==0.1.6 jq==0.1.6
json_delta>=2.0 json_delta>=2.0
kubernetes==3.0.0 kubernetes==3.0.0
requests==2.20.1 requests==2.22.0
stups-tokens>=1.1.19 stups-tokens>=1.1.19
wal_e==1.1.0 wal_e==1.1.0
werkzeug==0.16.1

View File

@ -19,10 +19,15 @@ default_operator_ui_config='{
"nat_gateways_visible": false, "nat_gateways_visible": false,
"resources_visible": true, "resources_visible": true,
"users_visible": true, "users_visible": true,
"cost_ebs": 0.119,
"cost_core": 0.0575,
"cost_memory": 0.014375,
"postgresql_versions": [ "postgresql_versions": [
"12",
"11", "11",
"10", "10",
"9.6" "9.6",
"9.5"
], ],
"static_network_whitelist": { "static_network_whitelist": {
"localhost": ["172.0.0.1/32"] "localhost": ["172.0.0.1/32"]