Merge branch 'master' into bump-v1.5.0

This commit is contained in:
Felix Kunde 2020-04-30 17:29:08 +02:00
commit af69afcbfe
15 changed files with 127 additions and 17 deletions

View File

@ -49,6 +49,11 @@ rules:
- events - events
verbs: verbs:
- create - create
- get
- list
- patch
- update
- watch
# to manage endpoints which are also used by Patroni # to manage endpoints which are also used by Patroni
- apiGroups: - apiGroups:
- "" - ""

View File

@ -53,8 +53,19 @@ them.
## Watch pods being created ## Watch pods being created
Check if the database pods are coming up. Use the label `application=spilo` to
filter and list the label `spilo-role` to see when the master is promoted and
replicas get their labels.
```bash ```bash
kubectl get pods -w --show-labels kubectl get pods -l application=spilo -L spilo-role -w
```
The operator also emits K8s events to the Postgresql CRD which can be inspected
in the operator logs or with:
```bash
kubectl describe postgresql acid-minimal-cluster
``` ```
## Connect to PostgreSQL ## Connect to PostgreSQL
@ -736,14 +747,14 @@ spin up more instances).
## Custom TLS certificates ## Custom TLS certificates
By default, the spilo image generates its own TLS certificate during startup. By default, the Spilo image generates its own TLS certificate during startup.
However, this certificate cannot be verified and thus doesn't protect from However, this certificate cannot be verified and thus doesn't protect from
active MITM attacks. In this section we show how to specify a custom TLS active MITM attacks. In this section we show how to specify a custom TLS
certificate which is mounted in the database pods via a K8s Secret. certificate which is mounted in the database pods via a K8s Secret.
Before applying these changes, in k8s the operator must also be configured with Before applying these changes, in k8s the operator must also be configured with
the `spilo_fsgroup` set to the GID matching the postgres user group. If you the `spilo_fsgroup` set to the GID matching the postgres user group. If you
don't know the value, use `103` which is the GID from the default spilo image don't know the value, use `103` which is the GID from the default Spilo image
(`spilo_fsgroup=103` in the cluster request spec). (`spilo_fsgroup=103` in the cluster request spec).
OpenShift allocates the users and groups dynamically (based on scc), and their OpenShift allocates the users and groups dynamically (based on scc), and their
@ -805,5 +816,5 @@ spec:
Alternatively, it is also possible to use Alternatively, it is also possible to use
[cert-manager](https://cert-manager.io/docs/) to generate these secrets. [cert-manager](https://cert-manager.io/docs/) to generate these secrets.
Certificate rotation is handled in the spilo image which checks every 5 Certificate rotation is handled in the Spilo image which checks every 5
minutes if the certificates have changed and reloads postgres accordingly. minutes if the certificates have changed and reloads postgres accordingly.

View File

@ -50,6 +50,11 @@ rules:
- events - events
verbs: verbs:
- create - create
- get
- list
- patch
- update
- watch
# to manage endpoints which are also used by Patroni # to manage endpoints which are also used by Patroni
- apiGroups: - apiGroups:
- "" - ""

View File

@ -76,7 +76,7 @@ func NewController(controllerConfig *spec.ControllerConfig, controllerId string)
} }
eventBroadcaster := record.NewBroadcaster() eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartLogging(logger.Debugf) eventBroadcaster.StartLogging(logger.Infof)
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: myComponentName}) recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: myComponentName})
c := &Controller{ c := &Controller{

View File

@ -1,7 +1,7 @@
FROM alpine:3.6 FROM alpine:3.6
MAINTAINER team-acid@zalando.de MAINTAINER team-acid@zalando.de
EXPOSE 8080 EXPOSE 8081
RUN \ RUN \
apk add --no-cache \ apk add --no-cache \
@ -29,6 +29,7 @@ RUN \
/var/cache/apk/* /var/cache/apk/*
COPY requirements.txt / COPY requirements.txt /
COPY start_server.sh /
RUN pip3 install -r /requirements.txt RUN pip3 install -r /requirements.txt
COPY operator_ui /operator_ui COPY operator_ui /operator_ui
@ -37,4 +38,4 @@ ARG VERSION=dev
RUN sed -i "s/__version__ = .*/__version__ = '${VERSION}'/" /operator_ui/__init__.py RUN sed -i "s/__version__ = .*/__version__ = '${VERSION}'/" /operator_ui/__init__.py
WORKDIR / WORKDIR /
ENTRYPOINT ["/usr/bin/python3", "-m", "operator_ui"] CMD ["/usr/bin/python3", "-m", "operator_ui"]

View File

@ -36,4 +36,4 @@ push:
docker push "$(IMAGE):$(TAG)$(CDP_TAG)" docker push "$(IMAGE):$(TAG)$(CDP_TAG)"
mock: mock:
docker run -it -p 8080:8080 "$(IMAGE):$(TAG)" --mock docker run -it -p 8081:8081 "$(IMAGE):$(TAG)" --mock

View File

@ -137,6 +137,7 @@ edit
o.spec.numberOfInstances = i.spec.numberOfInstances o.spec.numberOfInstances = i.spec.numberOfInstances
o.spec.enableMasterLoadBalancer = i.spec.enableMasterLoadBalancer || false o.spec.enableMasterLoadBalancer = i.spec.enableMasterLoadBalancer || false
o.spec.enableReplicaLoadBalancer = i.spec.enableReplicaLoadBalancer || false o.spec.enableReplicaLoadBalancer = i.spec.enableReplicaLoadBalancer || false
o.spec.enableConnectionPooler = i.spec.enableConnectionPooler || false
o.spec.volume = { size: i.spec.volume.size } o.spec.volume = { size: i.spec.volume.size }
if ('users' in i.spec && typeof i.spec.users === 'object') { if ('users' in i.spec && typeof i.spec.users === 'object') {

View File

@ -239,6 +239,18 @@ new
| |
| Enable replica ELB | Enable replica ELB
tr
td Enable Connection Pool
td
label
input(
type='checkbox'
value='{ enableConnectionPooler }'
onchange='{ toggleEnableConnectionPooler }'
)
|
| Enable Connection Pool (using PGBouncer)
tr tr
td Volume size td Volume size
td td
@ -493,6 +505,9 @@ new
{{#if enableReplicaLoadBalancer}} {{#if enableReplicaLoadBalancer}}
enableReplicaLoadBalancer: true enableReplicaLoadBalancer: true
{{/if}} {{/if}}
{{#if enableConnectionPooler}}
enableConnectionPooler: true
{{/if}}
volume: volume:
size: "{{ volumeSize }}Gi" size: "{{ volumeSize }}Gi"
{{#if users}} {{#if users}}
@ -516,13 +531,14 @@ new
- {{ odd }}/32 - {{ odd }}/32
{{/if}} {{/if}}
{{#if resourcesVisible}}
resources: resources:
requests: requests:
cpu: {{ cpu.state.request.state }}m cpu: {{ cpu.state.request.state }}m
memory: {{ memory.state.request.state }}Mi 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 }}Mi{{#if restoring}} memory: {{ memory.state.limit.state }}Mi{{/if}}{{#if restoring}}
clone: clone:
cluster: "{{ backup.state.name.state }}" cluster: "{{ backup.state.name.state }}"
@ -542,6 +558,7 @@ new
instanceCount: this.instanceCount, instanceCount: this.instanceCount,
enableMasterLoadBalancer: this.enableMasterLoadBalancer, enableMasterLoadBalancer: this.enableMasterLoadBalancer,
enableReplicaLoadBalancer: this.enableReplicaLoadBalancer, enableReplicaLoadBalancer: this.enableReplicaLoadBalancer,
enableConnectionPooler: this.enableConnectionPooler,
volumeSize: this.volumeSize, volumeSize: this.volumeSize,
users: this.users.valids, users: this.users.valids,
databases: this.databases.valids, databases: this.databases.valids,
@ -552,6 +569,7 @@ new
memory: this.memory, memory: this.memory,
backup: this.backup, backup: this.backup,
namespace: this.namespace, namespace: this.namespace,
resourcesVisible: this.config.resources_visible,
restoring: this.backup.state.type.state !== 'empty', restoring: this.backup.state.type.state !== 'empty',
pitr: this.backup.state.type.state === 'pitr', pitr: this.backup.state.type.state === 'pitr',
} }
@ -598,6 +616,10 @@ new
this.enableReplicaLoadBalancer = !this.enableReplicaLoadBalancer this.enableReplicaLoadBalancer = !this.enableReplicaLoadBalancer
} }
this.toggleEnableConnectionPooler = e => {
this.enableConnectionPooler = !this.enableConnectionPooler
}
this.volumeChange = e => { this.volumeChange = e => {
this.volumeSize = +e.target.value this.volumeSize = +e.target.value
} }
@ -892,6 +914,7 @@ new
this.odd = '' this.odd = ''
this.enableMasterLoadBalancer = false this.enableMasterLoadBalancer = false
this.enableReplicaLoadBalancer = false this.enableReplicaLoadBalancer = false
this.enableConnectionPooler = false
this.postgresqlVersion = this.postgresqlVersion = ( this.postgresqlVersion = this.postgresqlVersion = (
this.config.postgresql_versions[0] this.config.postgresql_versions[0]

View File

@ -92,6 +92,8 @@ postgresql
.alert.alert-success(if='{ progress.masterLabel }') PostgreSQL master available, label is attached .alert.alert-success(if='{ progress.masterLabel }') PostgreSQL master available, label is attached
.alert.alert-success(if='{ progress.masterLabel && progress.dnsName }') PostgreSQL ready: <strong>{ progress.dnsName }</strong> .alert.alert-success(if='{ progress.masterLabel && progress.dnsName }') PostgreSQL ready: <strong>{ progress.dnsName }</strong>
.alert.alert-success(if='{ progress.pooler }') Connection pooler deployment created
.col-lg-3 .col-lg-3
help-general(config='{ opts.config }') help-general(config='{ opts.config }')
@ -122,9 +124,11 @@ postgresql
jQuery.get( jQuery.get(
'/postgresqls/' + this.cluster_path, '/postgresqls/' + this.cluster_path,
).done(data => { ).done(data => {
this.progress.pooler = false
this.progress.postgresql = true this.progress.postgresql = true
this.progress.postgresqlManifest = data this.progress.postgresqlManifest = data
this.progress.createdTimestamp = data.metadata.creationTimestamp this.progress.createdTimestamp = data.metadata.creationTimestamp
this.progress.poolerEnabled = data.spec.enableConnectionPooler
this.uid = this.progress.postgresqlManifest.metadata.uid this.uid = this.progress.postgresqlManifest.metadata.uid
this.update() this.update()
@ -160,6 +164,11 @@ postgresql
this.progress.dnsName = data.metadata.name + '.' + data.metadata.namespace this.progress.dnsName = data.metadata.name + '.' + data.metadata.namespace
} }
jQuery.get('/pooler/' + this.cluster_path).done(data => {
this.progress.pooler = {"url": ""}
this.update()
})
this.update() this.update()
}) })
}) })

View File

@ -44,6 +44,8 @@ spec:
value: "http://postgres-operator:8080" value: "http://postgres-operator:8080"
- name: "OPERATOR_CLUSTER_NAME_LABEL" - name: "OPERATOR_CLUSTER_NAME_LABEL"
value: "cluster-name" value: "cluster-name"
- name: "RESOURCES_VISIBLE"
value: "False"
- name: "TARGET_NAMESPACE" - name: "TARGET_NAMESPACE"
value: "default" value: "default"
- name: "TEAMS" - name: "TEAMS"

View File

@ -39,6 +39,7 @@ rules:
- apiGroups: - apiGroups:
- apps - apps
resources: resources:
- deployments
- statefulsets - statefulsets
verbs: verbs:
- get - get

View File

@ -25,7 +25,7 @@ from flask import (
from flask_oauthlib.client import OAuth from flask_oauthlib.client import OAuth
from functools import wraps from functools import wraps
from gevent import sleep, spawn from gevent import sleep, spawn
from gevent.wsgi import WSGIServer from gevent.pywsgi import WSGIServer
from jq import jq from jq import jq
from json import dumps, loads from json import dumps, loads
from logging import DEBUG, ERROR, INFO, basicConfig, exception, getLogger from logging import DEBUG, ERROR, INFO, basicConfig, exception, getLogger
@ -44,6 +44,7 @@ from .spiloutils import (
create_postgresql, create_postgresql,
read_basebackups, read_basebackups,
read_namespaces, read_namespaces,
read_pooler,
read_pods, read_pods,
read_postgresql, read_postgresql,
read_postgresqls, read_postgresqls,
@ -80,6 +81,7 @@ OPERATOR_CLUSTER_NAME_LABEL = getenv('OPERATOR_CLUSTER_NAME_LABEL', 'cluster-nam
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']
RESOURCES_VISIBLE = getenv('RESOURCES_VISIBLE', True)
SPILO_S3_BACKUP_PREFIX = getenv('SPILO_S3_BACKUP_PREFIX', 'spilo/') SPILO_S3_BACKUP_PREFIX = getenv('SPILO_S3_BACKUP_PREFIX', 'spilo/')
SUPERUSER_TEAM = getenv('SUPERUSER_TEAM', 'acid') SUPERUSER_TEAM = getenv('SUPERUSER_TEAM', 'acid')
TARGET_NAMESPACE = getenv('TARGET_NAMESPACE') TARGET_NAMESPACE = getenv('TARGET_NAMESPACE')
@ -312,6 +314,7 @@ DEFAULT_UI_CONFIG = {
def get_config(): def get_config():
config = loads(OPERATOR_UI_CONFIG) or DEFAULT_UI_CONFIG config = loads(OPERATOR_UI_CONFIG) or DEFAULT_UI_CONFIG
config['read_only_mode'] = READ_ONLY_MODE config['read_only_mode'] = READ_ONLY_MODE
config['resources_visible'] = RESOURCES_VISIBLE
config['superuser_team'] = SUPERUSER_TEAM config['superuser_team'] = SUPERUSER_TEAM
config['target_namespace'] = TARGET_NAMESPACE config['target_namespace'] = TARGET_NAMESPACE
@ -397,6 +400,22 @@ def get_service(namespace: str, cluster: str):
) )
@app.route('/pooler/<namespace>/<cluster>')
@authorize
def get_list_poolers(namespace: str, cluster: str):
if TARGET_NAMESPACE not in ['', '*', namespace]:
return wrong_namespace()
return respond(
read_pooler(
get_cluster(),
namespace,
"{}-pooler".format(cluster),
),
)
@app.route('/statefulsets/<namespace>/<cluster>') @app.route('/statefulsets/<namespace>/<cluster>')
@authorize @authorize
def get_list_clusters(namespace: str, cluster: str): def get_list_clusters(namespace: str, cluster: str):
@ -587,6 +606,17 @@ def update_postgresql(namespace: str, cluster: str):
spec['volume'] = {'size': size} spec['volume'] = {'size': size}
if 'enableConnectionPooler' in postgresql['spec']:
cp = postgresql['spec']['enableConnectionPooler']
if not cp:
if 'enableConnectionPooler' in o['spec']:
del o['spec']['enableConnectionPooler']
else:
spec['enableConnectionPooler'] = True
else:
if 'enableConnectionPooler' in o['spec']:
del o['spec']['enableConnectionPooler']
if 'enableReplicaLoadBalancer' in postgresql['spec']: if 'enableReplicaLoadBalancer' in postgresql['spec']:
rlb = postgresql['spec']['enableReplicaLoadBalancer'] rlb = postgresql['spec']['enableReplicaLoadBalancer']
if not rlb: if not rlb:
@ -1006,7 +1036,7 @@ def init_cluster():
def main(port, secret_key, debug, clusters: list): def main(port, secret_key, debug, clusters: list):
global TARGET_NAMESPACE global TARGET_NAMESPACE
basicConfig(level=DEBUG if debug else INFO) basicConfig(stream=sys.stdout, level=(DEBUG if debug else INFO), format='%(asctime)s %(levelname)s: %(message)s',)
init_cluster() init_cluster()

View File

@ -1,7 +1,7 @@
from boto3 import client from boto3 import client
from datetime import datetime, timezone from datetime import datetime, timezone
from furl import furl from furl import furl
from json import dumps from json import dumps, loads
from logging import getLogger from logging import getLogger
from os import environ, getenv from os import environ, getenv
from requests import Session from requests import Session
@ -18,6 +18,15 @@ session = Session()
OPERATOR_CLUSTER_NAME_LABEL = getenv('OPERATOR_CLUSTER_NAME_LABEL', 'cluster-name') OPERATOR_CLUSTER_NAME_LABEL = getenv('OPERATOR_CLUSTER_NAME_LABEL', 'cluster-name')
COMMON_CLUSTER_LABEL = getenv('COMMON_CLUSTER_LABEL', '{"application":"spilo"}')
COMMON_POOLER_LABEL = getenv('COMMONG_POOLER_LABEL', '{"application":"db-connection-pooler"}')
logger.info("Common Cluster Label: {}".format(COMMON_CLUSTER_LABEL))
logger.info("Common Pooler Label: {}".format(COMMON_POOLER_LABEL))
COMMON_CLUSTER_LABEL = loads(COMMON_CLUSTER_LABEL)
COMMON_POOLER_LABEL = loads(COMMON_POOLER_LABEL)
def request(cluster, path, **kwargs): def request(cluster, path, **kwargs):
if 'timeout' not in kwargs: if 'timeout' not in kwargs:
@ -85,6 +94,7 @@ def resource_api_version(resource_type):
return { return {
'postgresqls': 'apis/acid.zalan.do/v1', 'postgresqls': 'apis/acid.zalan.do/v1',
'statefulsets': 'apis/apps/v1', 'statefulsets': 'apis/apps/v1',
'deployments': 'apis/apps/v1',
}.get(resource_type, 'api/v1') }.get(resource_type, 'api/v1')
@ -149,7 +159,7 @@ def read_pod(cluster, namespace, resource_name):
resource_type='pods', resource_type='pods',
namespace=namespace, namespace=namespace,
resource_name=resource_name, resource_name=resource_name,
label_selector={'application': 'spilo'}, label_selector=COMMON_CLUSTER_LABEL,
) )
@ -159,7 +169,17 @@ def read_service(cluster, namespace, resource_name):
resource_type='services', resource_type='services',
namespace=namespace, namespace=namespace,
resource_name=resource_name, resource_name=resource_name,
label_selector={'application': 'spilo'}, label_selector=COMMON_CLUSTER_LABEL,
)
def read_pooler(cluster, namespace, resource_name):
return kubernetes_get(
cluster=cluster,
resource_type='deployments',
namespace=namespace,
resource_name=resource_name,
label_selector=COMMON_POOLER_LABEL,
) )
@ -169,7 +189,7 @@ def read_statefulset(cluster, namespace, resource_name):
resource_type='statefulsets', resource_type='statefulsets',
namespace=namespace, namespace=namespace,
resource_name=resource_name, resource_name=resource_name,
label_selector={'application': 'spilo'}, label_selector=COMMON_CLUSTER_LABEL,
) )
@ -302,7 +322,7 @@ def read_basebackups(
f=configure_backup_cxt, f=configure_backup_cxt,
aws_instance_profile=use_aws_instance_profile, aws_instance_profile=use_aws_instance_profile,
s3_prefix=f's3://{bucket}/{prefix}{pg_cluster}{suffix}/wal/', s3_prefix=f's3://{bucket}/{prefix}{pg_cluster}{suffix}/wal/',
)._backup_list(detail=True) )._backup_list(detail=True)._backup_list(prefix=f"{prefix}{pg_cluster}{suffix}/wal/")
] ]

View File

@ -1,5 +1,5 @@
Flask-OAuthlib==0.9.5 Flask-OAuthlib==0.9.5
Flask==1.1.1 Flask==1.1.2
backoff==1.8.1 backoff==1.8.1
boto3==1.10.4 boto3==1.10.4
boto==2.49.0 boto==2.49.0

2
ui/start_server.sh Normal file
View File

@ -0,0 +1,2 @@
#!/bin/bash
/usr/bin/python3 -m operator_ui