1104 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			1104 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			Python
		
	
	
	
#!/usr/bin/env python3
 | 
						|
# pylama:ignore=E402
 | 
						|
 | 
						|
import gevent.monkey
 | 
						|
 | 
						|
gevent.monkey.patch_all()
 | 
						|
 | 
						|
import requests
 | 
						|
import tokens
 | 
						|
import sys
 | 
						|
 | 
						|
from backoff import expo, on_exception
 | 
						|
from click import ParamType, command, echo, option
 | 
						|
 | 
						|
from flask import (
 | 
						|
    Flask,
 | 
						|
    Response,
 | 
						|
    abort,
 | 
						|
    redirect,
 | 
						|
    render_template,
 | 
						|
    request,
 | 
						|
    send_from_directory,
 | 
						|
    session,
 | 
						|
)
 | 
						|
 | 
						|
from flask_oauthlib.client import OAuth
 | 
						|
from functools import wraps
 | 
						|
from gevent import sleep, spawn
 | 
						|
from gevent.pywsgi import WSGIServer
 | 
						|
from jq import jq
 | 
						|
from json import dumps, loads
 | 
						|
from logging import DEBUG, ERROR, INFO, basicConfig, exception, getLogger
 | 
						|
from os import getenv
 | 
						|
from re import X, compile
 | 
						|
from requests.exceptions import RequestException
 | 
						|
from signal import SIGTERM, signal
 | 
						|
from urllib.parse import urljoin
 | 
						|
 | 
						|
from . import __version__
 | 
						|
from .cluster_discovery import DEFAULT_CLUSTERS, StaticClusterDiscoverer
 | 
						|
from .oauth import OAuthRemoteAppWithRefresh
 | 
						|
 | 
						|
from .spiloutils import (
 | 
						|
    apply_postgresql,
 | 
						|
    create_postgresql,
 | 
						|
    read_basebackups,
 | 
						|
    read_namespaces,
 | 
						|
    read_pooler,
 | 
						|
    read_pods,
 | 
						|
    read_postgresql,
 | 
						|
    read_postgresqls,
 | 
						|
    read_service,
 | 
						|
    read_statefulset,
 | 
						|
    read_stored_clusters,
 | 
						|
    read_versions,
 | 
						|
    remove_postgresql,
 | 
						|
)
 | 
						|
 | 
						|
from .utils import (
 | 
						|
    const,
 | 
						|
    identity,
 | 
						|
    these,
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
# Disable access logs from Flask
 | 
						|
getLogger('gevent').setLevel(ERROR)
 | 
						|
 | 
						|
logger = getLogger(__name__)
 | 
						|
 | 
						|
SERVER_STATUS = {'shutdown': False}
 | 
						|
 | 
						|
APP_URL = getenv('APP_URL')
 | 
						|
AUTHORIZE_URL = getenv('AUTHORIZE_URL')
 | 
						|
SPILO_S3_BACKUP_BUCKET = getenv('SPILO_S3_BACKUP_BUCKET')
 | 
						|
TEAM_SERVICE_URL = getenv('TEAM_SERVICE_URL')
 | 
						|
ACCESS_TOKEN_URL = getenv('ACCESS_TOKEN_URL')
 | 
						|
TOKENINFO_URL = getenv('OAUTH2_TOKEN_INFO_URL')
 | 
						|
 | 
						|
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_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')
 | 
						|
GOOGLE_ANALYTICS = getenv('GOOGLE_ANALYTICS', False)
 | 
						|
MIN_PODS= getenv('MIN_PODS', 2)
 | 
						|
 | 
						|
# 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',
 | 
						|
    'https+path://s3.eu-central-1.amazonaws.com:443',
 | 
						|
)
 | 
						|
 | 
						|
USE_AWS_INSTANCE_PROFILE = (
 | 
						|
    getenv('USE_AWS_INSTANCE_PROFILE', 'false').lower() != 'false'
 | 
						|
)
 | 
						|
 | 
						|
AWS_ENDPOINT = getenv('AWS_ENDPOINT')
 | 
						|
 | 
						|
tokens.configure()
 | 
						|
tokens.manage('read-only')
 | 
						|
tokens.start()
 | 
						|
 | 
						|
 | 
						|
