From 5e81e09855179e58b85f8d186b8afe371b2dbcc3 Mon Sep 17 00:00:00 2001 From: "michael.yak" Date: Mon, 18 May 2020 00:52:43 +0300 Subject: [PATCH] Pyctuator should be able to register with SBA that's using basic-auth It should be possible to configure username and password for Pyctuator to use as basic-authentication when registering to spring-beet-admin. Solves #8 Set Pyctuator version to 0.10 --- README.md | 18 +++++++++-- examples/Advanced/pyproject.toml | 2 +- examples/FastAPI/pyproject.toml | 2 +- examples/Flask/pyproject.toml | 2 +- pyctuator/auth.py | 13 ++++++++ .../impl/spring_boot_admin_registration.py | 30 ++++++++++++++++--- pyctuator/pyctuator.py | 4 +++ pyproject.toml | 2 +- 8 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 pyctuator/auth.py diff --git a/README.md b/README.md index bfd2d0a..a4264d1 100644 --- a/README.md +++ b/README.md @@ -241,8 +241,6 @@ Note that the `psutil` dependency is **optional** and is only required if you wa Pyctuator leverages Python's builtin `logging` framework and allows controlling log levels at runtime. Note that in order to control uvicorn's log level, you need to provide a logger object when instantiating it. For example: - - ```python myFastAPIServer = Server( config=Config( @@ -253,6 +251,22 @@ myFastAPIServer = Server( ) ``` +### Spring Boot Admin Using Basic Authentication +Pyctuator supports registration with Spring Boot Admin that requires basic authentications. The credentials are provided when initializing the Pyctuator instance as follows: +```python +# NOTE: Never include secrets in your code !!! +auth = Auth(os.getenv("sba-username"), os.getenv("sba-password")) + +Pyctuator( + app, + "Flask Pyctuator", + "http://localhost:5000", + f"http://localhost:5000/pyctuator", + registration_url=f"http://spring-boot-admin:8082/instances", + registration_auth=auth, +) +``` + ## Full blown examples The `examples` folder contains full blown Python projects that are built using [Poetry](https://python-poetry.org/). diff --git a/examples/Advanced/pyproject.toml b/examples/Advanced/pyproject.toml index 6f8cabb..73cadeb 100644 --- a/examples/Advanced/pyproject.toml +++ b/examples/Advanced/pyproject.toml @@ -11,7 +11,7 @@ python = "^3.7" psutil = { version = "^5.6" } fastapi = { version = "^0.41.0" } uvicorn = { version = "^0.9.0" } -pyctuator = { version = "^0.9" } +pyctuator = { version = "^0.10" } sqlalchemy = { version = "^1.3" } PyMySQL = { version = "^0.9.3" } cryptography = { version = "^2.8" } diff --git a/examples/FastAPI/pyproject.toml b/examples/FastAPI/pyproject.toml index 4646b1b..36ee68a 100644 --- a/examples/FastAPI/pyproject.toml +++ b/examples/FastAPI/pyproject.toml @@ -11,7 +11,7 @@ python = "^3.7" psutil = { version = "^5.6" } fastapi = { version = "^0.41.0" } uvicorn = { version = "^0.9.0" } -pyctuator = { version = "^0.9" } +pyctuator = { version = "^0.10" } [build-system] requires = ["poetry>=0.12"] diff --git a/examples/Flask/pyproject.toml b/examples/Flask/pyproject.toml index 11282f5..f9852b1 100644 --- a/examples/Flask/pyproject.toml +++ b/examples/Flask/pyproject.toml @@ -10,7 +10,7 @@ authors = [ python = "^3.7" psutil = { version = "^5.6" } flask = { version = "^1.1" } -pyctuator = { version = "^0.9" } +pyctuator = { version = "^0.10" } [build-system] requires = ["poetry>=0.12"] diff --git a/pyctuator/auth.py b/pyctuator/auth.py new file mode 100644 index 0000000..c5b5b22 --- /dev/null +++ b/pyctuator/auth.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Auth: + pass + + +@dataclass +class BasicAuth(Auth): + username: str + password: Optional[str] diff --git a/pyctuator/impl/spring_boot_admin_registration.py b/pyctuator/impl/spring_boot_admin_registration.py index af44dff..6bc3c33 100644 --- a/pyctuator/impl/spring_boot_admin_registration.py +++ b/pyctuator/impl/spring_boot_admin_registration.py @@ -3,10 +3,13 @@ import logging import threading import urllib.parse +from base64 import b64encode from datetime import datetime from http.client import HTTPConnection -from typing import Optional +from typing import Optional, Dict + +from pyctuator.auth import Auth, BasicAuth # pylint: disable=too-many-instance-attributes @@ -15,6 +18,7 @@ class BootAdminRegistrationHandler: def __init__( self, registration_url: str, + registration_auth: Optional[Auth], application_name: str, pyctuator_base_url: str, start_time: datetime, @@ -22,6 +26,7 @@ def __init__( registration_interval_sec: int, ) -> None: self.registration_url = registration_url + self.registration_auth = registration_auth self.application_name = application_name self.pyctuator_base_url = pyctuator_base_url self.start_time = start_time @@ -56,17 +61,22 @@ def _register_with_admin_server(self) -> None: "metadata": {"startup": self.start_time.isoformat()} } - logging.debug("Trying to post registration data to %s: %s", self.registration_url, registration_data) + logging.debug("Trying to post registration data to %s: %s", + self.registration_url, registration_data) conn: Optional[HTTPConnection] = None try: + headers = {"Content-type": "application/json"} + self.authenticate(headers) + reg_url_split = urllib.parse.urlsplit(self.registration_url) conn = http.client.HTTPConnection(reg_url_split.hostname, reg_url_split.port) conn.request( "POST", reg_url_split.path, body=json.dumps(registration_data), - headers={"Content-type": "application/json"}) + headers=headers, + ) response = conn.getresponse() if response.status < 200 or response.status >= 300: @@ -89,6 +99,9 @@ def deregister_from_admin_server(self) -> None: if self.instance_id is None: return + headers = {} + self.authenticate(headers) + deregistration_url = f"{self.registration_url}/{self.instance_id}" logging.info("Deregistering from %s", deregistration_url) @@ -98,7 +111,9 @@ def deregister_from_admin_server(self) -> None: conn = http.client.HTTPConnection(reg_url_split.hostname, reg_url_split.port) conn.request( "DELETE", - reg_url_split.path) + reg_url_split.path, + headers=headers, + ) response = conn.getresponse() if response.status < 200 or response.status >= 300: @@ -111,6 +126,13 @@ def deregister_from_admin_server(self) -> None: if conn: conn.close() + def authenticate(self, headers: Dict) -> None: + if isinstance(self.registration_auth, BasicAuth): + password = self.registration_auth.password if self.registration_auth.password else "" + authorization_string = self.registration_auth.username + ":" + password + encoded_authorization: str = b64encode(bytes(authorization_string, "utf-8")).decode("ascii") + headers["Authorization"] = f"Basic {encoded_authorization}" + def start(self) -> None: logging.info("Starting recurring registration of %s with %s", self.pyctuator_base_url, self.registration_url) diff --git a/pyctuator/pyctuator.py b/pyctuator/pyctuator.py index 5900a60..302baee 100644 --- a/pyctuator/pyctuator.py +++ b/pyctuator/pyctuator.py @@ -9,6 +9,7 @@ # For example, if the webapp is a Flask webapp, we do not want to import FastAPI, and vice versa. # To do that, all imports are in conditional branches after detecting which frameworks are installed. # DO NOT add any web-framework-dependent imports to the global scope. +from pyctuator.auth import Auth from pyctuator.environment.custom_environment_provider import CustomEnvironmentProvider from pyctuator.environment.os_env_variables_impl import OsEnvironmentVariableProvider from pyctuator.health.diskspace_health_impl import DiskSpaceHealthProvider @@ -30,6 +31,7 @@ def __init__( app_url: str, pyctuator_endpoint_url: str, registration_url: Optional[str], + registration_auth: Optional[Auth] = None, app_description: Optional[str] = None, registration_interval_sec: int = 10, free_disk_space_down_threshold_bytes: int = 1024 * 1024 * 100, @@ -59,6 +61,7 @@ def __init__( registering the application with spring-boot-admin, must be accessible from spring-boot-admin server (i.e. don't use http://localhost:8080/... unless spring-boot-admin is running on the same host as the monitored application) :param registration_url: the spring-boot-admin endpoint to which registration requests must be posted + :param registration_auth: optional authentication details to use when registering with spring-boot-admin :param registration_interval_sec: how often pyctuator will renew its registration with spring-boot-admin :param free_disk_space_down_threshold_bytes: amount of free space in bytes in "./" (the application's current working directory) below which the built-in disk-space health-indicator will fail @@ -100,6 +103,7 @@ def __init__( if registration_url is not None: self.boot_admin_registration_handler = BootAdminRegistrationHandler( registration_url, + registration_auth, app_name, self.pyctuator_impl.pyctuator_endpoint_url, start_time, diff --git a/pyproject.toml b/pyproject.toml index 6380dda..913551b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyctuator" -version = "0.9" +version = "0.10" description = "A Python implementation of the Spring Actuator API for popular web frameworks" authors = [ "Michael Yakobi ",