Adapt everything so that the UI can work with every integration (some more tests are needed)

This commit is contained in:
TheophileDiot 2022-10-28 15:08:07 +02:00
parent 66fb266f8e
commit 5f8353c114
11 changed files with 816 additions and 185 deletions

View File

@ -86,7 +86,9 @@ class Database:
break
if not sqlalchemy_string:
sqlalchemy_string = getenv("DATABASE_URI", "sqlite:////data/db.sqlite3")
sqlalchemy_string = getenv(
"DATABASE_URI", "sqlite:////opt/bunkerweb/cache/db.sqlite3"
)
if sqlalchemy_string.startswith("sqlite"):
if not path.exists(sqlalchemy_string.split("///")[1]):
@ -290,24 +292,26 @@ class Database:
with self.__db_session() as session:
# Delete all the old config
session.execute(
Services.__table__.delete().where(Services.method == method)
Global_values.__table__.delete().where(Global_values.method == method)
)
session.execute(
Services_settings.__table__.delete().where(
Services_settings.method == method
)
)
session.execute(Global_values.__table__.delete())
if config:
if config["MULTISITE"] == "yes":
global_values = []
for server_name in config["SERVER_NAME"].split(" "):
if (
session.query(Services)
.with_entities(Services.id)
server_name
and session.query(Services)
.filter_by(id=server_name)
.first()
is None
):
continue
if server_name:
to_put.append(Services(id=server_name, method=method))
to_put.append(Services(id=server_name))
for key, value in deepcopy(config).items():
suffix = None
@ -315,35 +319,73 @@ class Database:
suffix = int(key.split("_")[-1])
key = key[: -len(str(suffix)) - 1]
setting = (
session.query(Settings)
.with_entities(Settings.default)
.filter_by(id=key.replace(f"{server_name}_", ""))
.first()
)
if server_name and key.startswith(server_name):
key = key.replace(f"{server_name}_", "")
to_put.append(
Services_settings(
service_setting = (
session.query(Services_settings)
.filter_by(
service_id=server_name,
setting_id=key,
value=value,
suffix=suffix,
)
)
elif key not in global_values:
setting = (
session.query(Settings)
.with_entities(Settings.default)
.filter_by(id=key)
.first()
)
if setting and value != setting.default:
global_values.append(key)
if not setting or (
value == setting.default and service_setting is None
):
continue
if service_setting is None:
to_put.append(
Global_values(
setting_id=key, value=value, suffix=suffix
Services_settings(
service_id=server_name,
setting_id=key,
value=value,
suffix=suffix,
method=method,
)
)
elif method == "autoconf":
service_setting.value = value
service_setting.method = method
to_put.append(service_setting)
elif key not in global_values:
global_values.append(key)
global_value = (
session.query(Global_values)
.filter_by(setting_id=key, suffix=suffix)
.first()
)
if not setting or (
value == setting.default and global_value is None
):
continue
if global_value is None:
to_put.append(
Global_values(
setting_id=key,
value=value,
suffix=suffix,
method=method,
)
)
elif method == "autoconf":
global_value.value = value
global_value.method = method
to_put.append(global_value)
else:
primary_server_name = config["SERVER_NAME"].split(" ")[0]
to_put.append(Services(id=primary_server_name, method=method))
to_put.append(Services(id=primary_server_name))
for key, value in config.items():
suffix = None
@ -351,12 +393,22 @@ class Database:
suffix = int(key.split("_")[-1])
key = key[: -len(str(suffix)) - 1]
setting = (
session.query(Settings)
.with_entities(Settings.default)
.filter_by(id=key)
.first()
)
if setting and value == setting.default:
continue
to_put.append(
Services_settings(
service_id=primary_server_name,
Global_values(
setting_id=key,
value=value,
suffix=suffix,
method=method,
)
)
@ -420,8 +472,20 @@ class Database:
"name": custom_config["exploded"][2],
}
)
to_put.append(Custom_configs(**config))
if (
method == "autoconf"
or session.query(Custom_configs)
.with_entities(Custom_configs.id)
.filter_by(
service_id=config.get("service_id", None),
type=config["type"],
name=config["name"],
)
.first()
is None
):
to_put.append(Custom_configs(**config))
try:
session.add_all(to_put)
session.commit()
@ -430,60 +494,76 @@ class Database:
return ""
def get_config(self) -> Dict[str, Any]:
def get_config(self, methods: bool = False) -> Dict[str, Any]:
"""Get the config from the database"""
with self.__db_session() as session:
config = {}
settings = (
session.query(Settings)
.with_entities(
Settings.id, Settings.context, Settings.default, Settings.multiple
)
.all()
)
for setting in settings:
suffix = 0
while True:
global_value = (
session.query(Global_values)
.with_entities(Global_values.value)
.filter_by(setting_id=setting.id, suffix=suffix)
.first()
)
if global_value is None:
if suffix > 0:
break
else:
config[setting.id] = setting.default
else:
config[
setting.id + (f"_{suffix}" if suffix > 0 else "")
] = global_value.value
if not setting.multiple:
break
suffix += 1
for service in session.query(Services).with_entities(Services.id).all():
for setting in settings:
if setting.context != "multisite":
continue
for setting in (
session.query(Settings)
.with_entities(
Settings.id,
Settings.context,
Settings.default,
Settings.multiple,
)
.all()
):
suffix = 0
while True:
global_value = (
session.query(Global_values)
.with_entities(Global_values.value, Global_values.method)
.filter_by(setting_id=setting.id, suffix=suffix)
.first()
)
if global_value is None:
if suffix == 0:
config[setting.id] = (
setting.default
if methods is False
else {"value": setting.default, "method": "default"}
)
else:
config[
setting.id + (f"_{suffix}" if suffix > 0 else "")
] = (
global_value.value
if methods is False
else {
"value": global_value.value,
"method": global_value.method,
}
)
if setting.context != "multisite":
break
if suffix == 0:
config[f"{service.id}_{setting.id}"] = config[setting.id]
config[f"{service.id}_{setting.id}"] = (
config[setting.id]
if methods is False
else {
"value": config[setting.id]["value"],
"method": "default",
}
)
elif f"{setting.id}_{suffix}" in config:
config[f"{service.id}_{setting.id}_{suffix}"] = config[
f"{setting.id}_{suffix}"
]
config[f"{service.id}_{setting.id}_{suffix}"] = (
config[f"{setting.id}_{suffix}"]
if methods is False
else {
"value": config[f"{setting.id}_{suffix}"]["value"],
"method": "default",
}
)
service_setting = (
session.query(Services_settings)
.with_entities(Services_settings.value)
.with_entities(
Services_settings.value, Services_settings.method
)
.filter_by(
service_id=service.id,
setting_id=setting.id,
@ -496,10 +576,20 @@ class Database:
config[
f"{service.id}_{setting.id}"
+ (f"_{suffix}" if suffix > 0 else "")
] = service_setting.value
] = (
service_setting.value
if methods is False
else {
"value": service_setting.value,
"method": service_setting.method,
}
)
elif suffix > 0:
break
if not setting.multiple:
break
suffix += 1
return config
@ -512,28 +602,35 @@ class Database:
"service_id": custom_config.service_id,
"type": custom_config.type,
"name": custom_config.name,
"data": custom_config.data.decode("utf-8"),
"data": custom_config.data,
"method": custom_config.method,
}
for custom_config in session.query(Custom_configs)
.with_entities(
Custom_configs.service_id,
Custom_configs.type,
Custom_configs.name,
Custom_configs.data,
Custom_configs.method,
for custom_config in (
session.query(Custom_configs)
.with_entities(
Custom_configs.service_id,
Custom_configs.type,
Custom_configs.name,
Custom_configs.data,
Custom_configs.method,
)
.all()
)
.all()
]
def get_services(self) -> List[Dict[str, Any]]:
def get_services_settings(self, methods: bool = False) -> List[Dict[str, Any]]:
"""Get the services' configs from the database"""
services = []
config = self.get_config(methods=methods)
with self.__db_session() as session:
for service in (
session.query(Services).with_entities(Services.settings).all()
):
services.append(service.settings)
for service in session.query(Services).with_entities(Services.id).all():
tmp_config = deepcopy(config)
for key, value in tmp_config.items():
if key.startswith(f"{service.id}_"):
tmp_config[key.replace(f"{service.id}_", "")] = value
services.append(tmp_config)
return services

View File

@ -96,6 +96,7 @@ class Global_values(Base):
)
value = Column(String(1023), nullable=False)
suffix = Column(SmallInteger, primary_key=True, nullable=True, default=0)
method = Column(METHODS_ENUM, nullable=False)
setting = relationship("Settings", back_populates="global_value")
@ -104,7 +105,6 @@ class Services(Base):
__tablename__ = "services"
id = Column(String(64), primary_key=True)
method = Column(METHODS_ENUM, nullable=False)
settings = relationship(
"Services_settings", back_populates="service", cascade="all, delete"
@ -132,6 +132,7 @@ class Services_settings(Base):
)
value = Column(String(1023), nullable=False)
suffix = Column(SmallInteger, primary_key=True, nullable=True, default=0)
method = Column(METHODS_ENUM, nullable=False)
service = relationship("Services", back_populates="settings")
setting = relationship("Settings", back_populates="services")

View File

@ -160,7 +160,7 @@ if __name__ == "__main__":
tmp_path += f"/{custom_config['service_id']}"
tmp_path += f"/{custom_config['name']}.conf"
makedirs(dirname(tmp_path), exist_ok=True)
with open(tmp_path, "w") as f:
with open(tmp_path, "wb") as f:
f.write(custom_config["data"])
if bw_integration != "Local":
@ -277,7 +277,7 @@ if __name__ == "__main__":
tmp_path += f"/{custom_config['service_id']}"
tmp_path += f"/{custom_config['name']}.conf"
makedirs(dirname(tmp_path), exist_ok=True)
with open(tmp_path, "w") as f:
with open(tmp_path, "wb") as f:
f.write(custom_config["data"])
if bw_integration != "Local":

View File

@ -1,32 +1,26 @@
FROM python:3.11-rc-alpine AS builder
# Copy python requirements
COPY bw/deps/requirements.txt /opt/bunkerweb/deps/requirements.txt
# Install python requirements
RUN apk add --no-cache --virtual build g++ gcc python3-dev musl-dev libffi-dev openssl-dev cargo && \
mkdir /opt/bunkerweb/deps/python && \
pip install --no-cache-dir --require-hashes --target /opt/bunkerweb/deps/python -r /opt/bunkerweb/deps/requirements.txt && \
apk del build
FROM python:3.11-rc-alpine
COPY --from=builder /opt/bunkerweb/deps/python /opt/bunkerweb/deps/python
# Copy files
# can't exclude specific files/dir from . so we are copying everything by hand
COPY bw/api /opt/bunkerweb/api
COPY bw/confs /opt/bunkerweb/confs
COPY bw/core /opt/bunkerweb/core
COPY bw/gen /opt/bunkerweb/gen
COPY utils /opt/bunkerweb/utils
COPY bw/settings.json /opt/bunkerweb/settings.json
COPY db /opt/bunkerweb/db
COPY utils /opt/bunkerweb/utils
COPY VERSION /opt/bunkerweb/VERSION
COPY ui/requirements.txt /opt/bunkerweb/ui/requirements.txt
# Install UI requirements
RUN apk add --no-cache --virtual build gcc python3-dev musl-dev libffi-dev openssl-dev cargo && \
pip install -r /opt/bunkerweb/ui/requirements.txt && \
# Copy python requirements
COPY ui/deps/requirements.txt /opt/bunkerweb/ui/deps/requirements.txt
# Install python requirements
RUN apk add --no-cache --virtual build py3-pip g++ gcc python3-dev musl-dev libffi-dev openssl-dev cargo && \
pip install --no-cache-dir --upgrade pip && \
pip install wheel && \
mkdir /opt/bunkerweb/ui/deps/python && \
pip install --no-cache-dir --require-hashes --target /opt/bunkerweb/ui/deps/python -r /opt/bunkerweb/ui/deps/requirements.txt && \
pip install --no-cache-dir gunicorn && \
apk del build
COPY ui /opt/bunkerweb/ui
@ -42,16 +36,13 @@ RUN apk add --no-cache bash file && \
find /opt/bunkerweb -type d -exec chmod 0750 {} \; && \
chown -R ui:ui /data && \
chmod 770 /opt/bunkerweb/tmp && \
chmod 750 /opt/bunkerweb/gen/main.py /opt/bunkerweb/deps/python/bin/* && \
mkdir /etc/nginx && \
chown -R ui:ui /etc/nginx && \
chmod -R 770 /etc/nginx && \
chmod 750 /opt/bunkerweb/gen/main.py /opt/bunkerweb/ui/deps/python/bin/* && \
ln -s /usr/local/bin/python /usr/bin/python3
# Fix CVEs
RUN apk add "libssl1.1>=1.1.1q-r0" "libcrypto1.1>=1.1.1q-r0" "git>=2.32.3-r0" "ncurses-libs>=6.2_p20210612-r1" "ncurses-terminfo-base>=6.2_p20210612-r1" "libtirpc>=1.3.2-r1" "libtirpc-conf>=1.3.2-r1" "zlib>=1.2.12-r2" "libxml2>=2.9.14-r1"
VOLUME /data /etc/nginx
VOLUME /data
EXPOSE 7000
@ -59,4 +50,4 @@ WORKDIR /opt/bunkerweb/ui
USER ui:ui
CMD ["gunicorn", "--bind=0.0.0.0:7000", "--workers=1", "--threads=2", "main:app"]
CMD ["python", "-m", "gunicorn", "--bind=0.0.0.0:7000", "--workers=1", "--threads=2", "main:app"]

3
ui/deps/pip-hashes.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
pip-compile --generate-hashes --allow-unsafe

View File

@ -6,5 +6,7 @@ requests==2.28.1
docker==6.0.0
python_dateutil==2.8.2
python-magic==0.4.27
bcrypt==4.0.0
gunicorn==20.1.0
bcrypt==4.0.1
sqlalchemy==1.4.42
pymysql==1.0.2
kubernetes==25.3.0

382
ui/deps/requirements.txt Normal file
View File

@ -0,0 +1,382 @@
#
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
#
# pip-compile --allow-unsafe --generate-hashes
#
bcrypt==4.0.1 \
--hash=sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535 \
--hash=sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0 \
--hash=sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410 \
--hash=sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd \
--hash=sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665 \
--hash=sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab \
--hash=sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71 \
--hash=sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215 \
--hash=sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b \
--hash=sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda \
--hash=sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9 \
--hash=sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a \
--hash=sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344 \
--hash=sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f \
--hash=sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d \
--hash=sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c \
--hash=sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c \
--hash=sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2 \
--hash=sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d \
--hash=sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e \
--hash=sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3
# via -r requirements.in
beautifulsoup4==4.11.1 \
--hash=sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30 \
--hash=sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693
# via -r requirements.in
cachetools==5.2.0 \
--hash=sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757 \
--hash=sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db
# via google-auth
certifi==2022.9.24 \
--hash=sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14 \
--hash=sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382
# via
# kubernetes
# requests
charset-normalizer==2.1.1 \
--hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \
--hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f
# via requests
click==8.1.3 \
--hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \
--hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
# via flask
docker==6.0.0 \
--hash=sha256:19e330470af40167d293b0352578c1fa22d74b34d3edf5d4ff90ebc203bbb2f1 \
--hash=sha256:6e06ee8eca46cd88733df09b6b80c24a1a556bc5cb1e1ae54b2c239886d245cf
# via -r requirements.in
flask==2.2.2 \
--hash=sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b \
--hash=sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526
# via
# -r requirements.in
# flask-login
# flask-wtf
flask-login==0.6.2 \
--hash=sha256:1ef79843f5eddd0f143c2cd994c1b05ac83c0401dc6234c143495af9a939613f \
--hash=sha256:c0a7baa9fdc448cdd3dd6f0939df72eec5177b2f7abe6cb82fc934d29caac9c3
# via -r requirements.in
flask-wtf==1.0.1 \
--hash=sha256:34fe5c6fee0f69b50e30f81a3b7ea16aa1492a771fe9ad0974d164610c09a6c9 \
--hash=sha256:9d733658c80be551ce7d5bc13c7a7ac0d80df509be1e23827c847d9520f4359a
# via -r requirements.in
google-auth==2.13.0 \
--hash=sha256:9352dd6394093169157e6971526bab9a2799244d68a94a4a609f0dd751ef6f5e \
--hash=sha256:99510e664155f1a3c0396a076b5deb6367c52ea04d280152c85ac7f51f50eb42
# via kubernetes
greenlet==1.1.3.post0 \
--hash=sha256:0120a879aa2b1ac5118bce959ea2492ba18783f65ea15821680a256dfad04754 \
--hash=sha256:025b8de2273d2809f027d347aa2541651d2e15d593bbce0d5f502ca438c54136 \
--hash=sha256:05ae7383f968bba4211b1fbfc90158f8e3da86804878442b4fb6c16ccbcaa519 \
--hash=sha256:0914f02fcaa8f84f13b2df4a81645d9e82de21ed95633765dd5cc4d3af9d7403 \
--hash=sha256:0971d37ae0eaf42344e8610d340aa0ad3d06cd2eee381891a10fe771879791f9 \
--hash=sha256:0a954002064ee919b444b19c1185e8cce307a1f20600f47d6f4b6d336972c809 \
--hash=sha256:0aa1845944e62f358d63fcc911ad3b415f585612946b8edc824825929b40e59e \
--hash=sha256:104f29dd822be678ef6b16bf0035dcd43206a8a48668a6cae4d2fe9c7a7abdeb \
--hash=sha256:11fc7692d95cc7a6a8447bb160d98671ab291e0a8ea90572d582d57361360f05 \
--hash=sha256:17a69967561269b691747e7f436d75a4def47e5efcbc3c573180fc828e176d80 \
--hash=sha256:2794eef1b04b5ba8948c72cc606aab62ac4b0c538b14806d9c0d88afd0576d6b \
--hash=sha256:2c6e942ca9835c0b97814d14f78da453241837419e0d26f7403058e8db3e38f8 \
--hash=sha256:2ccdc818cc106cc238ff7eba0d71b9c77be868fdca31d6c3b1347a54c9b187b2 \
--hash=sha256:325f272eb997916b4a3fc1fea7313a8adb760934c2140ce13a2117e1b0a8095d \
--hash=sha256:39464518a2abe9c505a727af7c0b4efff2cf242aa168be5f0daa47649f4d7ca8 \
--hash=sha256:3a24f3213579dc8459e485e333330a921f579543a5214dbc935bc0763474ece3 \
--hash=sha256:3aeac044c324c1a4027dca0cde550bd83a0c0fbff7ef2c98df9e718a5086c194 \
--hash=sha256:3c22998bfef3fcc1b15694818fc9b1b87c6cc8398198b96b6d355a7bcb8c934e \
--hash=sha256:467b73ce5dcd89e381292fb4314aede9b12906c18fab903f995b86034d96d5c8 \
--hash=sha256:4a8b58232f5b72973350c2b917ea3df0bebd07c3c82a0a0e34775fc2c1f857e9 \
--hash=sha256:4f74aa0092602da2069df0bc6553919a15169d77bcdab52a21f8c5242898f519 \
--hash=sha256:5662492df0588a51d5690f6578f3bbbd803e7f8d99a99f3bf6128a401be9c269 \
--hash=sha256:5c2d21c2b768d8c86ad935e404cc78c30d53dea009609c3ef3a9d49970c864b5 \
--hash=sha256:5edf75e7fcfa9725064ae0d8407c849456553a181ebefedb7606bac19aa1478b \
--hash=sha256:60839ab4ea7de6139a3be35b77e22e0398c270020050458b3d25db4c7c394df5 \
--hash=sha256:62723e7eb85fa52e536e516ee2ac91433c7bb60d51099293671815ff49ed1c21 \
--hash=sha256:64e10f303ea354500c927da5b59c3802196a07468332d292aef9ddaca08d03dd \
--hash=sha256:66aa4e9a726b70bcbfcc446b7ba89c8cec40f405e51422c39f42dfa206a96a05 \
--hash=sha256:695d0d8b5ae42c800f1763c9fce9d7b94ae3b878919379150ee5ba458a460d57 \
--hash=sha256:70048d7b2c07c5eadf8393e6398595591df5f59a2f26abc2f81abca09610492f \
--hash=sha256:7afa706510ab079fd6d039cc6e369d4535a48e202d042c32e2097f030a16450f \
--hash=sha256:7cf37343e43404699d58808e51f347f57efd3010cc7cee134cdb9141bd1ad9ea \
--hash=sha256:8149a6865b14c33be7ae760bcdb73548bb01e8e47ae15e013bf7ef9290ca309a \
--hash=sha256:814f26b864ed2230d3a7efe0336f5766ad012f94aad6ba43a7c54ca88dd77cba \
--hash=sha256:82a38d7d2077128a017094aff334e67e26194f46bd709f9dcdacbf3835d47ef5 \
--hash=sha256:83a7a6560df073ec9de2b7cb685b199dfd12519bc0020c62db9d1bb522f989fa \
--hash=sha256:8415239c68b2ec9de10a5adf1130ee9cb0ebd3e19573c55ba160ff0ca809e012 \
--hash=sha256:88720794390002b0c8fa29e9602b395093a9a766b229a847e8d88349e418b28a \
--hash=sha256:890f633dc8cb307761ec566bc0b4e350a93ddd77dc172839be122be12bae3e10 \
--hash=sha256:8926a78192b8b73c936f3e87929931455a6a6c6c385448a07b9f7d1072c19ff3 \
--hash=sha256:8c0581077cf2734569f3e500fab09c0ff6a2ab99b1afcacbad09b3c2843ae743 \
--hash=sha256:8fda1139d87ce5f7bd80e80e54f9f2c6fe2f47983f1a6f128c47bf310197deb6 \
--hash=sha256:91a84faf718e6f8b888ca63d0b2d6d185c8e2a198d2a7322d75c303e7097c8b7 \
--hash=sha256:924df1e7e5db27d19b1359dc7d052a917529c95ba5b8b62f4af611176da7c8ad \
--hash=sha256:949c9061b8c6d3e6e439466a9be1e787208dec6246f4ec5fffe9677b4c19fcc3 \
--hash=sha256:9649891ab4153f217f319914455ccf0b86986b55fc0573ce803eb998ad7d6854 \
--hash=sha256:96656c5f7c95fc02c36d4f6ef32f4e94bb0b6b36e6a002c21c39785a4eec5f5d \
--hash=sha256:a812df7282a8fc717eafd487fccc5ba40ea83bb5b13eb3c90c446d88dbdfd2be \
--hash=sha256:a8d24eb5cb67996fb84633fdc96dbc04f2d8b12bfcb20ab3222d6be271616b67 \
--hash=sha256:bef49c07fcb411c942da6ee7d7ea37430f830c482bf6e4b72d92fd506dd3a427 \
--hash=sha256:bffba15cff4802ff493d6edcf20d7f94ab1c2aee7cfc1e1c7627c05f1102eee8 \
--hash=sha256:c0643250dd0756f4960633f5359884f609a234d4066686754e834073d84e9b51 \
--hash=sha256:c6f90234e4438062d6d09f7d667f79edcc7c5e354ba3a145ff98176f974b8132 \
--hash=sha256:c8c9301e3274276d3d20ab6335aa7c5d9e5da2009cccb01127bddb5c951f8870 \
--hash=sha256:c8ece5d1a99a2adcb38f69af2f07d96fb615415d32820108cd340361f590d128 \
--hash=sha256:cb863057bed786f6622982fb8b2c122c68e6e9eddccaa9fa98fd937e45ee6c4f \
--hash=sha256:ccbe7129a282ec5797df0451ca1802f11578be018a32979131065565da89b392 \
--hash=sha256:d25cdedd72aa2271b984af54294e9527306966ec18963fd032cc851a725ddc1b \
--hash=sha256:d75afcbb214d429dacdf75e03a1d6d6c5bd1fa9c35e360df8ea5b6270fb2211c \
--hash=sha256:d7815e1519a8361c5ea2a7a5864945906f8e386fa1bc26797b4d443ab11a4589 \
--hash=sha256:eb6ac495dccb1520667cfea50d89e26f9ffb49fa28496dea2b95720d8b45eb54 \
--hash=sha256:ec615d2912b9ad807afd3be80bf32711c0ff9c2b00aa004a45fd5d5dde7853d9 \
--hash=sha256:f5e09dc5c6e1796969fd4b775ea1417d70e49a5df29aaa8e5d10675d9e11872c \
--hash=sha256:f6661b58412879a2aa099abb26d3c93e91dedaba55a6394d1fb1512a77e85de9 \
--hash=sha256:f7d20c3267385236b4ce54575cc8e9f43e7673fc761b069c820097092e318e3b \
--hash=sha256:fe7c51f8a2ab616cb34bc33d810c887e89117771028e1e3d3b77ca25ddeace04
# via sqlalchemy
idna==3.4 \
--hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
--hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
# via requests
itsdangerous==2.1.2 \
--hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44 \
--hash=sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a
# via
# flask
# flask-wtf
jinja2==3.1.2 \
--hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
--hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
# via flask
kubernetes==25.3.0 \
--hash=sha256:213befbb4e5aed95f94950c7eed0c2322fc5a2f8f40932e58d28fdd42d90836c \
--hash=sha256:eb42333dad0bb5caf4e66460c6a4a1a36f0f057a040f35018f6c05a699baed86
# via -r requirements.in
markupsafe==2.1.1 \
--hash=sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003 \
--hash=sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88 \
--hash=sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5 \
--hash=sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7 \
--hash=sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a \
--hash=sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603 \
--hash=sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1 \
--hash=sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135 \
--hash=sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247 \
--hash=sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6 \
--hash=sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601 \
--hash=sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77 \
--hash=sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02 \
--hash=sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e \
--hash=sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63 \
--hash=sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f \
--hash=sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980 \
--hash=sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b \
--hash=sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812 \
--hash=sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff \
--hash=sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96 \
--hash=sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1 \
--hash=sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925 \
--hash=sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a \
--hash=sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6 \
--hash=sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e \
--hash=sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f \
--hash=sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4 \
--hash=sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f \
--hash=sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3 \
--hash=sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c \
--hash=sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a \
--hash=sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417 \
--hash=sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a \
--hash=sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a \
--hash=sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37 \
--hash=sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452 \
--hash=sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933 \
--hash=sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a \
--hash=sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7
# via
# jinja2
# werkzeug
# wtforms
oauthlib==3.2.2 \
--hash=sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca \
--hash=sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918
# via requests-oauthlib
packaging==21.3 \
--hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
--hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
# via docker
pyasn1==0.4.8 \
--hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \
--hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.2.8 \
--hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \
--hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74
# via google-auth
pymysql==1.0.2 \
--hash=sha256:41fc3a0c5013d5f039639442321185532e3e2c8924687abe6537de157d403641 \
--hash=sha256:816927a350f38d56072aeca5dfb10221fe1dc653745853d30a216637f5d7ad36
# via -r requirements.in
pyparsing==3.0.9 \
--hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
--hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
# via packaging
python-dateutil==2.8.2 \
--hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
--hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
# via
# -r requirements.in
# kubernetes
python-magic==0.4.27 \
--hash=sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b \
--hash=sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3
# via -r requirements.in
pyyaml==6.0 \
--hash=sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf \
--hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \
--hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \
--hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \
--hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \
--hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \
--hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \
--hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \
--hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \
--hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \
--hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \
--hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \
--hash=sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782 \
--hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \
--hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \
--hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \
--hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \
--hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \
--hash=sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1 \
--hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \
--hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \
--hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \
--hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \
--hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \
--hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \
--hash=sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d \
--hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \
--hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \
--hash=sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7 \
--hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \
--hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \
--hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \
--hash=sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358 \
--hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \
--hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \
--hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \
--hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \
--hash=sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f \
--hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \
--hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5
# via kubernetes
requests==2.28.1 \
--hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \
--hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349
# via
# -r requirements.in
# docker
# kubernetes
# requests-oauthlib
requests-oauthlib==1.3.1 \
--hash=sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5 \
--hash=sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a
# via kubernetes
rsa==4.9 \
--hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \
--hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21
# via google-auth
six==1.16.0 \
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
# via
# google-auth
# kubernetes
# python-dateutil
soupsieve==2.3.2.post1 \
--hash=sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759 \
--hash=sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d
# via beautifulsoup4
sqlalchemy==1.4.42 \
--hash=sha256:04f2598c70ea4a29b12d429a80fad3a5202d56dce19dd4916cc46a965a5ca2e9 \
--hash=sha256:0501f74dd2745ec38f44c3a3900fb38b9db1ce21586b691482a19134062bf049 \
--hash=sha256:0ee377eb5c878f7cefd633ab23c09e99d97c449dd999df639600f49b74725b80 \
--hash=sha256:11b2ec26c5d2eefbc3e6dca4ec3d3d95028be62320b96d687b6e740424f83b7d \
--hash=sha256:15d878929c30e41fb3d757a5853b680a561974a0168cd33a750be4ab93181628 \
--hash=sha256:177e41914c476ed1e1b77fd05966ea88c094053e17a85303c4ce007f88eff363 \
--hash=sha256:1811a0b19a08af7750c0b69e38dec3d46e47c4ec1d74b6184d69f12e1c99a5e0 \
--hash=sha256:1d0c23ecf7b3bc81e29459c34a3f4c68ca538de01254e24718a7926810dc39a6 \
--hash=sha256:22459fc1718785d8a86171bbe7f01b5c9d7297301ac150f508d06e62a2b4e8d2 \
--hash=sha256:28e881266a172a4d3c5929182fde6bb6fba22ac93f137d5380cc78a11a9dd124 \
--hash=sha256:2e56dfed0cc3e57b2f5c35719d64f4682ef26836b81067ee6cfad062290fd9e2 \
--hash=sha256:2fd49af453e590884d9cdad3586415922a8e9bb669d874ee1dc55d2bc425aacd \
--hash=sha256:3ab7c158f98de6cb4f1faab2d12973b330c2878d0c6b689a8ca424c02d66e1b3 \
--hash=sha256:4948b6c5f4e56693bbeff52f574279e4ff972ea3353f45967a14c30fb7ae2beb \
--hash=sha256:4e1c5f8182b4f89628d782a183d44db51b5af84abd6ce17ebb9804355c88a7b5 \
--hash=sha256:5ce6929417d5dce5ad1d3f147db81735a4a0573b8fb36e3f95500a06eaddd93e \
--hash=sha256:5ede1495174e69e273fad68ad45b6d25c135c1ce67723e40f6cf536cb515e20b \
--hash=sha256:5f966b64c852592469a7eb759615bbd351571340b8b344f1d3fa2478b5a4c934 \
--hash=sha256:6045b3089195bc008aee5c273ec3ba9a93f6a55bc1b288841bd4cfac729b6516 \
--hash=sha256:6c9d004eb78c71dd4d3ce625b80c96a827d2e67af9c0d32b1c1e75992a7916cc \
--hash=sha256:6e39e97102f8e26c6c8550cb368c724028c575ec8bc71afbbf8faaffe2b2092a \
--hash=sha256:723e3b9374c1ce1b53564c863d1a6b2f1dc4e97b1c178d9b643b191d8b1be738 \
--hash=sha256:876eb185911c8b95342b50a8c4435e1c625944b698a5b4a978ad2ffe74502908 \
--hash=sha256:9256563506e040daddccaa948d055e006e971771768df3bb01feeb4386c242b0 \
--hash=sha256:934472bb7d8666727746a75670a1f8d91a9cae8c464bba79da30a0f6faccd9e1 \
--hash=sha256:97ff50cd85bb907c2a14afb50157d0d5486a4b4639976b4a3346f34b6d1b5272 \
--hash=sha256:9b01d9cd2f9096f688c71a3d0f33f3cd0af8549014e66a7a7dee6fc214a7277d \
--hash=sha256:9e3a65ce9ed250b2f096f7b559fe3ee92e6605fab3099b661f0397a9ac7c8d95 \
--hash=sha256:a7dd5b7b34a8ba8d181402d824b87c5cee8963cb2e23aa03dbfe8b1f1e417cde \
--hash=sha256:a85723c00a636eed863adb11f1e8aaa36ad1c10089537823b4540948a8429798 \
--hash=sha256:b42c59ffd2d625b28cdb2ae4cde8488543d428cba17ff672a543062f7caee525 \
--hash=sha256:bd448b262544b47a2766c34c0364de830f7fb0772d9959c1c42ad61d91ab6565 \
--hash=sha256:ca9389a00f639383c93ed00333ed763812f80b5ae9e772ea32f627043f8c9c88 \
--hash=sha256:df76e9c60879fdc785a34a82bf1e8691716ffac32e7790d31a98d7dec6e81545 \
--hash=sha256:e12c6949bae10f1012ab5c0ea52ab8db99adcb8c7b717938252137cdf694c775 \
--hash=sha256:e4ef8cb3c5b326f839bfeb6af5f406ba02ad69a78c7aac0fbeeba994ad9bb48a \
--hash=sha256:e7e740453f0149437c101ea4fdc7eea2689938c5760d7dcc436c863a12f1f565 \
--hash=sha256:effc89e606165ca55f04f3f24b86d3e1c605e534bf1a96e4e077ce1b027d0b71 \
--hash=sha256:f0f574465b78f29f533976c06b913e54ab4980b9931b69aa9d306afff13a9471 \
--hash=sha256:fa5b7eb2051e857bf83bade0641628efe5a88de189390725d3e6033a1fff4257 \
--hash=sha256:fdb94a3d1ba77ff2ef11912192c066f01e68416f554c194d769391638c8ad09a
# via -r requirements.in
urllib3==1.26.12 \
--hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \
--hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997
# via
# docker
# kubernetes
# requests
websocket-client==1.4.1 \
--hash=sha256:398909eb7e261f44b8f4bd474785b6ec5f5b499d4953342fe9755e01ef624090 \
--hash=sha256:f9611eb65c8241a67fb373bef040b3cf8ad377a9f6546a12b620b6511e8ea9ef
# via
# docker
# kubernetes
werkzeug==2.2.2 \
--hash=sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f \
--hash=sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5
# via
# flask
# flask-login
wtforms==3.0.1 \
--hash=sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc \
--hash=sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b
# via flask-wtf
# The following packages are considered to be unsafe in a requirements file:
setuptools==65.5.0 \
--hash=sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17 \
--hash=sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356
# via kubernetes

View File

@ -1,3 +1,7 @@
from sys import path as sys_path, exit as sys_exit, modules as sys_modules
sys_path.append("/opt/bunkerweb/ui/deps/python")
from bs4 import BeautifulSoup
from copy import deepcopy
from datetime import datetime, timezone
@ -23,16 +27,15 @@ from flask_wtf.csrf import CSRFProtect, CSRFError, generate_csrf
from json import JSONDecodeError, load as json_load
from jinja2 import Template
from logging import getLogger, INFO, ERROR, StreamHandler, Formatter
from os import chmod, getpid, listdir, mkdir, walk
from os import chmod, getenv, getpid, listdir, mkdir, walk
from os.path import exists, isdir, isfile, join
from re import match as re_match
from requests import get
from requests.utils import default_headers
from shutil import rmtree, copytree, chown
from sys import path as sys_path, exit as sys_exit, modules as sys_modules
from tarfile import CompressionError, HeaderError, ReadError, TarError, open as tar_open
from threading import Thread
from time import time
from time import sleep, time
from traceback import format_exc
from typing import Optional
from uuid import uuid4
@ -40,12 +43,14 @@ from zipfile import BadZipFile, ZipFile
sys_path.append("/opt/bunkerweb/utils")
sys_path.append("/opt/bunkerweb/api")
sys_path.append("/opt/bunkerweb/db")
from src.Instances import Instances
from src.ConfigFiles import ConfigFiles
from src.Config import Config
from src.ReverseProxied import ReverseProxied
from src.User import User
from utils import (
check_settings,
env_to_summary_class,
@ -57,20 +62,10 @@ from utils import (
get_variables,
path_to_dict,
)
from API import API
from ApiCaller import ApiCaller
from logger import setup_logger
from Database import Database
# Set up logger
logger = getLogger("flask_app")
logger.setLevel(INFO)
# create console handler with a higher log level
ch = StreamHandler()
ch.setLevel(ERROR)
# create formatter and add it to the handlers
formatter = Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
ch.setFormatter(formatter)
# add the handlers to logger
logger.addHandler(ch)
logger = setup_logger("UI", getenv("LOG_LEVEL", "INFO"))
# Flask app
app = Flask(
@ -84,13 +79,25 @@ app.wsgi_app = ReverseProxied(app.wsgi_app)
# Set variables and instantiate objects
vars = get_variables()
if "ABSOLUTE_URI" not in vars:
logger.error("ABSOLUTE_URI is not set")
sys_exit(1)
elif "ADMIN_USERNAME" not in vars:
logger.error("ADMIN_USERNAME is not set")
sys_exit(1)
elif "ADMIN_PASSWORD" not in vars:
logger.error("ADMIN_PASSWORD is not set")
sys_exit(1)
if not vars["FLASK_ENV"] == "development" and vars["ADMIN_PASSWORD"] == "changeme":
logger.error("Please change the default admin password.")
sys_exit(1)
if not vars["FLASK_ENV"] == "development" and (
vars["ABSOLUTE_URI"].endswith("/changeme/")
or vars["ABSOLUTE_URI"].endswith("/changeme")
if not vars["ABSOLUTE_URI"].endswith("/"):
vars["ABSOLUTE_URI"] += "/"
if not vars["FLASK_ENV"] == "development" and vars["ABSOLUTE_URI"].endswith(
"/changeme/"
):
logger.error("Please change the default URL.")
sys_exit(1)
@ -102,7 +109,6 @@ login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "login"
user = User(vars["ADMIN_USERNAME"], vars["ADMIN_PASSWORD"])
api_caller = ApiCaller()
PLUGIN_KEYS = [
"id",
"order",
@ -112,39 +118,31 @@ PLUGIN_KEYS = [
"settings",
]
bw_integration = "Local"
if getenv("KUBERNETES_MODE", "no") == "yes":
bw_integration = "Kubernetes"
elif getenv("SWARM_MODE", "no") == "yes" or getenv("AUTOCONF_MODE", "no") == "yes":
bw_integration = "Cluster"
try:
docker_client: DockerClient = DockerClient(base_url=vars["DOCKER_HOST"])
docker_client: DockerClient = DockerClient(
base_url=vars.get("DOCKER_HOST", "unix:///var/run/docker.sock")
)
bw_integration = "Cluster"
except (docker_APIError, DockerException):
logger.warning("No docker host found")
docker_client = None
if docker_client:
apis: list[API] = []
for container in docker_client.containers.list(
filters={"label": "bunkerweb.INSTANCE"}
):
env_variables = {
x[0]: x[1]
for x in [env.split("=") for env in container.attrs["Config"]["Env"]]
}
apis.append(
API(
f"http://{container.name}:{env_variables.get('API_HTTP_PORT', '5000')}",
env_variables.get("API_SERVER_NAME", "bwapi"),
)
)
api_caller._set_apis(apis)
db = Database(logger, bw_integration=bw_integration)
try:
app.config.update(
DEBUG=True,
SECRET_KEY=vars["FLASK_SECRET"],
ABSOLUTE_URI=vars["ABSOLUTE_URI"],
INSTANCES=Instances(docker_client),
CONFIG=Config(),
CONFIGFILES=ConfigFiles(),
INSTANCES=Instances(docker_client, bw_integration),
CONFIG=Config(logger, db),
CONFIGFILES=ConfigFiles(db),
SESSION_COOKIE_DOMAIN=vars["ABSOLUTE_URI"]
.replace("http://", "")
.replace("https://", "")
@ -610,6 +608,10 @@ def configs():
flash(operation)
error = app.config["CONFIGFILES"].save_configs()
if error:
flash("Couldn't save custom configs to database", "error")
# Reload instances
app.config["RELOADING"] = True
Thread(

View File

@ -1,7 +1,8 @@
from copy import deepcopy
from os import listdir
from time import sleep
from flask import flash
from os.path import isfile
from os.path import exists, isfile
from typing import List, Tuple
from json import load as json_load
from uuid import uuid4
@ -11,10 +12,28 @@ from subprocess import run, DEVNULL, STDOUT
class Config:
def __init__(self):
def __init__(self, logger, db) -> None:
with open("/opt/bunkerweb/settings.json", "r") as f:
self.__settings: dict = json_load(f)
self.__logger = logger
self.__db = db
if not exists("/usr/sbin/nginx"):
while not self.__db.is_initialized():
self.__logger.warning(
"Database is not initialized, retrying in 5s ...",
)
sleep(3)
env = self.__db.get_config()
while not self.__db.is_first_config_saved() or not env:
self.__logger.warning(
"Database doesn't have any config saved yet, retrying in 5s ...",
)
sleep(3)
env = self.__db.get_config()
self.reload_plugins()
def reload_plugins(self) -> None:
@ -145,6 +164,8 @@ class Config:
"/etc/nginx",
"--variables",
env_file,
"--method",
"ui",
],
stdin=DEVNULL,
stderr=STDOUT,
@ -153,6 +174,12 @@ class Config:
if proc.returncode != 0:
raise Exception(f"Error from generator (return code = {proc.returncode})")
ret = self.__db.save_config(conf, "ui")
if ret:
self.__logger.error(
f"Can't save config in database: {ret}",
)
def get_plugins_settings(self) -> dict:
return self.__plugins_settings
@ -173,7 +200,13 @@ class Config:
dict
The nginx variables env file as a dict
"""
return self.__env_to_dict("/etc/nginx/variables.env")
if exists("/usr/sbin/nginx"):
return {
k: {"value": v, "method": "ui"}
for k, v in self.__env_to_dict("/etc/nginx/variables.env").items()
}
return self.__db.get_config(methods=True)
def get_services(self) -> list[dict]:
"""Get nginx's services
@ -183,12 +216,18 @@ class Config:
list
The services
"""
services = []
for filename in iglob("/etc/nginx/**/variables.env"):
env = self.__env_to_dict(filename)
services.append(env)
if exists("/usr/sbin/nginx"):
services = []
for filename in iglob("/etc/nginx/**/variables.env"):
env = {
k: {"value": v, "method": "ui"}
for k, v in self.__env_to_dict(filename).items()
}
services.append(env)
return services
return services
return self.__db.get_services_settings(methods=True)
def check_variables(self, variables: dict, _global: bool = False) -> int:
"""Testify that the variables passed are valid

View File

@ -1,4 +1,5 @@
import os
from os import listdir, mkdir, remove, replace, walk
from os.path import dirname, exists, join, isfile
from re import compile as re_compile
from shutil import rmtree, move as shutil_move
from typing import Tuple
@ -7,13 +8,41 @@ from utils import path_to_dict
class ConfigFiles:
def __init__(self):
def __init__(self, db):
self.__name_regex = re_compile(r"^[a-zA-Z0-9_-]{1,64}$")
self.__root_dirs = [
child["name"]
for child in path_to_dict("/opt/bunkerweb/configs")["children"]
]
self.__file_creation_blacklist = ["http", "stream"]
self.__db = db
def save_configs(self) -> str:
custom_configs = {}
root_dirs = listdir("/opt/bunkerweb/configs")
for (root, dirs, files) in walk("/opt/bunkerweb/configs", topdown=True):
if (
root != "configs"
and (dirs and not root.split("/")[-1] in root_dirs)
or files
):
path_exploded = root.split("/")
for file in files:
with open(join(root, file), "r") as f:
custom_configs[
(
f"{path_exploded.pop()}"
if path_exploded[-1] not in root_dirs
else ""
)
+ f"CUSTOM_CONF_{path_exploded[-1].replace('-', '_').upper()}_{file.replace('.conf', '')}"
] = f.read()
ret = self.__db.save_custom_configs(custom_configs, "ui")
if ret:
return "Couldn't save custom configs to database"
return ""
def check_name(self, name: str) -> bool:
return self.__name_regex.match(name)
@ -39,7 +68,7 @@ class ConfigFiles:
dirs = "/".join(dirs)
if len(dirs) > 1:
for x in range(nbr_children - 1):
if not os.path.exists(
if not exists(
f"{root_path}{root_dir}/{'/'.join(dirs.split('/')[0:-x])}"
):
return f"{root_path}{root_dir}/{'/'.join(dirs.split('/')[0:-x])} doesn't exist"
@ -48,8 +77,8 @@ class ConfigFiles:
def delete_path(self, path: str) -> Tuple[str, int]:
try:
if os.path.isfile(path):
os.remove(path)
if isfile(path):
remove(path)
else:
rmtree(path)
except OSError:
@ -58,23 +87,23 @@ class ConfigFiles:
return f"{path} was successfully deleted", 0
def create_folder(self, path: str, name: str) -> Tuple[str, int]:
folder_path = os.path.join(path, name)
folder_path = join(path, name)
try:
os.mkdir(folder_path)
mkdir(folder_path)
except OSError:
return f"Could not create {folder_path}", 1
return f"The folder {folder_path} was successfully created", 0
def create_file(self, path: str, name: str, content: str) -> Tuple[str, int]:
file_path = os.path.join(path, name)
file_path = join(path, name)
with open(file_path, "w") as f:
f.write(content)
return f"The file {file_path} was successfully created", 0
def edit_folder(self, path: str, name: str) -> Tuple[str, int]:
new_folder_path = os.path.dirname(os.path.join(path, name))
new_folder_path = dirname(join(path, name))
if path == new_folder_path:
return (
@ -90,7 +119,7 @@ class ConfigFiles:
return f"The folder {path} was successfully renamed to {new_folder_path}", 0
def edit_file(self, path: str, name: str, content: str) -> Tuple[str, int]:
new_path = os.path.dirname(os.path.join(path, name))
new_path = dirname(join(path, name))
try:
with open(path, "r") as f:
file_content = f.read()
@ -104,7 +133,7 @@ class ConfigFiles:
)
elif file_content == content:
try:
os.replace(path, new_path)
replace(path, new_path)
return f"{path} was successfully renamed to {new_path}", 0
except OSError:
return f"Could not rename {path} into {new_path}", 1
@ -112,7 +141,7 @@ class ConfigFiles:
new_path = path
else:
try:
os.remove(path)
remove(path)
except OSError:
return f"Could not remove {path}", 1

View File

@ -1,6 +1,8 @@
import os
from os import getenv
from os.path import exists
from typing import Any, Union
from subprocess import run
from kubernetes import client as kube_client
from API import API
from ApiCaller import ApiCaller
@ -35,7 +37,7 @@ class Instance:
if "Health" in data.attrs["State"]
else False
)
if data
if _type == "container" and data
else True
)
self.env = data
@ -44,8 +46,8 @@ class Instance:
def get_id(self) -> str:
return self._id
def run_jobs(self) -> bool:
return self.apiCaller._send_to_apis("POST", "/jobs")
# def run_jobs(self) -> bool:
# return self.apiCaller._send_to_apis("POST", "/jobs")
def reload(self) -> bool:
return self.apiCaller._send_to_apis("POST", "/reload")
@ -59,10 +61,14 @@ class Instance:
def restart(self) -> bool:
return self.apiCaller._send_to_apis("POST", "/restart")
def send_custom_configs(self) -> bool:
return self.apiCaller._send_files("/opt/bunkerweb/configs", "/custom_configs")
class Instances:
def __init__(self, docker_client):
def __init__(self, docker_client, bw_integration: str):
self.__docker = docker_client
self.__bw_integration = bw_integration
def __instance_from_id(self, _id) -> Instance:
instances: list[Instance] = self.get_instances()
@ -106,13 +112,80 @@ class Instances:
)
)
is_swarm = True
try:
self.__docker.swarm.version
except:
is_swarm = False
if is_swarm:
for instance in self.__docker.services.list(
filters={"label": "bunkerweb.INSTANCE"}
):
status = "down"
desired_tasks = instance.attrs["ServiceStatus"]["DesiredTasks"]
running_tasks = instance.attrs["ServiceStatus"]["RunningTasks"]
if desired_tasks > 0 and (desired_tasks == running_tasks):
status = "up"
instances.append(
Instance(
instance.id,
instance.name,
instance.name,
"service",
status,
instance,
apiCaller,
)
)
elif self.__bw_integration == "Kubernetes":
corev1 = kube_client.CoreV1Api()
for pod in corev1.list_pod_for_all_namespaces(watch=False).items:
if (
pod.metadata.annotations != None
and "bunkerweb.io/INSTANCE" in pod.metadata.annotations
):
env_variables = {
e.name: e.value for e in pod.spec.containers[0].env
}
apiCaller = ApiCaller()
apiCaller._set_apis(
[
API(
f"http://{pod.status.pod_ip}:{env_variables.get('API_HTTP_PORT', '5000')}",
env_variables.get("API_SERVER_NAME", "bwapi"),
)
]
)
status = "up"
if pod.status.conditions is not None:
for condition in pod.status.conditions:
if condition.type == "Ready" and condition.status == "True":
status = "down"
break
instances.append(
Instance(
pod.metadata.uid,
pod.metadata.name,
pod.status.pod_ip,
"container",
status,
pod,
apiCaller,
)
)
instances = sorted(
instances,
key=lambda x: x.name,
)
# Local instance
if os.path.exists("/usr/sbin/nginx"):
if exists("/usr/sbin/nginx"):
instances.insert(
0,
Instance(
@ -120,12 +193,24 @@ class Instances:
"local",
"127.0.0.1",
"local",
"up" if os.path.exists("/opt/bunkerweb/tmp/nginx.pid") else "down",
"up" if exists("/opt/bunkerweb/tmp/nginx.pid") else "down",
),
)
return instances
def send_custom_configs_to_instances(self) -> Union[list[str], str]:
failed_to_send: list[str] = []
for instance in self.get_instances():
if instance.health is False:
failed_to_send.append(instance.name)
continue
if not instance.send_custom_configs():
failed_to_send.append(instance.name)
return failed_to_send or "Successfully sent custom configs to instances"
def reload_instances(self) -> Union[list[str], str]:
not_reloaded: list[str] = []
for instance in self.get_instances():
@ -151,7 +236,7 @@ class Instances:
!= 0
)
elif instance._type == "container":
result = instance.run_jobs()
# result = instance.run_jobs()
result = result & instance.reload()
if result: