From 33928904ec3388ffe9dbebf374dcff0fcde708fa Mon Sep 17 00:00:00 2001 From: "Johannes M. Scheuermann" Date: Wed, 7 Jul 2021 14:26:51 +0100 Subject: [PATCH] Update sidecar to match latest changes in operator --- packaging/docker/sidecar/sidecar.py | 180 ++++++++++++++++++++-------- 1 file changed, 132 insertions(+), 48 deletions(-) diff --git a/packaging/docker/sidecar/sidecar.py b/packaging/docker/sidecar/sidecar.py index 12262db02e..6759578e52 100755 --- a/packaging/docker/sidecar/sidecar.py +++ b/packaging/docker/sidecar/sidecar.py @@ -1,10 +1,10 @@ -#! /usr/bin/env python3 +#! /usr/bin/python # entrypoint.py # # This source file is part of the FoundationDB open source project # -# Copyright 2018-2019 Apple Inc. and the FoundationDB project authors +# Copyright 2018-2021 Apple Inc. and the FoundationDB project authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,10 +21,11 @@ import argparse import hashlib -import http.server +import ipaddress import logging import json import os +import re import shutil import socket import ssl @@ -32,8 +33,11 @@ import stat import time import traceback import sys +import tempfile from pathlib import Path +from http.server import HTTPServer, ThreadingHTTPServer, BaseHTTPRequestHandler + from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler @@ -53,12 +57,10 @@ class Config(object): ), action="store_true", ) - parser.add_argument( - "--bind-address", help="IP and port to bind on", default="0.0.0.0:8080" - ) + parser.add_argument("--bind-address", help="IP and port to bind on") parser.add_argument( "--tls", - help=("This flag enables TLS for incoming " "connections"), + help=("This flag enables TLS for incoming connections"), action="store_true", ) parser.add_argument( @@ -103,7 +105,7 @@ class Config(object): ) parser.add_argument( "--input-dir", - help=("The directory containing the input files " "the config map."), + help=("The directory containing the input files the config map."), default="/var/input-files", ) parser.add_argument( @@ -125,28 +127,33 @@ class Config(object): ) parser.add_argument( "--copy-file", - help=("A file to copy from the config map to the " "output directory."), + help=("A file to copy from the config map to the output directory."), action="append", ) parser.add_argument( "--copy-binary", - help=("A binary to copy from the to the output" "directory."), + help=("A binary to copy from the to the output directory."), action="append", ) parser.add_argument( "--copy-library", - help=( - "A version of the client library to copy " "to the output directory." - ), + help=("A version of the client library to copy to the output directory."), action="append", ) parser.add_argument( "--input-monitor-conf", - help=("The name of a monitor conf template in the " "input files"), + help=("The name of a monitor conf template in the input files"), ) parser.add_argument( "--main-container-version", - help=("The version of the main foundationdb " "container in the pod"), + help=("The version of the main foundationdb container in the pod"), + ) + parser.add_argument( + "--public-ip-family", + help=( + "Tells the sidecar to treat the public IP as a comma-separated " + "list, and use the first entry in the specified IP family" + ), ) parser.add_argument( "--main-container-conf-dir", @@ -216,6 +223,7 @@ class Config(object): "FDB_MACHINE_ID", "FDB_ZONE_ID", "FDB_INSTANCE_ID", + "FDB_POD_IP", ]: self.substitutions[key] = os.getenv(key, "") @@ -225,13 +233,12 @@ class Config(object): if self.substitutions["FDB_ZONE_ID"] == "": self.substitutions["FDB_ZONE_ID"] = self.substitutions["FDB_MACHINE_ID"] if self.substitutions["FDB_PUBLIC_IP"] == "": - address_info = socket.getaddrinfo( - self.substitutions["FDB_MACHINE_ID"], - 4500, - family=socket.AddressFamily.AF_INET, - ) - if len(address_info) > 0: - self.substitutions["FDB_PUBLIC_IP"] = address_info[0][4][0] + # As long as the public IP is not set fallback to the + # Pod IP address. + pod_ip = os.getenv("FDB_POD_IP") + if pod_ip is none: + pod_ip = socket.gethostbyname(socket.gethostname()) + self.substitutions["FDB_PUBLIC_IP"] = pod_ip if self.main_container_version == self.primary_version: self.substitutions["BINARY_DIR"] = "/usr/bin" @@ -290,6 +297,21 @@ class Config(object): if os.getenv("COPY_ONCE", "0") == "1": self.init_mode = True + if args.public_ip_family: + version = int(args.public_ip_family) + self.substitutions["FDB_PUBLIC_IP"] = Config.extract_desired_ip( + version, self.substitutions["FDB_PUBLIC_IP"] + ) + self.substitutions["FDB_POD_IP"] = Config.extract_desired_ip( + version, self.substitutions["FDB_POD_IP"] + ) + + if not self.bind_address: + if self.substitutions["FDB_POD_IP"] != "": + self.bind_address = self.substitutions["FDB_POD_IP"] + ":8080" + else: + self.bind_address = self.substitutions["FDB_PUBLIC_IP"] + ":8080" + @classmethod def shared(cls): if cls.shared_config: @@ -305,8 +327,26 @@ class Config(object): and self.minor_version[1] >= target_version[1] ) + @classmethod + def extract_desired_ip(cls, version, string): + if string == "": + return string -class Server(http.server.BaseHTTPRequestHandler): + ips = string.split(",") + matching_ips = [ip for ip in ips if ipaddress.ip_address(ip).version == version] + if len(matching_ips) == 0: + raise Exception(f"Failed to find IPv{version} entry in {ips}") + ip = matching_ips[0] + if version == 6: + ip = f"[{ip}]" + return ip + + +class ThreadingHTTPServerV6(ThreadingHTTPServer): + address_family = socket.AF_INET6 + + +class Server(BaseHTTPRequestHandler): ssl_context = None @classmethod @@ -315,13 +355,20 @@ class Server(http.server.BaseHTTPRequestHandler): This method starts the server. """ config = Config.shared() - (address, port) = config.bind_address.split(":") - log.info("Listening on %s:%s" % (address, port)) - httpd = http.server.HTTPServer((address, int(port)), cls) + colon_index = config.bind_address.rindex(":") + port_index = colon_index + 1 + address = config.bind_address[:colon_index] + port = config.bind_address[port_index:] + log.info(f"Listening on {address}:{port}") + + if address.startswith("[") and address.endswith("]"): + server = ThreadingHTTPServerV6((address[1:-1], int(port)), cls) + else: + server = ThreadingHTTPServer((address, int(port)), cls) if config.enable_tls: context = Server.load_ssl_context() - httpd.socket = context.wrap_socket(httpd.socket, server_side=True) + server.socket = context.wrap_socket(server.socket, server_side=True) observer = Observer() event_handler = CertificateEventHandler() for path in set( @@ -333,7 +380,7 @@ class Server(http.server.BaseHTTPRequestHandler): observer.schedule(event_handler, path) observer.start() - httpd.serve_forever() + server.serve_forever() @classmethod def load_ssl_context(cls): @@ -341,7 +388,7 @@ class Server(http.server.BaseHTTPRequestHandler): if not cls.ssl_context: cls.ssl_context = ssl.create_default_context(cafile=config.ca_file) cls.ssl_context.check_hostname = False - cls.ssl_context.verify_mode = ssl.CERT_REQUIRED + cls.ssl_context.verify_mode = ssl.CERT_OPTIONAL cls.ssl_context.load_cert_chain(config.certificate_file, config.key_file) return cls.ssl_context @@ -359,13 +406,21 @@ class Server(http.server.BaseHTTPRequestHandler): self.end_headers() self.wfile.write(response) - def check_request_cert(self): + def check_request_cert(self, path): config = Config.shared() - approved = not config.enable_tls or self.check_cert( + + if path == "/ready": + return True + + if not config.enable_tls: + return True + + approved = self.check_cert( self.connection.getpeercert(), config.peer_verification_rules ) if not approved: self.send_error(401, "Client certificate was not approved") + return approved def check_cert(self, cert, rules): @@ -375,6 +430,9 @@ class Server(http.server.BaseHTTPRequestHandler): If there is any problem with the certificate, this will return a string describing the error. """ + if cert is None: + return False + if not rules: return True @@ -456,7 +514,7 @@ class Server(http.server.BaseHTTPRequestHandler): This method executes a GET request. """ try: - if not self.check_request_cert(): + if not self.check_request_cert(self.path): return if self.path.startswith("/check_hash/"): try: @@ -483,7 +541,7 @@ class Server(http.server.BaseHTTPRequestHandler): This method executes a POST request. """ try: - if not self.check_request_cert(): + if not self.check_request_cert(self.path): return if self.path == "/copy_files": self.send_text(copy_files()) @@ -516,7 +574,19 @@ class Server(http.server.BaseHTTPRequestHandler): class CertificateEventHandler(FileSystemEventHandler): def on_any_event(self, event): - log.info("Detected change to certificates") + if event.is_directory: + return None + + if event.event_type not in ["created", "modified"]: + return None + + # We ignore all old files + if event.src_path.endswith(".old"): + return None + + log.info( + f"Detected change to certificates path: {event.src_path}, type: {event.event_type }" + ) time.sleep(10) log.info("Reloading certificates") Server.load_ssl_context() @@ -536,10 +606,13 @@ def copy_files(): path = os.path.join(config.input_dir, filename) if not os.path.isfile(path) or os.path.getsize(path) == 0: raise Exception("No contents for file %s" % path) + for filename in config.copy_files: - tmp_file = os.path.join(config.output_dir, f"{filename}.tmp") - shutil.copy(os.path.join(config.input_dir, filename), tmp_file) - os.replace(tmp_file, os.path.join(config.output_dir, filename)) + tmp_file = tempfile.NamedTemporaryFile( + mode="w+b", dir=config.output_dir, delete=False + ) + shutil.copy(os.path.join(config.input_dir, filename), tmp_file.name) + os.replace(tmp_file.name, os.path.join(config.output_dir, filename)) return "OK" @@ -554,9 +627,13 @@ def copy_binaries(): ) if not target_path.exists(): target_path.parent.mkdir(parents=True, exist_ok=True) - tmp_file = f"{target_path}.tmp" - shutil.copy(path, tmp_file) - os.replace(tmp_file, target_path) + tmp_file = tempfile.NamedTemporaryFile( + mode="w+b", + dir=target_path.parent, + delete=False, + ) + shutil.copy(path, tmp_file.name) + os.replace(tmp_file.name, target_path) target_path.chmod(0o744) return "OK" @@ -573,9 +650,11 @@ def copy_libraries(): ) if not target_path.exists(): target_path.parent.mkdir(parents=True, exist_ok=True) - tmp_file = f"{target_path}.tmp" - shutil.copy(path, tmp_file) - os.replace(tmp_file, target_path) + tmp_file = tempfile.NamedTemporaryFile( + mode="w+b", dir=target_path.parent, delete=False + ) + shutil.copy(path, tmp_file.name) + os.replace(tmp_file.name, target_path) return "OK" @@ -591,13 +670,16 @@ def copy_monitor_conf(): "$" + variable, config.substitutions[variable] ) - tmp_file = os.path.join(config.output_dir, "fdbmonitor.conf.tmp") + tmp_file = tempfile.NamedTemporaryFile( + mode="w+b", dir=config.output_dir, delete=False + ) target_file = os.path.join(config.output_dir, "fdbmonitor.conf") - with open(tmp_file, "w") as output_conf_file: + with open(tmp_file.name, "w") as output_conf_file: output_conf_file.write(monitor_conf) - os.replace(tmp_file, target_file) + os.replace(tmp_file.name, target_file) + return "OK" @@ -629,5 +711,7 @@ if __name__ == "__main__": copy_libraries() copy_monitor_conf() - if not Config.shared().init_mode: - Server.start() + if Config.shared().init_mode: + sys.exit(0) + + Server.start()