18 Commits
k8s ... new-ui

Author SHA1 Message Date
c7a41e6a2f Init api 2024-03-27 20:03:34 +02:00
Alexandr Bogomyakov
f6bcb42ec4 Bump version 2024-03-24 18:07:23 +02:00
Alexandr Bogomyakov
bae0b91bab Merge pull request #7 from Sanapach/master
Outfleet::log fixing
2024-03-24 18:04:18 +02:00
mmilavkin
ab6d53a837 Outfleet::log fixing::2 2024-03-24 18:02:51 +02:00
mmilavkin
9709d2f029 Outfleet::log fixing 2024-03-24 17:58:54 +02:00
e818d63cad fix k8s things 2024-03-19 02:47:29 +02:00
2039654f12 fix k8s things 2024-03-19 01:44:38 +02:00
f82631b174 fix k8s things 2024-03-19 01:11:08 +02:00
77b78ec751 fix k8s things 2024-03-18 22:53:43 +02:00
f6a728ef1a fix k8s things 2024-03-18 22:08:43 +02:00
5c1ffcbdc3 fix k8s things 2024-03-18 21:30:34 +02:00
f6c3262fb8 fix V1 api define 2024-03-18 20:42:14 +02:00
607730e781 fix V1 api define 2024-03-18 20:36:07 +02:00
0e7cabe336 fix V1 api define 2024-03-18 20:02:33 +02:00
614140840d Fix k8s exception 2024-03-18 19:50:24 +02:00
Alexandr Bogomyakov
3a6a60032e Update README.md 2024-03-18 19:39:57 +02:00
263acf540d Fix docker build 2024-03-18 19:21:25 +02:00
Alexandr Bogomyakov
443198aad1 Merge pull request #6 from house-of-vanity/k8s
K8s
2024-03-18 19:12:05 +02:00
6 changed files with 304 additions and 214 deletions

View File

@@ -5,8 +5,7 @@ WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
COPY static static COPY static static
COPY templates templates COPY templates templates
COPY main.py . COPY *.py .
COPY lib.py .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt

View File