def service_auth_header():
 | 
						|
    token = getenv('SERVICE_TOKEN') or tokens.get('read-only')
 | 
						|
    return {
 | 
						|
        'Authorization': f'Bearer {token}',
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
MAX_CONTENT_LENGTH = 16 * 1024 * 1024
 | 
						|
app = Flask(__name__)
 | 
						|
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH
 | 
						|
 | 
						|
 | 
						|
class WSGITransferEncodingChunked:
 | 
						|
    """Support HTTP Transfer-Encoding: chunked transfers"""
 | 
						|
 | 
						|
    def __init__(self, app):
 | 
						|
        self.app = app
 | 
						|
 | 
						|
    def __call__(self, environ, start_response):
 | 
						|
        from io import BytesIO
 | 
						|
        input = environ.get('wsgi.input')
 | 
						|
        length = environ.get('CONTENT_LENGTH', '0')
 | 
						|
        length = 0 if length == '' else int(length)
 | 
						|
        body = b''
 | 
						|
        if length == 0:
 | 
						|
            if input is None:
 | 
						|
                return
 | 
						|
            if environ.get('HTTP_TRANSFER_ENCODING', '0') == 'chunked':
 | 
						|
                size = int(input.readline(), 16)
 | 
						|
                total_size = 0
 | 
						|
                while size > 0:
 | 
						|
                    # Validate max size to avoid DoS attacks
 | 
						|
                    total_size += size
 | 
						|
                    if total_size > MAX_CONTENT_LENGTH:
 | 
						|
                        # Avoid DoS (using all available memory by streaming an
 | 
						|
                        # infinite file)
 | 
						|
                        start_response(
 | 
						|
                            '413 Request Entity Too Large',
 | 
						|
                            [('Content-Type', 'text/plain')],
 | 
						|
                        )
 | 
						|
                        return []
 | 
						|
 | 
						|
                    body += input.read(size + 2)
 | 
						|
                    size = int(input.readline(), 16)
 | 
						|
 | 
						|
        else:
 | 
						|
            body = environ['wsgi.input'].read(length)
 | 
						|
 | 
						|
        environ['CONTENT_LENGTH'] = str(len(body))
 | 
						|
        environ['wsgi.input'] = BytesIO(body)
 | 
						|
 | 
						|
        return self.app(environ, start_response)
 | 
						|
 | 
						|
 | 
						|
oauth = OAuth(app)
 | 
						|
 | 
						|
auth = OAuthRemoteAppWithRefresh(
 | 
						|
    oauth,
 | 
						|
    'auth',
 | 
						|
    request_token_url=None,
 | 
						|
    access_token_method='POST',
 | 
						|
    access_token_url=ACCESS_TOKEN_URL,
 | 
						|
    authorize_url=AUTHORIZE_URL,
 | 
						|
)
 | 
						|
oauth.remote_apps['auth'] = auth
 | 
						|
 | 
						|
 | 
						|
def verify_token(token):
 | 
						|
    if not token:
 | 
						|
        return False
 | 
						|
 | 
						|
    r = requests.get(TOKENINFO_URL, headers={'Authorization': token})
 | 
						|
 | 
						|
    return r.status_code == 200
 | 
						|
 | 
						|
 | 
						|
def authorize(f):
 | 
						|
    @wraps(f)
 | 
						|
    def wrapper(*args, **kwargs):
 | 
						|
        if AUTHORIZE_URL and 'auth_token' not in session:
 | 
						|
            return redirect(urljoin(APP_URL, '/login'))
 | 
						|
        return f(*args, **kwargs)
 | 
						|
 | 
						|
    return wrapper
 | 
						|
 | 
						|
 | 
						|
def ok(body={}, status=200):
 | 
						|
    return (
 | 
						|
        Response(
 | 
						|
            (
 | 
						|
                dumps(body)
 | 
						|
                if isinstance(body, dict) or isinstance(body, list)
 | 
						|
                else body
 | 
						|
            ),
 | 
						|
            mimetype='application/json',
 | 
						|
        ),
 | 
						|
        status
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def fail(body={}, status=400, **kwargs):
 | 
						|
    return (
 | 
						|
        Response(
 | 
						|
            dumps(
 | 
						|
                {
 | 
						|
                    'error': ' '.join(body.split()).format(**kwargs),
 | 
						|
                }
 | 
						|
                if isinstance(body, str)
 | 
						|
                else body,
 | 
						|
            ),
 | 
						|
            mimetype='application/json',
 | 
						|
        ),
 | 
						|
        status,
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def not_found(body={}, **kwargs):
 | 
						|
    return fail(body=body, status=404, **kwargs)
 | 
						|
 | 
						|
 | 
						|
def respond(data, f=identity):
 | 
						|
    return (
 | 
						|
        ok(f(data))
 | 
						|
        if data is not None
 | 
						|
        else not_found()
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def wrong_namespace(**kwargs):
 | 
						|
    return fail(
 | 
						|
        body=f'The Kubernetes namespace must be {TARGET_NAMESPACE}',
 | 
						|
        status=403,
 | 
						|
        **kwargs
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def no_writes_when_read_only(**kwargs):
 | 
						|
    return fail(
 | 
						|
        body='UI is in read-only mode for production',
 | 
						|
        status=403,
 | 
						|
        **kwargs
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
@app.route('/health')
 | 
						|
def health():
 | 
						|
    if SERVER_STATUS['shutdown']:
 | 
						|
        abort(503)
 | 
						|
    else:
 | 
						|
        return 'OK'
 | 
						|
 | 
						|
 | 
						|
STATIC_HEADERS = {
 | 
						|
    'cache-control': ', '.join([
 | 
						|
        'no-store',
 | 
						|
        'no-cache',
 | 
						|
        'must-revalidate',
 | 
						|
        'post-check=0',
 | 
						|
        'pre-check=0',
 | 
						|
        'max-age=0',
 | 
						|
    ]),
 | 
						|
    'Pragma': 'no-cache',
 | 
						|
    'Expires': '-1',
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
@app.route('/css/<path:path>')
 | 
						|
@authorize
 | 
						|
def send_css(path):
 | 
						|
    return send_from_directory('static/', path), 200, STATIC_HEADERS
 | 
						|
 | 
						|
 | 
						|
@app.route('/js/<path:path>')
 | 
						|
@authorize
 | 
						|
def send_js(path):
 | 
						|
    return send_from_directory('static/', path), 200, STATIC_HEADERS
 | 
						|
 | 
						|
 | 
						|
@app.route('/')
 | 
						|
@authorize
 | 
						|
def index():
 | 
						|
    return render_template('index.html', google_analytics=GOOGLE_ANALYTICS)
 | 
						|
 | 
						|
 | 
						|
DEFAULT_UI_CONFIG = {
 | 
						|
    'docs_link': 'https://github.com/zalando/postgres-operator',
 | 
						|
    'odd_host_visible': True,
 | 
						|
    'nat_gateways_visible': True,
 | 
						|
    'users_visible': True,
 | 
						|
    'databases_visible': True,
 | 
						|
    'resources_visible': True,
 | 
						|
    'postgresql_versions': ['11','12','13'],
 | 
						|
    'dns_format_string': '{0}.{1}.{2}',
 | 
						|
    'pgui_link': '',
 | 
						|
    'static_network_whitelist': {},
 | 
						|
    'cost_ebs': COST_EBS,
 | 
						|
    'cost_core': COST_CORE,
 | 
						|
    'cost_memory': COST_MEMORY,
 | 
						|
    'min_pods': MIN_PODS
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
@app.route('/config')
 | 
						|
@authorize
 | 
						|
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
 | 
						|
    config['min_pods'] = MIN_PODS
 | 
						|
 | 
						|
    config['namespaces'] = (
 | 
						|
        [TARGET_NAMESPACE]
 | 
						|
        if TARGET_NAMESPACE not in ['', '*']
 | 
						|
        else [
 | 
						|
            namespace_name
 | 
						|
            for namespace in these(
 | 
						|
                read_namespaces(get_cluster()),
 | 
						|
                'items',
 | 
						|
            )
 | 
						|
            for namespace_name in [namespace['metadata']['name']]
 | 
						|
            if namespace_name not in [
 | 
						|
                'kube-public',
 | 
						|
                'kube-system',
 | 
						|
            ]
 | 
						|
        ]
 | 
						|
    )
 | 
						|
 | 
						|
    try:
 | 
						|
 | 
						|
        kubernetes_maintenance_check = (
 | 
						|
            config.get('kubernetes_maintenance_check') or
 | 
						|
            loads(OPERATOR_UI_MAINTENANCE_CHECK)
 | 
						|
        )
 | 
						|
 | 
						|
        if (
 | 
						|
            kubernetes_maintenance_check and
 | 
						|
            {'url', 'query'} <= kubernetes_maintenance_check.keys()
 | 
						|
        ):
 | 
						|
            config['kubernetes_in_maintenance'] = (
 | 
						|
                jq(kubernetes_maintenance_check['query']).transform(
 | 
						|
                    requests.get(
 | 
						|
                        kubernetes_maintenance_check['url'],
 | 
						|
                        headers=service_auth_header(),
 | 
						|
                    ).json(),
 | 
						|
                )
 | 
						|
            )
 | 
						|
 | 
						|
    except ValueError:
 | 
						|
        exception('Could not determine Kubernetes cluster status')
 | 
						|
 | 
						|
    return ok(config)
 | 
						|
 | 
						|
 | 
						|
def get_teams_for_user(user_name):
 | 
						|
    if not TEAM_SERVICE_URL:
 | 
						|
        return loads(getenv('TEAMS', '[]'))
 | 
						|
 | 
						|
    return [
 | 
						|
        team['id'].lower()
 | 
						|
        for team in requests.get(
 | 
						|
            TEAM_SERVICE_URL.format(user_name),
 | 
						|
            headers=service_auth_header(),
 | 
						|
        ).json()
 | 
						|
    ]
 | 
						|
 | 
						|
 | 
						|
@app.route('/teams')
 | 
						|
@authorize
 | 
						|
def get_teams():
 | 
						|
    return ok(
 | 
						|
        get_teams_for_user(
 | 
						|
            session.get('user_name', ''),
 | 
						|
        )
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
@app.route('/services/<namespace>/<cluster>')
 | 
						|
@authorize
 | 
						|
def get_service(namespace: str, cluster: str):
 | 
						|
 | 
						|
    if TARGET_NAMESPACE not in ['', '*', namespace]:
 | 
						|
        return wrong_namespace()
 | 
						|
 | 
						|
    return respond(
 | 
						|
        read_service(
 | 
						|
            get_cluster(),
 | 
						|
            namespace,
 | 
						|
            cluster,
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
@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):
 | 
						|
 | 
						|
    if TARGET_NAMESPACE not in ['', '*', namespace]:
 | 
						|
        return wrong_namespace()
 | 
						|
 | 
						|
    return respond(
 | 
						|
        read_statefulset(
 | 
						|
            get_cluster(),
 | 
						|
            namespace,
 | 
						|
            cluster,
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
@app.route('/statefulsets/<namespace>/<cluster>/pods')
 | 
						|
@authorize
 | 
						|
def get_list_members(namespace: str, cluster: str):
 | 
						|
 | 
						|
    if TARGET_NAMESPACE not in ['', '*', namespace]:
 | 
						|
        return wrong_namespace()
 | 
						|
 | 
						|
    return respond(
 | 
						|
        read_pods(
 | 
						|
            get_cluster(),
 | 
						|
            namespace,
 | 
						|
            cluster,
 | 
						|
        ),
 | 
						|
        lambda pods: [
 | 
						|
            pod['metadata']
 | 
						|
            for pod in these(pods, 'items')
 | 
						|
        ],
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
@app.route('/namespaces')
 | 
						|
@authorize
 | 
						|
def get_namespaces():
 | 
						|
 | 
						|
    if TARGET_NAMESPACE not in ['', '*']:
 | 
						|
        return ok([TARGET_NAMESPACE])
 | 
						|
 | 
						|
    return respond(
 | 
						|
        read_namespaces(
 | 
						|
            get_cluster(),
 | 
						|
        ),
 | 
						|
        lambda namespaces: [
 | 
						|
            namespace['name']
 | 
						|
            for namespace in these(namespaces, 'items')
 | 
						|
        ],
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
@app.route('/postgresqls')
 | 
						|
@authorize
 | 
						|
def get_postgresqls():
 | 
						|
    postgresqls = [
 | 
						|
        {
 | 
						|
            'nodes': spec.get('numberOfInstances', ''),
 | 
						|
            'memory': spec.get('resources', {}).get('requests', {}).get('memory', 0),
 | 
						|
            'memory_limit': spec.get('resources', {}).get('limits', {}).get('memory', 0),
 | 
						|
            'cpu': spec.get('resources', {}).get('requests', {}).get('cpu', 0),
 | 
						|
            'cpu_limit': spec.get('resources', {}).get('limits', {}).get('cpu', 0),
 | 
						|
            'volume_size': spec.get('volume', {}).get('size', 0),
 | 
						|
            'team': (
 | 
						|
                spec.get('teamId') or
 | 
						|
                metadata.get('labels', {}).get('team', '')
 | 
						|
            ),
 | 
						|
            'namespace': namespace,
 | 
						|
            'name': name,
 | 
						|
            'uid': uid,
 | 
						|
            'namespaced_name': namespace + '/' + name,
 | 
						|
            'full_name': namespace + '/' + name + ('/' + uid if uid else ''),
 | 
						|
            'status': status,
 | 
						|
        }
 | 
						|
        for cluster in these(
 | 
						|
            read_postgresqls(
 | 
						|
                get_cluster(),
 | 
						|
                namespace=(
 | 
						|
                    None
 | 
						|
                    if TARGET_NAMESPACE in ['', '*']
 | 
						|
                    else TARGET_NAMESPACE
 | 
						|
                ),
 | 
						|
            ),
 | 
						|
            'items',
 | 
						|
        )
 | 
						|
        for spec in [cluster.get('spec', {}) if cluster.get('spec', {}) is not None else {"error": "Invalid spec in manifest"}]
 | 
						|
        for status in [cluster.get('status', {})]
 | 
						|
        for metadata in [cluster['metadata']]
 | 
						|
        for namespace in [metadata['namespace']]
 | 
						|
        for name in [metadata['name']]
 | 
						|
        for uid in [metadata.get('uid', '')]
 | 
						|
    ]
 | 
						|
    return respond(postgresqls)
 | 
						|
 | 
						|
 | 
						|
# Note these are meant to be consistent with the operator backend validations;
 | 
						|
# See https://github.com/zalando/postgres-operator/blob/master/pkg/cluster/cluster.go  # noqa
 | 
						|
VALID_SIZE = compile(r'^[1-9][0-9]{0,3}Gi$')
 | 
						|
VALID_CLUSTER_NAME = compile(r'^[a-z0-9]+[a-z0-9\-]+[a-z0-9]+$')
 | 
						|
VALID_DATABASE_NAME = compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$')
 | 
						|
VALID_USERNAME = compile(
 | 
						|
    r'''
 | 
						|
        ^[a-z0-9]([-_a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-_a-z0-9]*[a-z0-9])?)*$
 | 
						|
    ''',
 | 
						|
    X,
 | 
						|
)
 | 
						|
 | 
						|
ROLEFLAGS = '''
 | 
						|
    SUPERUSER
 | 
						|
    INHERIT
 | 
						|
    LOGIN
 | 
						|
    NOLOGIN
 | 
						|
    CREATEROLE
 | 
						|
    CREATEDB
 | 
						|
    REPLICATION
 | 
						|
    BYPASSRLS
 | 
						|
'''.split()
 | 
						|
 | 
						|
 | 
						|
def namespaced(handler):
 | 
						|
 | 
						|
    def run(*args, **kwargs):
 | 
						|
        namespace = (
 | 
						|
            args[1]
 | 
						|
            if len(args) >= 2
 | 
						|
            else kwargs['namespace']
 | 
						|
        )
 | 
						|
 | 
						|
        if TARGET_NAMESPACE not in ['', '*', namespace]:
 | 
						|
            return wrong_namespace()
 | 
						|
 | 
						|
        return handler(*args, **kwargs)
 | 
						|
 | 
						|
    return run
 | 
						|
 | 
						|
 | 
						|
def read_only(handler):
 | 
						|
 | 
						|
    def run(*args, **kwargs):
 | 
						|
        if READ_ONLY_MODE:
 | 
						|
            return no_writes_when_read_only()
 | 
						|
 | 
						|
        return handler(*args, **kwargs)
 | 
						|
 | 
						|
    return run
 | 
						|
 | 
						|
 | 
						|
@app.route('/postgresqls/<namespace>/<cluster>', methods=['POST'])
 | 
						|
@authorize
 | 
						|
@namespaced
 | 
						|
def update_postgresql(namespace: str, cluster: str):
 | 
						|
    if READ_ONLY_MODE:
 | 
						|
        return no_writes_when_read_only()
 | 
						|
 | 
						|
    o = read_postgresql(get_cluster(), namespace, cluster)
 | 
						|
    if o is None:
 | 
						|
        return not_found()
 | 
						|
 | 
						|
    postgresql = request.get_json(force=True)
 | 
						|
 | 
						|
    teams = get_teams_for_user(session.get('user_name', ''))
 | 
						|
    logger.info(f'Changes to: {cluster} by {session.get("user_name", "local-user")}/{teams} {postgresql}')  # noqa
 | 
						|
 | 
						|
    if SUPERUSER_TEAM and SUPERUSER_TEAM in teams:
 | 
						|
        logger.info(f'Allowing edit due to membership in superuser team {SUPERUSER_TEAM}')  # noqa
 | 
						|
    elif not o['spec']['teamId'].lower() in teams:
 | 
						|
        return fail('Not a member of the owning team', status=401)
 | 
						|
 | 
						|
    # do spec copy 1 by 1 not to do unsupporeted changes for now
 | 
						|
    spec = {}
 | 
						|
    if 'allowedSourceRanges' in postgresql['spec']:
 | 
						|
        if not isinstance(postgresql['spec']['allowedSourceRanges'], list):
 | 
						|
            return fail('allowedSourceRanges invalid')
 | 
						|
        spec['allowedSourceRanges'] = postgresql['spec']['allowedSourceRanges']
 | 
						|
 | 
						|
    if 'numberOfInstances' in postgresql['spec']:
 | 
						|
        if not isinstance(postgresql['spec']['numberOfInstances'], int):
 | 
						|
            return fail('numberOfInstances invalid')
 | 
						|
        spec['numberOfInstances'] = postgresql['spec']['numberOfInstances']
 | 
						|
 | 
						|
    if (
 | 
						|
        'volume' in postgresql['spec']
 | 
						|
        and 'size' in postgresql['spec']['volume']
 | 
						|
    ):
 | 
						|
        size = str(postgresql['spec']['volume']['size'])
 | 
						|
        if not VALID_SIZE.match(size):
 | 
						|
            return fail('volume.size is invalid; should be like 123Gi')
 | 
						|
 | 
						|
        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 'enableReplicaConnectionPooler' in postgresql['spec']:
 | 
						|
        cp = postgresql['spec']['enableReplicaConnectionPooler']
 | 
						|
        if not cp:
 | 
						|
            if 'enableReplicaConnectionPooler' in o['spec']:
 | 
						|
                del o['spec']['enableReplicaConnectionPooler']
 | 
						|
        else:
 | 
						|
            spec['enableReplicaConnectionPooler'] = True
 | 
						|
    else:
 | 
						|
        if 'enableReplicaConnectionPooler' in o['spec']:
 | 
						|
            del o['spec']['enableReplicaConnectionPooler']
 | 
						|
 | 
						|
    if 'enableReplicaLoadBalancer' in postgresql['spec']:
 | 
						|
        rlb = postgresql['spec']['enableReplicaLoadBalancer']
 | 
						|
        if not rlb:
 | 
						|
            if 'enableReplicaLoadBalancer' in o['spec']:
 | 
						|
                del o['spec']['enableReplicaLoadBalancer']
 | 
						|
        else:
 | 
						|
            spec['enableReplicaLoadBalancer'] = True
 | 
						|
    else:
 | 
						|
        if 'enableReplicaLoadBalancer' in o['spec']:
 | 
						|
            del o['spec']['enableReplicaLoadBalancer']
 | 
						|
 | 
						|
    if 'enableMasterLoadBalancer' in postgresql['spec']:
 | 
						|
        rlb = postgresql['spec']['enableMasterLoadBalancer']
 | 
						|
        if not rlb:
 | 
						|
            if 'enableMasterLoadBalancer' in o['spec']:
 | 
						|
                del o['spec']['enableMasterLoadBalancer']
 | 
						|
        else:
 | 
						|
            spec['enableMasterLoadBalancer'] = True
 | 
						|
    else:
 | 
						|
        if 'enableMasterLoadBalancer' in o['spec']:
 | 
						|
            del o['spec']['enableMasterLoadBalancer']
 | 
						|
 | 
						|
    if 'users' in postgresql['spec']:
 | 
						|
        spec['users'] = postgresql['spec']['users']
 | 
						|
 | 
						|
        if not isinstance(postgresql['spec']['users'], dict):
 | 
						|
            return fail('''
 | 
						|
                the "users" key must hold a key-value object mapping usernames
 | 
						|
                to a list of their role flags as simple strings.
 | 
						|
                e.g.: {{"some_username": ["createdb", "login"]}}
 | 
						|
            ''')
 | 
						|
 | 
						|
        for username, role_flags in postgresql['spec']['users'].items():
 | 
						|
 | 
						|
            if not VALID_USERNAME.match(username):
 | 
						|
                return fail(
 | 
						|
                    '''
 | 
						|
                        no match for valid username pattern {VALID_USERNAME} in
 | 
						|
                        invalid username {username}
 | 
						|
                    ''',
 | 
						|
                    VALID_USERNAME=VALID_USERNAME.pattern,
 | 
						|
                    username=username,
 | 
						|
                )
 | 
						|
 | 
						|
            if not isinstance(role_flags, list):
 | 
						|
                return fail(
 | 
						|
                    '''
 | 
						|
                        the value for the user key {username} must be a list of
 | 
						|
                        the user's permissions as simple strings.
 | 
						|
                        e.g.: ["createdb", "login"]
 | 
						|
                    ''',
 | 
						|
                    username=username,
 | 
						|
                )
 | 
						|
 | 
						|
            for role_flag in role_flags:
 | 
						|
 | 
						|
                if not isinstance(role_flag, str):
 | 
						|
                    return fail(
 | 
						|
                        '''
 | 
						|
                            the value for the user key {username} must be a
 | 
						|
                            list of the user's permissions as simple strings.
 | 
						|
                            e.g.: ["createdb", "login"]
 | 
						|
                        ''',
 | 
						|
                        username=username,
 | 
						|
                    )
 | 
						|
 | 
						|
                if role_flag.upper() not in ROLEFLAGS:
 | 
						|
                    return fail(
 | 
						|
                        '''
 | 
						|
                            user {username} has invalid role flag {role_flag}
 | 
						|
                            - allowed flags are {all_flags}
 | 
						|
                        ''',
 | 
						|
                        username=username,
 | 
						|
                        role_flag=role_flag,
 | 
						|
                        all_flags=', '.join(ROLEFLAGS),
 | 
						|
                    )
 | 
						|
 | 
						|
    if 'databases' in postgresql['spec']:
 | 
						|
        spec['databases'] = postgresql['spec']['databases']
 | 
						|
 | 
						|
        if not isinstance(postgresql['spec']['databases'], dict):
 | 
						|
            return fail('''
 | 
						|
                the "databases" key must hold a key-value object mapping
 | 
						|
                database names to their respective owner's username as a simple
 | 
						|
                string.  e.g. {{"some_database_name": "some_username"}}
 | 
						|
            ''')
 | 
						|
 | 
						|
        for database_name, owner_username in (
 | 
						|
            postgresql['spec']['databases'].items()
 | 
						|
        ):
 | 
						|
 | 
						|
            if not VALID_DATABASE_NAME.match(database_name):
 | 
						|
                return fail(
 | 
						|
                    '''
 | 
						|
                        no match for valid database name pattern
 | 
						|
                        {VALID_DATABASE_NAME} in invalid database name
 | 
						|
                        {database_name}
 | 
						|
                    ''',
 | 
						|
                    VALID_DATABASE_NAME=VALID_DATABASE_NAME.pattern,
 | 
						|
                    database_name=database_name,
 | 
						|
                )
 | 
						|
 | 
						|
            if not isinstance(owner_username, str):
 | 
						|
                return fail(
 | 
						|
                    '''
 | 
						|
                        the value for the database key {database_name} must be
 | 
						|
                        the owning user's username as a simple string.  e.g.:
 | 
						|
                        "some_username"
 | 
						|
                    ''',
 | 
						|
                    database_name=database_name,
 | 
						|
                )
 | 
						|
 | 
						|
            if not VALID_USERNAME.match(owner_username):
 | 
						|
                return fail(
 | 
						|
                    '''
 | 
						|
                        no match for valid username pattern {VALID_USERNAME} in
 | 
						|
                        invalid database owner username {owner_username}
 | 
						|
                    ''',
 | 
						|
                    VALID_USERNAME=VALID_USERNAME.pattern,
 | 
						|
                    owner_username=owner_username,
 | 
						|
                )
 | 
						|
 | 
						|
    o['spec'].update(spec)
 | 
						|
 | 
						|
    apply_postgresql(get_cluster(), namespace, cluster, o)
 | 
						|
 | 
						|
    return ok(o)
 | 
						|
 | 
						|
 | 
						|
@app.route('/postgresqls/<namespace>/<cluster>', methods=['GET'])
 | 
						|
@authorize
 | 
						|
def get_postgresql(namespace: str, cluster: str):
 | 
						|
 | 
						|
    if TARGET_NAMESPACE not in ['', '*', namespace]:
 | 
						|
        return wrong_namespace()
 | 
						|
 | 
						|
    return respond(
 | 
						|
        read_postgresql(
 | 
						|
            get_cluster(),
 | 
						|
            namespace,
 | 
						|
            cluster,
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
@app.route('/stored_clusters')
 | 
						|
@authorize
 | 
						|
def get_stored_clusters():
 | 
						|
    return respond(
 | 
						|
        read_stored_clusters(
 | 
						|
            bucket=SPILO_S3_BACKUP_BUCKET,
 | 
						|
            prefix=SPILO_S3_BACKUP_PREFIX,
 | 
						|
        )
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
@app.route('/stored_clusters/<pg_cluster>', methods=['GET'])
 | 
						|
@authorize
 | 
						|
def get_versions(pg_cluster: str):
 | 
						|
    return respond(
 | 
						|
        read_versions(
 | 
						|
            bucket=SPILO_S3_BACKUP_BUCKET,
 | 
						|
            pg_cluster=pg_cluster,
 | 
						|
            prefix=SPILO_S3_BACKUP_PREFIX,
 | 
						|
            s3_endpoint=WALE_S3_ENDPOINT,
 | 
						|
            use_aws_instance_profile=USE_AWS_INSTANCE_PROFILE,
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
 | 
						|
@app.route('/stored_clusters/<pg_cluster>/<uid>', methods=['GET'])
 | 
						|
@authorize
 | 
						|
def get_basebackups(pg_cluster: str, uid: str):
 | 
						|
    return respond(
 | 
						|
        read_basebackups(
 | 
						|
            bucket=SPILO_S3_BACKUP_BUCKET,
 | 
						|
            pg_cluster=pg_cluster,
 | 
						|
            prefix=SPILO_S3_BACKUP_PREFIX,
 | 
						|
            s3_endpoint=WALE_S3_ENDPOINT,
 | 
						|
            uid=uid,
 | 
						|
            use_aws_instance_profile=USE_AWS_INSTANCE_PROFILE,
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
@app.route('/create-cluster', methods=['POST'])
 | 
						|
@authorize
 | 
						|
def create_new_cluster():
 | 
						|
 | 
						|
    if READ_ONLY_MODE:
 | 
						|
        return no_writes_when_read_only()
 | 
						|
 | 
						|
    postgresql = request.get_json(force=True)
 | 
						|
 | 
						|
    cluster_name = postgresql['metadata']['name']
 | 
						|
    if not VALID_CLUSTER_NAME.match(cluster_name):
 | 
						|
        return fail(r'metadata.name is invalid. [a-z0-9\-]+')
 | 
						|
 | 
						|
    namespace = postgresql['metadata']['namespace']
 | 
						|
    if not namespace:
 | 
						|
        return fail('metadata.namespace must not be empty')
 | 
						|
    if TARGET_NAMESPACE not in ['', '*', namespace]:
 | 
						|
        return wrong_namespace()
 | 
						|
 | 
						|
    teams = get_teams_for_user(session.get('user_name', ''))
 | 
						|
    logger.info(f'Create cluster by {session.get("user_name", "local-user")}/{teams} {postgresql}')  # noqa
 | 
						|
 | 
						|
    if SUPERUSER_TEAM and SUPERUSER_TEAM in teams:
 | 
						|
        logger.info(f'Allowing create due to membership in superuser team {SUPERUSER_TEAM}')  # noqa
 | 
						|
    elif not postgresql['spec']['teamId'].lower() in teams:
 | 
						|
        return fail('Not a member of the owning team', status=401)
 | 
						|
 | 
						|
    r = create_postgresql(get_cluster(), namespace, postgresql)
 | 
						|
    return ok() if r else fail(status=500)
 | 
						|
 | 
						|
 | 
						|
@app.route('/postgresqls/<namespace>/<cluster>', methods=['DELETE'])
 | 
						|
@authorize
 | 
						|
def delete_postgresql(namespace: str, cluster: str):
 | 
						|
    if TARGET_NAMESPACE not in ['', '*', namespace]:
 | 
						|
        return wrong_namespace()
 | 
						|
 | 
						|
    if READ_ONLY_MODE:
 | 
						|
        return no_writes_when_read_only()
 | 
						|
 | 
						|
    postgresql = read_postgresql(get_cluster(), namespace, cluster)
 | 
						|
    if postgresql is None:
 | 
						|
        return not_found()
 | 
						|
 | 
						|
    teams = get_teams_for_user(session.get('user_name', ''))
 | 
						|
 | 
						|
    logger.info(f'Delete cluster: {cluster} by {session.get("user_name", "local-user")}/{teams}')  # noqa
 | 
						|
 | 
						|
    if SUPERUSER_TEAM and SUPERUSER_TEAM in teams:
 | 
						|
        logger.info(f'Allowing delete due to membership in superuser team {SUPERUSER_TEAM}')  # noqa
 | 
						|
    elif not postgresql['spec']['teamId'].lower() in teams:
 | 
						|
        return fail('Not a member of the owning team', status=401)
 | 
						|
 | 
						|
    return respond(
 | 
						|
        remove_postgresql(
 | 
						|
            get_cluster(),
 | 
						|
            namespace,
 | 
						|
            cluster,
 | 
						|
        ),
 | 
						|
        const(None),
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def proxy_operator(url: str):
 | 
						|
    response = requests.get(OPERATOR_API_URL + url)
 | 
						|
    response.raise_for_status()
 | 
						|
    return respond(response.json())
 | 
						|
 | 
						|
 | 
						|
@app.route('/operator/status')
 | 
						|
@authorize
 | 
						|
def get_operator_status():
 | 
						|
    return proxy_operator('/status/')
 | 
						|
 | 
						|
 | 
						|
@app.route('/operator/workers/<worker>/queue')
 | 
						|
@authorize
 | 
						|
def get_operator_get_queue(worker: int):
 | 
						|
    return proxy_operator(f'/workers/{worker}/queue')
 | 
						|
 | 
						|
 | 
						|
@app.route('/operator/workers/<worker>/logs')
 | 
						|
@authorize
 | 
						|
def get_operator_get_logs(worker: int):
 | 
						|
    return proxy_operator(f'/workers/{worker}/logs')
 | 
						|
 | 
						|
 | 
						|
@app.route('/operator/clusters/<namespace>/<cluster>/logs')
 | 
						|
@authorize
 | 
						|
def get_operator_get_logs_per_cluster(namespace: str, cluster: str):
 | 
						|
    team, clustername = cluster.split('-', 1)
 | 
						|
    return proxy_operator(f'/clusters/{team}/{namespace}/{clustername}/logs/')
 | 
						|
 | 
						|
 | 
						|
@app.route('/login')
 | 
						|
def login():
 | 
						|
    redirect = request.args.get('redirect', False)
 | 
						|
    if not redirect:
 | 
						|
        return render_template('login-deeplink.html')
 | 
						|
 | 
						|
    redirect_uri = urljoin(APP_URL, '/login/authorized')
 | 
						|
    return auth.authorize(callback=redirect_uri)
 | 
						|
 | 
						|
 | 
						|
@app.route('/logout')
 | 
						|
def logout():
 | 
						|
    session.pop('auth_token', None)
 | 
						|
    return redirect(urljoin(APP_URL, '/'))
 | 
						|
 | 
						|
 | 
						|
@app.route('/favicon.png')
 | 
						|
def favicon():
 | 
						|
    return send_from_directory('static/', 'favicon-96x96.png'), 200
 | 
						|
 | 
						|
 | 
						|
@app.route('/login/authorized')
 | 
						|
def authorized():
 | 
						|
    resp = auth.authorized_response()
 | 
						|
    if resp is None:
 | 
						|
        return 'Access denied: reason=%s error=%s' % (
 | 
						|
            request.args['error'],
 | 
						|
            request.args['error_description']
 | 
						|
        )
 | 
						|
 | 
						|
    if not isinstance(resp, dict):
 | 
						|
        return 'Invalid auth response'
 | 
						|
 | 
						|
    session['auth_token'] = (resp['access_token'], '')
 | 
						|
 | 
						|
    r = requests.get(
 | 
						|
        TOKENINFO_URL,
 | 
						|
        headers={
 | 
						|
            'Authorization': f'Bearer {session["auth_token"][0]}',
 | 
						|
        },
 | 
						|
    )
 | 
						|
    session['user_name'] = r.json().get('uid')
 | 
						|
 | 
						|
    logger.info(f'Login from: {session["user_name"]}')
 | 
						|
 | 
						|
    # return redirect(urljoin(APP_URL, '/'))
 | 
						|
    return render_template('login-resolve-deeplink.html')
 | 
						|
 | 
						|
 | 
						|
def shutdown():
 | 
						|
    # just wait some time to give Kubernetes time to update endpoints
 | 
						|
    # this requires changing the readinessProbe's
 | 
						|
    # PeriodSeconds and FailureThreshold appropriately
 | 
						|
    # see https://godoc.org/k8s.io/kubernetes/pkg/api/v1#Probe
 | 
						|
    sleep(10)
 | 
						|
    exit(0)
 | 
						|
 | 
						|
 | 
						|
def exit_gracefully(signum, frame):
 | 
						|
    logger.info('Received TERM signal, shutting down..')
 | 
						|
    SERVER_STATUS['shutdown'] = True
 | 
						|
    spawn(shutdown)
 | 
						|
 | 
						|
 | 
						|
def print_version(ctx, param, value):
 | 
						|
    if not value or ctx.resilient_parsing:
 | 
						|
        return
 | 
						|
    echo(f'PostgreSQL Operator UI {__version__}')
 | 
						|
    ctx.exit()
 | 
						|
 | 
						|
 | 
						|
class CommaSeparatedValues(ParamType):
 | 
						|
    name = 'comma_separated_values'
 | 
						|
 | 
						|
    def convert(self, value, param, ctx):
 | 
						|
        return (
 | 
						|
            filter(None, value.split(','))
 | 
						|
            if isinstance(value, str)
 | 
						|
            else value
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
CLUSTER = None
 | 
						|
 | 
						|
 | 
						|
def get_cluster():
 | 
						|
    return CLUSTER
 | 
						|
 | 
						|
 | 
						|
def set_cluster(c):
 | 
						|
    global CLUSTER
 | 
						|
    CLUSTER = c
 | 
						|
    return CLUSTER
 | 
						|
 | 
						|
 | 
						|
def init_cluster():
 | 
						|
    discoverer = StaticClusterDiscoverer([])
 | 
						|
    set_cluster(discoverer.get_clusters()[0])
 | 
						|
 | 
						|
 | 
						|
@command(context_settings={'help_option_names': ['-h', '--help']})
 | 
						|
@option(
 | 
						|
    '-V',
 | 
						|
    '--version',
 | 
						|
    callback=print_version,
 | 
						|
    expose_value=False,
 | 
						|
    help='Print the current version number and exit.',
 | 
						|
    is_eager=True,
 | 
						|
    is_flag=True,
 | 
						|
)
 | 
						|
@option(
 | 
						|
    '-p',
 | 
						|
    '--port',
 | 
						|
    default=8081,
 | 
						|
    envvar='SERVER_PORT',
 | 
						|
    help='HTTP port to listen on (default: 8081)',
 | 
						|
    type=int,
 | 
						|
)
 | 
						|
@option(
 | 
						|
    '-d',
 | 
						|
    '--debug',
 | 
						|
    help='Verbose logging',
 | 
						|
    is_flag=True,
 | 
						|
)
 | 
						|
@option(
 | 
						|
    '--secret-key',
 | 
						|
    default='development',
 | 
						|
    envvar='SECRET_KEY',
 | 
						|
    help='Secret key for session cookies',
 | 
						|
)
 | 
						|
@option(
 | 
						|
    '--clusters',
 | 
						|
    envvar='CLUSTERS',
 | 
						|
    help=f'Comma separated list of Kubernetes API server URLs (default: {DEFAULT_CLUSTERS})',  # noqa
 | 
						|
    type=CommaSeparatedValues(),
 | 
						|
)
 | 
						|
def main(port, secret_key, debug, clusters: list):
 | 
						|
    global TARGET_NAMESPACE
 | 
						|
 | 
						|
    basicConfig(stream=sys.stdout, level=(DEBUG if debug else INFO), format='%(asctime)s %(levelname)s: %(message)s',)
 | 
						|
 | 
						|
    init_cluster()
 | 
						|
 | 
						|
    logger.info(f'Access token URL: {ACCESS_TOKEN_URL}')
 | 
						|
    logger.info(f'App URL: {APP_URL}')
 | 
						|
    logger.info(f'Authorize URL: {AUTHORIZE_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'Spilo S3 backup bucket: {SPILO_S3_BACKUP_BUCKET}')
 | 
						|
    logger.info(f'Spilo S3 backup prefix: {SPILO_S3_BACKUP_PREFIX}')
 | 
						|
    logger.info(f'Superuser team: {SUPERUSER_TEAM}')
 | 
						|
    logger.info(f'Target namespace: {TARGET_NAMESPACE}')
 | 
						|
    logger.info(f'Teamservice URL: {TEAM_SERVICE_URL}')
 | 
						|
    logger.info(f'Tokeninfo URL: {TOKENINFO_URL}')
 | 
						|
    logger.info(f'Use AWS instance_profile: {USE_AWS_INSTANCE_PROFILE}')
 | 
						|
    logger.info(f'WAL-E S3 endpoint: {WALE_S3_ENDPOINT}')
 | 
						|
    logger.info(f'AWS S3 endpoint: {AWS_ENDPOINT}')
 | 
						|
 | 
						|
    if TARGET_NAMESPACE is None:
 | 
						|
        @on_exception(
 | 
						|
            expo,
 | 
						|
            RequestException,
 | 
						|
        )
 | 
						|
        def get_target_namespace():
 | 
						|
            logger.info('Fetching target namespace from Operator API')
 | 
						|
            return (
 | 
						|
                requests
 | 
						|
                .get(OPERATOR_API_URL + '/config/')
 | 
						|
                .json()
 | 
						|
                ['operator']
 | 
						|
                ['WatchedNamespace']
 | 
						|
            )
 | 
						|
        TARGET_NAMESPACE = get_target_namespace()
 | 
						|
        logger.info(f'Target namespace set to: {TARGET_NAMESPACE or "*"}')
 | 
						|
 | 
						|
    app.debug = debug
 | 
						|
    app.secret_key = secret_key
 | 
						|
 | 
						|
    signal(SIGTERM, exit_gracefully)
 | 
						|
 | 
						|
    app.wsgi_app = WSGITransferEncodingChunked(app.wsgi_app)
 | 
						|
    http_server = WSGIServer(('0.0.0.0', port), app)
 | 
						|
    logger.info(f'Listening on :{port}')
 | 
						|
    http_server.serve_forever()
 |