mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-07-06 17:14:07 +00:00
k8s discovery works
This commit is contained in:
0
.github/workflows/main.yml
vendored
Normal file → Executable file
0
.github/workflows/main.yml
vendored
Normal file → Executable file
3
.gitignore
vendored
Normal file → Executable file
3
.gitignore
vendored
Normal file → Executable 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
0
.idea/.gitignore
generated
vendored
Normal file → Executable file
0
.idea/OutlineFleet.iml
generated
Normal file → Executable file
0
.idea/OutlineFleet.iml
generated
Normal file → Executable file
0
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file → Executable file
0
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file → Executable file
0
.idea/misc.xml
generated
Normal file → Executable file
0
.idea/misc.xml
generated
Normal file → Executable file
0
.idea/modules.xml
generated
Normal file → Executable file
0
.idea/modules.xml
generated
Normal file → Executable file
0
.idea/vcs.xml
generated
Normal file → Executable file
0
.idea/vcs.xml
generated
Normal file → Executable file
0
.vscode/extensions.json
vendored
Normal file → Executable file
0
.vscode/extensions.json
vendored
Normal file → Executable file
0
Dockerfile
Normal file → Executable file
0
Dockerfile
Normal file → Executable file
0
buildx.yaml
Normal file → Executable file
0
buildx.yaml
Normal file → Executable file
0
img/servers.png
Normal file → Executable file
0
img/servers.png
Normal file → Executable file
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
47
k8s.py
Normal file → Executable file
47
k8s.py
Normal file → Executable 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
45
lib.py
Normal file → Executable 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
94
main.py
Normal file → Executable 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
0
requirements.txt
Normal file → Executable file
0
static/layout.css
Normal file → Executable file
0
static/layout.css
Normal file → Executable file
0
static/pure.css
Normal file → Executable file
0
static/pure.css
Normal file → Executable file
0
templates/base.html
Normal file → Executable file
0
templates/base.html
Normal file → Executable file
0
templates/clients.html
Normal file → Executable file
0
templates/clients.html
Normal file → Executable file
0
templates/index.html
Normal file → Executable file
0
templates/index.html
Normal file → Executable file
0
templates/sync.html
Normal file → Executable file
0
templates/sync.html
Normal file → Executable file
Reference in New Issue
Block a user