Merge branch 'master' into bump-v1.5.0
This commit is contained in:
		
						commit
						af69afcbfe
					
				|  | @ -49,6 +49,11 @@ rules: | |||
|   - events | ||||
|   verbs: | ||||
|   - create | ||||
|   - get | ||||
|   - list | ||||
|   - patch | ||||
|   - update | ||||
|   - watch | ||||
| # to manage endpoints which are also used by Patroni | ||||
| - apiGroups: | ||||
|   - "" | ||||
|  |  | |||
							
								
								
									
										19
									
								
								docs/user.md
								
								
								
								
							
							
						
						
									
										19
									
								
								docs/user.md
								
								
								
								
							|  | @ -53,8 +53,19 @@ them. | |||
| 
 | ||||
| ## 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 | ||||
| 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 | ||||
|  | @ -736,14 +747,14 @@ spin up more instances). | |||
| 
 | ||||
| ## 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 | ||||
| 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. | ||||
| 
 | ||||
| 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 | ||||
| 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). | ||||
| 
 | ||||
| OpenShift allocates the users and groups dynamically (based on scc), and their | ||||
|  | @ -805,5 +816,5 @@ spec: | |||
| Alternatively, it is also possible to use | ||||
| [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. | ||||
|  |  | |||
|  | @ -50,6 +50,11 @@ rules: | |||
|   - events | ||||
|   verbs: | ||||
|   - create | ||||
|   - get | ||||
|   - list | ||||
|   - patch | ||||
|   - update | ||||
|   - watch | ||||
| # to manage endpoints which are also used by Patroni | ||||
| - apiGroups: | ||||
|   - "" | ||||
|  |  | |||
|  | @ -76,7 +76,7 @@ func NewController(controllerConfig *spec.ControllerConfig, controllerId string) | |||
| 	} | ||||
| 
 | ||||
| 	eventBroadcaster := record.NewBroadcaster() | ||||
| 	eventBroadcaster.StartLogging(logger.Debugf) | ||||
| 	eventBroadcaster.StartLogging(logger.Infof) | ||||
| 	recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: myComponentName}) | ||||
| 
 | ||||
