k8s discovery works

This commit is contained in:
2024-03-18 18:53:38 +02:00
parent 8c05d324d3
commit 5109de5c9a
24 changed files with 101 additions and 88 deletions

0
.github/workflows/main.yml vendored Normal file → Executable file
View File

3
.gitignore vendored Normal file → Executable file
View File

@ -2,7 +2,8 @@ config.yaml
__pycache__/ __pycache__/
sync.log sync.log
main.py main.py
.vscode/launch.json .idea/*
.vscode/*
*.swp *.swp
*.swo *.swo
*.swn *.swn

0
.idea/.gitignore generated vendored Normal file → Executable file
View File

0
.idea/OutlineFleet.iml generated Normal file → Executable file
View File

0
.idea/inspectionProfiles/profiles_settings.xml generated Normal file → Executable file
View File

0
.idea/misc.xml generated Normal file → Executable file
View File

0
.idea/modules.xml generated Normal file → Executable file
View File

0
.idea/vcs.xml generated Normal file → Executable file
View File

0
.vscode/extensions.json vendored Normal file → Executable file
View File

0
Dockerfile Normal file → Executable file
View File

0
LICENSE Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

0
buildx.yaml Normal file → Executable file
View File

0
img/servers.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

47
k8s.py Normal file → Executable file
View File

@ -1,5 +1,6 @@
import base64 import base64
import json import json
import yaml
import logging import logging
from kubernetes import client, config from kubernetes import client, config
from kubernetes.client.rest import ApiException from kubernetes.client.rest import ApiException
@ -19,16 +20,58 @@ formatter = logging.Formatter(
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
log.addHandler(file_handler) 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() config.load_incluster_config()
v1 = client.CoreV1Api() v1 = client.CoreV1Api()
NAMESPACE = "" NAMESPACE = False
SERVERS = list()
CONFIG = None
log.info("Checking for Kubernetes environment") log.info("Checking for Kubernetes environment")
try: try:
with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace") as f: with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace") as f:
NAMESPACE = f.read().strip() NAMESPACE = f.read().strip()
log.info(f"Found Kubernetes environment. Namespace {NAMESPACE}")
except IOError: except IOError:
log.info("Kubernetes environment not detected") log.info("Kubernetes environment not detected")
pass 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'])

45
lib.py Normal file → Executable file
View File

@ -1,7 +1,10 @@
import argparse
import logging import logging
from typing import TypedDict, List from typing import TypedDict, List
from outline_vpn.outline_vpn import OutlineKey, OutlineVPN from outline_vpn.outline_vpn import OutlineKey, OutlineVPN
import yaml import yaml
import k8s
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
@ -9,6 +12,42 @@ logging.basicConfig(
datefmt="%d-%m-%Y %H:%M:%S", 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): class ServerDict(TypedDict):
server_id: str server_id: str
@ -114,13 +153,11 @@ class Server:
config.get("hostname_for_access_keys"), config.get("hostname_for_access_keys"),
) )
if config.get("comment"): if config.get("comment"):
with open(CFG_PATH, "r") as file: config_file = get_config()
config_file = yaml.safe_load(file) or {}
config_file["servers"][self.data["local_server_id"]]["comment"] = config.get( config_file["servers"][self.data["local_server_id"]]["comment"] = config.get(
"comment" "comment"
) )
with open(CFG_PATH, "w") as file: write_config(config_file)
yaml.safe_dump(config_file, file)
self.log.info( self.log.info(
"Changed %s comment to '%s'", "Changed %s comment to '%s'",
self.data["local_server_id"], self.data["local_server_id"],

94
main.py Normal file → Executable file
View File

@ -10,7 +10,7 @@ import uuid
import k8s import k8s
from flask import Flask, render_template, request, url_for, redirect from flask import Flask, render_template, request, url_for, redirect
from flask_cors import CORS from flask_cors import CORS
from lib import Server from lib import Server, write_config, get_config, args
logging.getLogger("werkzeug").setLevel(logging.ERROR) logging.getLogger("werkzeug").setLevel(logging.ERROR)
@ -30,22 +30,9 @@ formatter = logging.Formatter(
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
log.addHandler(file_handler) 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() SERVERS = list()
BROKEN_SERVERS = list() BROKEN_SERVERS = list()
CLIENTS = dict() CLIENTS = dict()
@ -64,20 +51,6 @@ def random_string(length=64):
return "".join(random.choice(letters) for i in range(length)) 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(): def update_state():
@ -89,16 +62,8 @@ def update_state():
SERVERS = list() SERVERS = list()
BROKEN_SERVERS = list() BROKEN_SERVERS = list()
CLIENTS = dict() CLIENTS = dict()
config = dict() config = get_config()
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}")
if config: if config:
HOSTNAME = config.get("ui_hostname", "my-own-SSL-ENABLED-domain.com") HOSTNAME = config.get("ui_hostname", "my-own-SSL-ENABLED-domain.com")
@ -164,13 +129,6 @@ def index():
@app.route("/clients", methods=["GET", "POST"]) @app.route("/clients", methods=["GET", "POST"])
def clients(): 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": if request.method == "GET":
return render_template( return render_template(
"clients.html", "clients.html",
@ -184,22 +142,13 @@ def clients():
format_timestamp=format_timestamp, format_timestamp=format_timestamp,
dynamic_hostname=HOSTNAME, 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"]) @app.route("/add_server", methods=["POST"])
def add_server(): def add_server():
if request.method == "POST": if request.method == "POST":
try: try:
with open(CFG_PATH, "r") as file: config = get_config()
config = yaml.safe_load(file) or {}
servers = config.get("servers", dict()) servers = config.get("servers", dict())
local_server_id = str(uuid.uuid4()) local_server_id = str(uuid.uuid4())
@ -217,16 +166,7 @@ def add_server():
"cert": request.form["cert"], "cert": request.form["cert"],
} }
config["servers"] = servers config["servers"] = servers
try: write_config(config)
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"
)
)
log.info("Added server: %s", new_server.data["name"]) log.info("Added server: %s", new_server.data["name"])
update_state() update_state()
return redirect(url_for("index", nt="Added Outline VPN Server")) return redirect(url_for("index", nt="Added Outline VPN Server"))
@ -240,8 +180,7 @@ def add_server():
@app.route("/del_server", methods=["POST"]) @app.route("/del_server", methods=["POST"])
def del_server(): def del_server():
if request.method == "POST": if request.method == "POST":
with open(CFG_PATH, "r") as file: config = get_config()
config = yaml.safe_load(file) or {}
local_server_id = request.form.get("local_server_id") local_server_id = request.form.get("local_server_id")
server_name = None server_name = None
@ -254,9 +193,7 @@ def del_server():
client_config["servers"].remove(local_server_id) client_config["servers"].remove(local_server_id)
except ValueError as e: except ValueError as e:
pass pass
write_config(config)
with open(CFG_PATH, "w") as file:
yaml.safe_dump(config, file)
log.info("Deleting server %s [%s]", server_name, request.form.get("local_server_id")) log.info("Deleting server %s [%s]", server_name, request.form.get("local_server_id"))
update_state() update_state()
return redirect(url_for("index", nt=f"Server {server_name} has been deleted")) 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"]) @app.route("/add_client", methods=["POST"])
def add_client(): def add_client():
if request.method == "POST": if request.method == "POST":
with open(CFG_PATH, "r") as file: config = get_config()
config = yaml.safe_load(file) or {}
clients = config.get("clients", dict()) clients = config.get("clients", dict())
user_id = request.form.get("user_id", random_string()) user_id = request.form.get("user_id", random_string())
@ -277,8 +213,7 @@ def add_client():
"servers": request.form.getlist("servers"), "servers": request.form.getlist("servers"),
} }
config["clients"] = clients config["clients"] = clients
with open(CFG_PATH, "w") as file: write_config(config)
yaml.safe_dump(config, file)
log.info("Client %s updated", request.form.get("name")) log.info("Client %s updated", request.form.get("name"))
for server in SERVERS: for server in SERVERS:
@ -340,9 +275,7 @@ def add_client():
@app.route("/del_client", methods=["POST"]) @app.route("/del_client", methods=["POST"])
def del_client(): def del_client():
if request.method == "POST": if request.method == "POST":
with open(CFG_PATH, "r") as file: config = get_config()
config = yaml.safe_load(file) or {}
clients = config.get("clients", dict()) clients = config.get("clients", dict())
user_id = request.form.get("user_id") user_id = request.form.get("user_id")
if user_id in clients: if user_id in clients:
@ -359,8 +292,7 @@ def del_client():
server.delete_key(client.key_id) server.delete_key(client.key_id)
config["clients"].pop(user_id) config["clients"].pop(user_id)
with open(CFG_PATH, "w") as file: write_config(config)
yaml.safe_dump(config, file)
log.info("Deleting client %s", request.form.get("name")) log.info("Deleting client %s", request.form.get("name"))
update_state() update_state()
return redirect(url_for("clients", nt="User has been deleted")) return redirect(url_for("clients", nt="User has been deleted"))

0
requirements.txt Normal file → Executable file
View File

0
static/layout.css Normal file → Executable file
View File

0
static/pure.css Normal file → Executable file
View File

0
templates/base.html Normal file → Executable file
View File

0
templates/clients.html Normal file → Executable file
View File

0
templates/index.html Normal file → Executable file
View File

0
templates/sync.html Normal file → Executable file
View File