@@ -86,4 +86,6 @@ Keep in mind that all user keys are stored in a single **config.yaml** file. If
## Authors ## Authors
* **UltraDesu** - *Humble amateur developer* - [UltraDesu](https://github.com/house-of-vanity) - *All the work* * **UltraDesu** - *Humble amateur developer* - [UltraDesu](https://github.com/house-of-vanity) - *Author*
* **Contributors**
* * @Sanapach

127
k8s.py
View File

@@ -1,24 +1,67 @@
import base64 import base64
import json import json
import uuid
import yaml import yaml
import logging import logging
from kubernetes import client, config import threading
import time
import lib
from kubernetes import client, config as kube_config
from kubernetes.client.rest import ApiException from kubernetes.client.rest import ApiException
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%d-%m-%Y %H:%M:%S",
)
log = logging.getLogger("OutFleet.k8s") log = logging.getLogger("OutFleet.k8s")
file_handler = logging.FileHandler("sync.log")
file_handler.setLevel(logging.DEBUG) NAMESPACE = False
formatter = logging.Formatter( SERVERS = list()
"%(asctime)s - %(name)s - %(levelname)s - %(message)s" CONFIG = None
) V1 = None
file_handler.setFormatter(formatter) K8S_DETECTED = False
log.addHandler(file_handler)
def discovery_servers():
global CONFIG
interval = 60
log = logging.getLogger("OutFleet.discovery")
if K8S_DETECTED:
while True:
pods = V1.list_namespaced_pod(NAMESPACE, label_selector="app=shadowbox")
log.debug(f"Started discovery thread every {interval}")
for pod in pods.items:
log.debug(f"Found Outline server pod {pod.metadata.name}")
container_log = V1.read_namespaced_pod_log(name=pod.metadata.name, namespace=NAMESPACE, container='manager-config-json')
secret = json.loads(container_log.replace('\'', '\"'))
config = lib.get_config()
config_servers = find_server(secret, config["servers"])
#log.info(f"config_servers {config_servers}")
if len(config_servers) > 0:
log.debug(f"Already exist")
pass
else:
with lib.lock:
config["servers"][str(uuid.uuid4())] = {
"cert": secret["certSha256"],
"name": f"{pod.metadata.name}",
"comment": f"{pod.spec.node_name}",
"url": secret["apiUrl"],
}
write_config(config)
log.info(f"Added discovered server")
time.sleep(interval)
def find_server(search_data, servers):
found_servers = {}
for server_id, server_info in servers.items():
if server_info["url"] == search_data["apiUrl"] and server_info["cert"] == search_data["certSha256"]:
found_servers[server_id] = server_info
return found_servers
def write_config(config): def write_config(config):
config_map = client.V1ConfigMap( config_map = client.V1ConfigMap(
@@ -33,45 +76,53 @@ def write_config(config):
data={"config.yaml": yaml.dump(config)} data={"config.yaml": yaml.dump(config)}
) )
try: try:
api_response = v1.create_namespaced_config_map( api_response = V1.create_namespaced_config_map(
namespace=NAMESPACE, namespace=NAMESPACE,
body=config_map, body=config_map,
) )
except ApiException as e: except ApiException as e:
api_response = v1.patch_namespaced_config_map( api_response = V1.patch_namespaced_config_map(
name="config-outfleet", name="config-outfleet",
namespace=NAMESPACE, namespace=NAMESPACE,
body=config_map, body=config_map,
) )
log.info("Updated config in Kubernetes ConfigMap [config-outfleet]")
config.load_incluster_config()
v1 = client.CoreV1Api() def reload_config():
global CONFIG
while True:
new_config = yaml.safe_load(V1.read_namespaced_config_map(name="config-outfleet", namespace=NAMESPACE).data['config.yaml'])
with lib.lock:
CONFIG = new_config
log.debug(f"Synced system config with ConfigMap [config-outfleet].")
time.sleep(30)
NAMESPACE = False
SERVERS = list()
CONFIG = None
log.info("Checking for Kubernetes environment") try:
kube_config.load_incluster_config()
V1 = client.CoreV1Api()
if V1 != None:
K8S_DETECTED = True
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}") log.info(f"Found Kubernetes environment. Deployed to 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: try:
CONFIG = yaml.safe_load(v1.read_namespaced_config_map(name="config-outfleet", namespace=NAMESPACE).data['config.yaml']) 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'])}") log.info(f"ConfigMap loaded from Kubernetes API. Servers: {len(CONFIG['servers'])}, Clients: {len(CONFIG['clients'])}. Started monitoring for changes every minute.")
except ApiException as e: except Exception as e:
log.warning(f"ConfigMap not found. Fisrt run?") try:
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'])
log.info("Created new ConfigMap [config-outfleet]. Started monitoring for changes every minute.")
except Exception as e:
log.info(f"Failed to create new ConfigMap [config-outfleet] {e}")
thread = threading.Thread(target=reload_config)
thread.start()
#servers = v1.list_namespaced_secret(NAMESPACE, label_selector="app=shadowbox") except:
log.info("Kubernetes environment not detected")
if not CONFIG: except:
log.info(f"Creating new ConfigMap [config-outfleet]") log.info("Kubernetes environment not detected")
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'])

27
lib.py
View File