| 	c := &Controller{ | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| FROM alpine:3.6 | ||||
| MAINTAINER team-acid@zalando.de | ||||
| 
 | ||||
| EXPOSE 8080 | ||||
| EXPOSE 8081 | ||||
| 
 | ||||
| RUN \ | ||||
|   apk add --no-cache \ | ||||
|  | @ -29,6 +29,7 @@ RUN \ | |||
|     /var/cache/apk/* | ||||
| 
 | ||||
| COPY requirements.txt / | ||||
| COPY start_server.sh / | ||||
| RUN pip3 install -r /requirements.txt | ||||
| 
 | ||||
| COPY operator_ui /operator_ui | ||||
|  | @ -37,4 +38,4 @@ ARG VERSION=dev | |||
| RUN sed -i "s/__version__ = .*/__version__ = '${VERSION}'/" /operator_ui/__init__.py | ||||
| 
 | ||||
| WORKDIR / | ||||
| ENTRYPOINT ["/usr/bin/python3", "-m", "operator_ui"] | ||||
| CMD ["/usr/bin/python3", "-m", "operator_ui"] | ||||
|  |  | |||
|  | @ -36,4 +36,4 @@ push: | |||
| 	docker push "$(IMAGE):$(TAG)$(CDP_TAG)" | ||||
| 
 | ||||
| mock: | ||||
| 	docker run -it -p 8080:8080 "$(IMAGE):$(TAG)" --mock | ||||
| 	docker run -it -p 8081:8081 "$(IMAGE):$(TAG)" --mock | ||||
|  |  | |||
|  | @ -137,6 +137,7 @@ edit | |||
|         o.spec.numberOfInstances = i.spec.numberOfInstances | ||||
|         o.spec.enableMasterLoadBalancer = i.spec.enableMasterLoadBalancer || false | ||||
|         o.spec.enableReplicaLoadBalancer = i.spec.enableReplicaLoadBalancer || false | ||||
|         o.spec.enableConnectionPooler = i.spec.enableConnectionPooler || false | ||||
|         o.spec.volume = { size: i.spec.volume.size } | ||||
| 
 | ||||
|         if ('users' in i.spec && typeof i.spec.users === 'object') { | ||||
|  |  | |||
|  | @ -239,6 +239,18 @@ new | |||
|                   | | ||||
|                   | Enable replica ELB | ||||
| 
 | ||||
|             tr | ||||
|               td Enable Connection Pool | ||||
|               td | ||||
|                 label | ||||
|                   input( | ||||
|                     type='checkbox' | ||||
|                     value='{ enableConnectionPooler }' | ||||
|                     onchange='{ toggleEnableConnectionPooler }' | ||||
|                   ) | ||||
|                   | | ||||
|                   | Enable Connection Pool (using PGBouncer) | ||||
| 
 | ||||
|             tr | ||||
|               td Volume size | ||||
|               td | ||||
|  | @ -493,6 +505,9 @@ new | |||
|         {{#if enableReplicaLoadBalancer}} | ||||
|         enableReplicaLoadBalancer: true | ||||
|         {{/if}} | ||||
|         {{#if enableConnectionPooler}} | ||||
|         enableConnectionPooler: true | ||||
|         {{/if}} | ||||
|         volume: | ||||
|           size: "{{ volumeSize }}Gi" | ||||
|         {{#if users}} | ||||
|  | @ -516,13 +531,14 @@ new | |||
|           - {{ odd }}/32 | ||||
|         {{/if}} | ||||
| 
 | ||||
|         {{#if resourcesVisible}} | ||||
|         resources: | ||||
|           requests: | ||||
|             cpu: {{ cpu.state.request.state }}m | ||||
|             memory: {{ memory.state.request.state }}Mi | ||||
|           limits: | ||||
|             cpu: {{ cpu.state.limit.state }}m | ||||
|             memory: {{ memory.state.limit.state }}Mi{{#if restoring}} | ||||
|             memory: {{ memory.state.limit.state }}Mi{{/if}}{{#if restoring}} | ||||
| 
 | ||||
|         clone: | ||||
|           cluster: "{{ backup.state.name.state }}" | ||||
|  | @ -542,6 +558,7 @@ new | |||
|         instanceCount: this.instanceCount, | ||||
|         enableMasterLoadBalancer: this.enableMasterLoadBalancer, | ||||
|         enableReplicaLoadBalancer: this.enableReplicaLoadBalancer, | ||||
|         enableConnectionPooler: this.enableConnectionPooler, | ||||
|         volumeSize: this.volumeSize, | ||||
|         users: this.users.valids, | ||||
|         databases: this.databases.valids, | ||||
|  | @ -552,6 +569,7 @@ new | |||
|         memory: this.memory, | ||||
|         backup: this.backup, | ||||
|         namespace: this.namespace, | ||||
|         resourcesVisible: this.config.resources_visible, | ||||
|         restoring: this.backup.state.type.state !== 'empty', | ||||
|         pitr: this.backup.state.type.state === 'pitr', | ||||
|       } | ||||
|  | @ -598,6 +616,10 @@ new | |||
|       this.enableReplicaLoadBalancer = !this.enableReplicaLoadBalancer | ||||
|     } | ||||
| 
 | ||||
|     this.toggleEnableConnectionPooler = e => { | ||||
|       this.enableConnectionPooler = !this.enableConnectionPooler | ||||
|     } | ||||
| 
 | ||||
|     this.volumeChange = e => { | ||||
|       this.volumeSize = +e.target.value | ||||
|     } | ||||
|  | @ -892,6 +914,7 @@ new | |||
|       this.odd = '' | ||||
|       this.enableMasterLoadBalancer = false | ||||
|       this.enableReplicaLoadBalancer = false | ||||
|       this.enableConnectionPooler = false | ||||
| 
 | ||||
|       this.postgresqlVersion = this.postgresqlVersion = ( | ||||
|         this.config.postgresql_versions[0] | ||||
|  |  | |||
|  | @ -92,6 +92,8 @@ postgresql | |||
|         .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.pooler }') Connection pooler deployment created | ||||
| 
 | ||||
|       .col-lg-3 | ||||
|         help-general(config='{ opts.config }') | ||||
| 
 | ||||
|  | @ -122,9 +124,11 @@ postgresql | |||
|       jQuery.get( | ||||
|         '/postgresqls/' + this.cluster_path, | ||||
|       ).done(data => { | ||||
|         this.progress.pooler = false | ||||
|         this.progress.postgresql = true | ||||
|         this.progress.postgresqlManifest = data | ||||
|         this.progress.createdTimestamp = data.metadata.creationTimestamp | ||||
|         this.progress.poolerEnabled = data.spec.enableConnectionPooler | ||||
|         this.uid = this.progress.postgresqlManifest.metadata.uid | ||||
|         this.update() | ||||
| 
 | ||||
|  | @ -160,6 +164,11 @@ postgresql | |||
|                 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() | ||||
|             }) | ||||
|           }) | ||||
|  |  | |||
|  | @ -44,6 +44,8 @@ spec: | |||
|               value: "http://postgres-operator:8080" | ||||
|             - name: "OPERATOR_CLUSTER_NAME_LABEL" | ||||
|               value: "cluster-name" | ||||
|             - name: "RESOURCES_VISIBLE" | ||||
|               value: "False" | ||||
|             - name: "TARGET_NAMESPACE" | ||||
|               value: "default" | ||||
|             - name: "TEAMS" | ||||
|  |  | |||
|  | @ -39,6 +39,7 @@ rules: | |||
| - apiGroups: | ||||
|   - apps | ||||
|   resources: | ||||
|   - deployments | ||||
|   - statefulsets | ||||
|   verbs: | ||||
|   - get | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ from flask import ( | |||
| from flask_oauthlib.client import OAuth | ||||
| from functools import wraps | ||||
| from gevent import sleep, spawn | ||||
| from gevent.wsgi import WSGIServer | ||||
| from gevent.pywsgi import WSGIServer | ||||
| from jq import jq | ||||
| from json import dumps, loads | ||||
| from logging import DEBUG, ERROR, INFO, basicConfig, exception, getLogger | ||||
|  | @ -44,6 +44,7 @@ from .spiloutils import ( | |||
|     create_postgresql, | ||||
|     read_basebackups, | ||||
|     read_namespaces, | ||||
|     read_pooler, | ||||
|     read_pods, | ||||
|     read_postgresql, | ||||
|     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_MAINTENANCE_CHECK = getenv('OPERATOR_UI_MAINTENANCE_CHECK', '{}') | ||||
| 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/') | ||||
| SUPERUSER_TEAM = getenv('SUPERUSER_TEAM', 'acid') | ||||
| TARGET_NAMESPACE = getenv('TARGET_NAMESPACE') | ||||
|  | @ -312,6 +314,7 @@ DEFAULT_UI_CONFIG = { | |||
| def get_config(): | ||||
|     config = loads(OPERATOR_UI_CONFIG) or DEFAULT_UI_CONFIG | ||||
|     config['read_only_mode'] = READ_ONLY_MODE | ||||
|     config['resources_visible'] = RESOURCES_VISIBLE | ||||
|     config['superuser_team'] = SUPERUSER_TEAM | ||||
|     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>') | ||||
| @authorize | ||||
| def get_list_clusters(namespace: str, cluster: str): | ||||
|  | @ -587,6 +606,17 @@ def update_postgresql(namespace: str, cluster: str): | |||
| 
 | ||||
|         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']: | ||||
|         rlb = postgresql['spec']['enableReplicaLoadBalancer'] | ||||
|         if not rlb: | ||||
|  | @ -1006,7 +1036,7 @@ def init_cluster(): | |||
| def main(port, secret_key, debug, clusters: list): | ||||
|     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() | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| from boto3 import client | ||||
| from datetime import datetime, timezone | ||||
| from furl import furl | ||||
| from json import dumps | ||||
| from json import dumps, loads | ||||
| from logging import getLogger | ||||
| from os import environ, getenv | ||||
| from requests import Session | ||||
|  | @ -18,6 +18,15 @@ session = Session() | |||
| 
 | ||||
| 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): | ||||
|     if 'timeout' not in kwargs: | ||||
|  | @ -85,6 +94,7 @@ def resource_api_version(resource_type): | |||
|     return { | ||||
|         'postgresqls': 'apis/acid.zalan.do/v1', | ||||
|         'statefulsets': 'apis/apps/v1', | ||||
|         'deployments': 'apis/apps/v1', | ||||
|     }.get(resource_type, 'api/v1') | ||||
| 
 | ||||
| 
 | ||||
|  | @ -149,7 +159,7 @@ def read_pod(cluster, namespace, resource_name): | |||
|         resource_type='pods', | ||||
|         namespace=namespace, | ||||
|         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', | ||||
|         namespace=namespace, | ||||
|         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', | ||||
|         namespace=namespace, | ||||
|         resource_name=resource_name, | ||||
|         label_selector={'application': 'spilo'}, | ||||
|         label_selector=COMMON_CLUSTER_LABEL, | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -302,7 +322,7 @@ def read_basebackups( | |||
|             f=configure_backup_cxt, | ||||
|             aws_instance_profile=use_aws_instance_profile, | ||||
|             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/") | ||||
|     ] | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| Flask-OAuthlib==0.9.5 | ||||
| Flask==1.1.1 | ||||
| Flask==1.1.2 | ||||
| backoff==1.8.1 | ||||
| boto3==1.10.4 | ||||
| boto==2.49.0 | ||||
|  |  | |||
|  | @ -0,0 +1,2 @@ | |||
| #!/bin/bash | ||||
| /usr/bin/python3 -m operator_ui | ||||
		Loading…
	
		Reference in New Issue