From 5109de5c9ad21129156fefb6ebf72af0cb1517c4 Mon Sep 17 00:00:00 2001 From: Alexandr Bogomyakov Date: Mon, 18 Mar 2024 18:53:38 +0200 Subject: [PATCH] k8s discovery works --- .github/workflows/main.yml | 0 .gitignore | 3 +- .idea/.gitignore | 0 .idea/OutlineFleet.iml | 0 .../inspectionProfiles/profiles_settings.xml | 0 .idea/misc.xml | 0 .idea/modules.xml | 0 .idea/vcs.xml | 0 .vscode/extensions.json | 0 Dockerfile | 0 LICENSE | 0 README.md | 0 buildx.yaml | 0 img/servers.png | Bin k8s.py | 47 ++++++++- lib.py | 45 ++++++++- main.py | 94 +++--------------- requirements.txt | 0 static/layout.css | 0 static/pure.css | 0 templates/base.html | 0 templates/clients.html | 0 templates/index.html | 0 templates/sync.html | 0 24 files changed, 101 insertions(+), 88 deletions(-) mode change 100644 => 100755 .github/workflows/main.yml mode change 100644 => 100755 .gitignore mode change 100644 => 100755 .idea/.gitignore mode change 100644 => 100755 .idea/OutlineFleet.iml mode change 100644 => 100755 .idea/inspectionProfiles/profiles_settings.xml mode change 100644 => 100755 .idea/misc.xml mode change 100644 => 100755 .idea/modules.xml mode change 100644 => 100755 .idea/vcs.xml mode change 100644 => 100755 .vscode/extensions.json mode change 100644 => 100755 Dockerfile mode change 100644 => 100755 LICENSE mode change 100644 => 100755 README.md mode change 100644 => 100755 buildx.yaml mode change 100644 => 100755 img/servers.png mode change 100644 => 100755 k8s.py mode change 100644 => 100755 lib.py mode change 100644 => 100755 main.py mode change 100644 => 100755 requirements.txt mode change 100644 => 100755 static/layout.css mode change 100644 => 100755 static/pure.css mode change 100644 => 100755 templates/base.html mode change 100644 => 100755 templates/clients.html mode change 100644 => 100755 templates/index.html mode change 100644 => 100755 templates/sync.html diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index a51b144..105f280 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ config.yaml __pycache__/ sync.log main.py -.vscode/launch.json +.idea/* +.vscode/* *.swp *.swo *.swn diff --git a/.idea/.gitignore b/.idea/.gitignore old mode 100644 new mode 100755 diff --git a/.idea/OutlineFleet.iml b/.idea/OutlineFleet.iml old mode 100644 new mode 100755 diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml old mode 100644 new mode 100755 diff --git a/.idea/misc.xml b/.idea/misc.xml old mode 100644 new mode 100755 diff --git a/.idea/modules.xml b/.idea/modules.xml old mode 100644 new mode 100755 diff --git a/.idea/vcs.xml b/.idea/vcs.xml old mode 100644 new mode 100755 diff --git a/.vscode/extensions.json b/.vscode/extensions.json old mode 100644 new mode 100755 diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/buildx.yaml b/buildx.yaml old mode 100644 new mode 100755 diff --git a/img/servers.png b/img/servers.png old mode 100644 new mode 100755 diff --git a/k8s.py b/k8s.py old mode 100644 new mode 100755 index da263c7..743d489 --- a/k8s.py +++ b/k8s.py @@ -1,5 +1,6 @@ import base64 import json +import yaml import logging from kubernetes import client, config from kubernetes.client.rest import ApiException @@ -19,16 +20,58 @@ formatter = logging.Formatter( file_handler.setFormatter(formatter) log.addHandler(file_handler) +def write_config(config): + config_map = client.V1ConfigMap( + api_version="v1", + kind="ConfigMap", + metadata=client.V1ObjectMeta( + name=f"config-outfleet", + labels={ + "app": "outfleet", + } + ), + data={"config.yaml": yaml.dump(config)} + ) + try: + api_response = v1.create_namespaced_config_map( + namespace=NAMESPACE, + body=config_map, + ) + except ApiException as e: + api_response = v1.patch_namespaced_config_map( + name="config-outfleet", + namespace=NAMESPACE, + body=config_map, + ) + config.load_incluster_config() v1 = client.CoreV1Api() -NAMESPACE = "" +NAMESPACE = False +SERVERS = list() +CONFIG = None log.info("Checking for Kubernetes environment") try: with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace") as f: NAMESPACE = f.read().strip() + log.info(f"Found Kubernetes environment. Namespace {NAMESPACE}") except IOError: log.info("Kubernetes environment not detected") - pass \ No newline at end of file + pass + +# config = v1.list_namespaced_config_map(NAMESPACE, label_selector="app=outfleet").items["data"]["config.yaml"] +try: + CONFIG = yaml.safe_load(v1.read_namespaced_config_map(name="config-outfleet", namespace=NAMESPACE).data['config.yaml']) + log.info(f"ConfigMap config.yaml loaded from Kubernetes API. Servers: {len(CONFIG['servers'])}, Clients: {len(CONFIG['clients'])}") +except ApiException as e: + log.warning(f"ConfigMap not found. Fisrt run?") + +#servers = v1.list_namespaced_secret(NAMESPACE, label_selector="app=shadowbox") + +if not CONFIG: + log.info(f"Creating new ConfigMap [config-outfleet]") + write_config({"clients": [], "servers": [], "ui_hostname": "accessible-address.com"}) + CONFIG = yaml.safe_load(v1.read_namespaced_config_map(name="config-outfleet", namespace=NAMESPACE).data['config.yaml']) + diff --git a/lib.py b/lib.py old mode 100644 new mode 100755 index 48eab65..47e1054 --- a/lib.py +++ b/lib.py @@ -1,7 +1,10 @@ +import argparse import logging from typing import TypedDict, List from outline_vpn.outline_vpn import OutlineKey, OutlineVPN import yaml +import k8s + logging.basicConfig( level=logging.INFO, @@ -9,6 +12,42 @@ logging.basicConfig( datefmt="%d-%m-%Y %H:%M:%S", ) +log = logging.getLogger(f'OutFleet.lib') +parser = argparse.ArgumentParser() +parser.add_argument( + "-c", + "--config", + default="/usr/local/etc/outfleet/config.yaml", + help="Config file location", +) + +args = parser.parse_args() +def get_config(): + if not k8s.NAMESPACE: + try: + with open(args.config, "r") as file: + config = yaml.safe_load(file) + except: + try: + with open(args.config, "w"): + pass + except Exception as exp: + log.error(f"Couldn't create config. {exp}") + return None + return config + else: + return k8s.CONFIG + +def write_config(config): + if not k8s.NAMESPACE: + try: + with open(args.config, "w") as file: + yaml.safe_dump(config, file) + except Exception as e: + log.error(f"Couldn't write Outfleet config: {e}") + else: + k8s.write_config(config) + class ServerDict(TypedDict): server_id: str @@ -114,13 +153,11 @@ class Server: config.get("hostname_for_access_keys"), ) if config.get("comment"): - with open(CFG_PATH, "r") as file: - config_file = yaml.safe_load(file) or {} + config_file = get_config() config_file["servers"][self.data["local_server_id"]]["comment"] = config.get( "comment" ) - with open(CFG_PATH, "w") as file: - yaml.safe_dump(config_file, file) + write_config(config_file) self.log.info( "Changed %s comment to '%s'", self.data["local_server_id"], diff --git a/main.py b/main.py old mode 100644 new mode 100755 index 88135a5..bec2e8c --- a/main.py +++ b/main.py @@ -10,7 +10,7 @@ import uuid import k8s from flask import Flask, render_template, request, url_for, redirect from flask_cors import CORS -from lib import Server +from lib import Server, write_config, get_config, args logging.getLogger("werkzeug").setLevel(logging.ERROR) @@ -30,22 +30,9 @@ formatter = logging.Formatter( file_handler.setFormatter(formatter) log.addHandler(file_handler) -parser = argparse.ArgumentParser() -parser.add_argument( - "-c", - "--config", - default="/usr/local/etc/outfleet/config.yaml", - help="Config file location", -) -parser.add_argument( - "--k8s", - default=False, - action="store_true", - help="Kubernetes Outline server discovery", -) -args = parser.parse_args() -CFG_PATH = args.config +CFG_PATH = args.config +NAMESPACE = k8s.NAMESPACE SERVERS = list() BROKEN_SERVERS = list() CLIENTS = dict() @@ -64,20 +51,6 @@ def random_string(length=64): return "".join(random.choice(letters) for i in range(length)) -def get_config(): - if not args.k8s: - try: - with open(CFG_PATH, "r") as file: - config = yaml.safe_load(file) - except: - try: - with open(CFG_PATH, "w"): - pass - except Exception as exp: - log.error(f"Couldn't create config. {exp}") - else: - pass - def update_state(): @@ -89,16 +62,8 @@ def update_state(): SERVERS = list() BROKEN_SERVERS = list() CLIENTS = dict() - config = dict() - try: - with open(CFG_PATH, "r") as file: - config = yaml.safe_load(file) - except: - try: - with open(CFG_PATH, "w"): - pass - except Exception as exp: - log.error(f"Couldn't create config. {exp}") + config = get_config() + if config: HOSTNAME = config.get("ui_hostname", "my-own-SSL-ENABLED-domain.com") @@ -164,13 +129,6 @@ def index(): @app.route("/clients", methods=["GET", "POST"]) def clients(): - # {% for server in SERVERS %} - # {% for key in server.data["keys"] %} - # {% if key.name == client['name'] %} - # ssconf://{{ dynamic_hostname }}/dynamic/{{server.info()['name']}}/{{selected_client}}#{{server.info()['comment']}} - # {% endif %} - # {% endfor %} - # {% endfor %} if request.method == "GET": return render_template( "clients.html", @@ -184,22 +142,13 @@ def clients(): format_timestamp=format_timestamp, dynamic_hostname=HOSTNAME, ) - # else: - # server = request.form['server_id'] - # server = next((item for item in SERVERS if item.info()["server_id"] == server), None) - # server.apply_config(request.form) - # update_state() - # return redirect( - # url_for('index', nt="Updated Outline VPN Server", selected_server=request.args.get('selected_server'))) @app.route("/add_server", methods=["POST"]) def add_server(): if request.method == "POST": try: - with open(CFG_PATH, "r") as file: - config = yaml.safe_load(file) or {} - + config = get_config() servers = config.get("servers", dict()) local_server_id = str(uuid.uuid4()) @@ -217,16 +166,7 @@ def add_server(): "cert": request.form["cert"], } config["servers"] = servers - try: - with open(CFG_PATH, "w") as file: - yaml.safe_dump(config, file) - except Exception as e: - return redirect( - url_for( - "index", nt=f"Couldn't write Outfleet config: {e}", nl="error" - ) - ) - + write_config(config) log.info("Added server: %s", new_server.data["name"]) update_state() return redirect(url_for("index", nt="Added Outline VPN Server")) @@ -240,8 +180,7 @@ def add_server(): @app.route("/del_server", methods=["POST"]) def del_server(): if request.method == "POST": - with open(CFG_PATH, "r") as file: - config = yaml.safe_load(file) or {} + config = get_config() local_server_id = request.form.get("local_server_id") server_name = None @@ -254,9 +193,7 @@ def del_server(): client_config["servers"].remove(local_server_id) except ValueError as e: pass - - with open(CFG_PATH, "w") as file: - yaml.safe_dump(config, file) + write_config(config) log.info("Deleting server %s [%s]", server_name, request.form.get("local_server_id")) update_state() return redirect(url_for("index", nt=f"Server {server_name} has been deleted")) @@ -265,8 +202,7 @@ def del_server(): @app.route("/add_client", methods=["POST"]) def add_client(): if request.method == "POST": - with open(CFG_PATH, "r") as file: - config = yaml.safe_load(file) or {} + config = get_config() clients = config.get("clients", dict()) user_id = request.form.get("user_id", random_string()) @@ -277,8 +213,7 @@ def add_client(): "servers": request.form.getlist("servers"), } config["clients"] = clients - with open(CFG_PATH, "w") as file: - yaml.safe_dump(config, file) + write_config(config) log.info("Client %s updated", request.form.get("name")) for server in SERVERS: @@ -340,9 +275,7 @@ def add_client(): @app.route("/del_client", methods=["POST"]) def del_client(): if request.method == "POST": - with open(CFG_PATH, "r") as file: - config = yaml.safe_load(file) or {} - + config = get_config() clients = config.get("clients", dict()) user_id = request.form.get("user_id") if user_id in clients: @@ -359,8 +292,7 @@ def del_client(): server.delete_key(client.key_id) config["clients"].pop(user_id) - with open(CFG_PATH, "w") as file: - yaml.safe_dump(config, file) + write_config(config) log.info("Deleting client %s", request.form.get("name")) update_state() return redirect(url_for("clients", nt="User has been deleted")) diff --git a/requirements.txt b/requirements.txt old mode 100644 new mode 100755 diff --git a/static/layout.css b/static/layout.css old mode 100644 new mode 100755 diff --git a/static/pure.css b/static/pure.css old mode 100644 new mode 100755 diff --git a/templates/base.html b/templates/base.html old mode 100644 new mode 100755 diff --git a/templates/clients.html b/templates/clients.html old mode 100644 new mode 100755 diff --git a/templates/index.html b/templates/index.html old mode 100644 new mode 100755 diff --git a/templates/sync.html b/templates/sync.html old mode 100644 new mode 100755