@@ -1,5 +1,6 @@
import argparse import argparse
import logging import logging
import threading
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
@@ -21,9 +22,14 @@ parser.add_argument(
help="Config file location", help="Config file location",
) )
lock = threading.Lock()
args = parser.parse_args() args = parser.parse_args()
def get_config(): def get_config():
if not k8s.NAMESPACE: if k8s.CONFIG:
return k8s.CONFIG
else:
try: try:
with open(args.config, "r") as file: with open(args.config, "r") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
@@ -35,18 +41,16 @@ def get_config():
log.error(f"Couldn't create config. {exp}") log.error(f"Couldn't create config. {exp}")
return None return None
return config return config
else:
return k8s.CONFIG
def write_config(config): def write_config(config):
if not k8s.NAMESPACE: if k8s.CONFIG:
k8s.write_config(config)
else:
try: try:
with open(args.config, "w") as file: with open(args.config, "w") as file:
yaml.safe_dump(config, file) yaml.safe_dump(config, file)
except Exception as e: except Exception as e:
log.error(f"Couldn't write Outfleet config: {e}") log.error(f"Couldn't write Outfleet config: {e}")
else:
k8s.write_config(config)
class ServerDict(TypedDict): class ServerDict(TypedDict):
@@ -95,13 +99,6 @@ class Server:
"keys": self.client.get_keys(), "keys": self.client.get_keys(),
} }
self.log = logging.getLogger(f'OutFleet.server[{self.data["name"]}]') self.log = logging.getLogger(f'OutFleet.server[{self.data["name"]}]')
file_handler = logging.FileHandler("sync.log")
file_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
file_handler.setFormatter(formatter)
self.log.addHandler(file_handler)
def info(self) -> ServerDict: def info(self) -> ServerDict:
return self.data return self.data
@@ -110,7 +107,7 @@ class Server:
# Looking for any users with provided name. len(result) != 1 is a problem. # Looking for any users with provided name. len(result) != 1 is a problem.
result = [] result = []
for key in self.client.get_keys(): for key in self.client.get_keys():
if key.key_id == name: if key.name == name:
result.append(name) result.append(name)
self.log.info(f"check_client found client `{name}` config is correct.") self.log.info(f"check_client found client `{name}` config is correct.")
if len(result) != 1: if len(result) != 1:
@@ -121,7 +118,7 @@ class Server:
else: else:
return True return True
def apply_config(self, config, CFG_PATH): def apply_config(self, config):
if config.get("name"): if config.get("name"):
self.client.set_server_name(config.get("name")) self.client.set_server_name(config.get("name"))
self.log.info( self.log.info(

94
main.py
View File

@@ -1,16 +1,17 @@
import threading
import time
import yaml import yaml
import logging import logging
from datetime import datetime from datetime import datetime
import random import random
import string import string
import argparse
import uuid 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, jsonify
from flask_cors import CORS from flask_cors import CORS
from lib import Server, write_config, get_config, args from lib import Server, write_config, get_config, args, lock
logging.getLogger("werkzeug").setLevel(logging.ERROR) logging.getLogger("werkzeug").setLevel(logging.ERROR)
@@ -23,20 +24,18 @@ logging.basicConfig(
log = logging.getLogger("OutFleet") log = logging.getLogger("OutFleet")
file_handler = logging.FileHandler("sync.log") file_handler = logging.FileHandler("sync.log")
file_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter( formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s" "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
) )
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
log.addHandler(file_handler) log.addHandler(file_handler)
CFG_PATH = args.config CFG_PATH = args.config
NAMESPACE = k8s.NAMESPACE NAMESPACE = k8s.NAMESPACE
SERVERS = list() SERVERS = list()
BROKEN_SERVERS = list() BROKEN_SERVERS = list()
CLIENTS = dict() CLIENTS = dict()
VERSION = '3' VERSION = '5'
HOSTNAME = "" HOSTNAME = ""
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
@@ -53,31 +52,30 @@ def random_string(length=64):
def update_state(): def update_state(timer=40):
while True:
with lock:
global SERVERS global SERVERS
global CLIENTS global CLIENTS
global BROKEN_SERVERS global BROKEN_SERVERS
global HOSTNAME global HOSTNAME
SERVERS = list()
BROKEN_SERVERS = list()
CLIENTS = dict()
config = get_config() config = get_config()
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")
servers = config.get("servers", dict()) servers = config.get("servers", dict())
_SERVERS = list()
for local_server_id, server_config in servers.items(): for local_server_id, server_config in servers.items():
try: try:
server = Server( server = Server(
url=server_config["url"], url=server_config["url"],
cert=server_config["cert"], cert=server_config["cert"],
comment=server_config["comment"], comment=server_config.get("comment", ''),
local_server_id=local_server_id, local_server_id=local_server_id,
) )
SERVERS.append(server) _SERVERS.append(server)
log.info( log.debug(
"Server state updated: %s, [%s]", "Server state updated: %s, [%s]",
server.info()["name"], server.info()["name"],
local_server_id, local_server_id,
@@ -89,9 +87,11 @@ def update_state():
"id": local_server_id "id": local_server_id
}) })
log.warning("Can't access server: %s - %s", server_config["url"], e) log.warning("Can't access server: %s - %s", server_config["url"], e)
SERVERS = _SERVERS
CLIENTS = config.get("clients", dict()) CLIENTS = config.get("clients", dict())
if timer == 0:
break
time.sleep(40)
@app.route("/", methods=["GET", "POST"]) @app.route("/", methods=["GET", "POST"])
def index(): def index():
@@ -101,6 +101,7 @@ def index():
"index.html", "index.html",
SERVERS=SERVERS, SERVERS=SERVERS,
VERSION=VERSION, VERSION=VERSION,
K8S_NAMESPACE=k8s.NAMESPACE,
BROKEN_SERVERS=BROKEN_SERVERS, BROKEN_SERVERS=BROKEN_SERVERS,
nt=request.args.get("nt"), nt=request.args.get("nt"),
nl=request.args.get("nl"), nl=request.args.get("nl"),
@@ -114,8 +115,35 @@ def index():
server = next( server = next(
(item for item in SERVERS if item.info()["local_server_id"] == server), None (item for item in SERVERS if item.info()["local_server_id"] == server), None
) )
server.apply_config(request.form, CFG_PATH) server.apply_config(request.form)
update_state() update_state(timer=0)
return redirect(
url_for(
"index",
nt="Updated Outline VPN Server",
selected_server=request.args.get("selected_server"),
)
)
else:
return redirect(url_for("index"))
@app.route("/servers", methods=["GET", "POST"])
@app.route("/servers/<string:local_server_id>", methods=["GET", "POST", "DELETE"])
def servers(local_server_id=None):
if local_server_id:
log.info(f"Got {local_server_id} Config {get_config()}")
server = get_config()['servers'].get(local_server_id, None)
return jsonify(server)
if request.method == "GET":
return jsonify({ "servers": get_config()['servers']})
elif request.method == "POST":
server = request.form["server_id"]
server = next(
(item for item in SERVERS if item.info()["local_server_id"] == server), None
)
server.apply_config(request.form)
update_state(timer=0)
return redirect( return redirect(
url_for( url_for(
"index", "index",
@@ -135,6 +163,7 @@ def clients():
SERVERS=SERVERS, SERVERS=SERVERS,
CLIENTS=CLIENTS, CLIENTS=CLIENTS,
VERSION=VERSION, VERSION=VERSION,
K8S_NAMESPACE=k8s.NAMESPACE,
nt=request.args.get("nt"), nt=request.args.get("nt"),
nl=request.args.get("nl"), nl=request.args.get("nl"),
selected_client=request.args.get("selected_client"), selected_client=request.args.get("selected_client"),
@@ -168,7 +197,7 @@ def add_server():
config["servers"] = servers config["servers"] = servers
write_config(config) write_config(config)
log.info("Added server: %s", new_server.data["name"]) log.info("Added server: %s", new_server.data["name"])
update_state() update_state(timer=0)
return redirect(url_for("index", nt="Added Outline VPN Server")) return redirect(url_for("index", nt="Added Outline VPN Server"))
except Exception as e: except Exception as e:
return redirect( return redirect(
@@ -195,7 +224,7 @@ def del_server():
pass pass
write_config(config) write_config(config)
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(timer=0)
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"))
@@ -260,7 +289,7 @@ def add_client():
request.form.get("name"), request.form.get("name"),
server.data["name"], server.data["name"],
) )
update_state() update_state(timer=0)
return redirect( return redirect(
url_for( url_for(
"clients", "clients",
@@ -294,7 +323,7 @@ def del_client():
config["clients"].pop(user_id) config["clients"].pop(user_id)
write_config(config) write_config(config)
log.info("Deleting client %s", request.form.get("name")) log.info("Deleting client %s", request.form.get("name"))
update_state() update_state(timer=0)
return redirect(url_for("clients", nt="User has been deleted")) return redirect(url_for("clients", nt="User has been deleted"))
@@ -357,14 +386,7 @@ def sync():
lines=lines, lines=lines,
) )
if request.method == "POST": if request.method == "POST":
log = logging.getLogger("sync") with lock:
file_handler = logging.FileHandler("sync.log")
file_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
file_handler.setFormatter(formatter)
log.addHandler(file_handler)
if request.form.get("wipe") == 'all': if request.form.get("wipe") == 'all':
for server in SERVERS: for server in SERVERS:
log.info("Wiping all keys on [%s]", server.data["name"]) log.info("Wiping all keys on [%s]", server.data["name"])
@@ -372,8 +394,10 @@ def sync():
server.delete_key(client.key_id) server.delete_key(client.key_id)
server_hash = {} server_hash = {}
with lock:
for server in SERVERS: for server in SERVERS:
server_hash[server.data["local_server_id"]] = server server_hash[server.data["local_server_id"]] = server
with lock:
for key, client in CLIENTS.items(): for key, client in CLIENTS.items():
for u_server_id in client["servers"]: for u_server_id in client["servers"]:
if u_server_id in server_hash: if u_server_id in server_hash:
@@ -390,10 +414,14 @@ def sync():
log.info( log.info(
f"Client {client['name']} incorrect server_id {u_server_id}" f"Client {client['name']} incorrect server_id {u_server_id}"
) )
update_state() update_state(timer=0)
return redirect(url_for("sync")) return redirect(url_for("sync"))
if __name__ == "__main__": if __name__ == "__main__":
update_state() update_state_thread = threading.Thread(target=update_state)
update_state_thread.start()
discovery_servers_thread = threading.Thread(target=k8s.discovery_servers)
discovery_servers_thread.start()
app.run(host="0.0.0.0") app.run(host="0.0.0.0")

File diff suppressed because one or more lines are too long