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__/
sync.log
main.py
.vscode/launch.json
.idea/*
.vscode/*
*.swp
*.swo
*.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

45
k8s.py Normal file → Executable file
View File

@ -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
# 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
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"],

94
main.py Normal file → Executable file
View File

@ -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"))

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