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

View file

@ -96,6 +96,7 @@ class Global_values(Base):
) )
value = Column(String(1023), nullable=False) value = Column(String(1023), nullable=False)
suffix = Column(SmallInteger, primary_key=True, nullable=True, default=0) suffix = Column(SmallInteger, primary_key=True, nullable=True, default=0)
method = Column(METHODS_ENUM, nullable=False)
setting = relationship("Settings", back_populates="global_value") setting = relationship("Settings", back_populates="global_value")
@ -104,7 +105,6 @@ class Services(Base):
__tablename__ = "services" __tablename__ = "services"
id = Column(String(64), primary_key=True) id = Column(String(64), primary_key=True)
method = Column(METHODS_ENUM, nullable=False)
settings = relationship( settings = relationship(
"Services_settings", back_populates="service", cascade="all, delete" "Services_settings", back_populates="service", cascade="all, delete"
@ -132,6 +132,7 @@ class Services_settings(Base):
) )
value = Column(String(1023), nullable=False) value = Column(String(1023), nullable=False)
suffix = Column(SmallInteger, primary_key=True, nullable=True, default=0) suffix = Column(SmallInteger, primary_key=True, nullable=True, default=0)
method = Column(METHODS_ENUM, nullable=False)
service = relationship("Services", back_populates="settings") service = relationship("Services", back_populates="settings")
setting = relationship("Settings", back_populates="services") 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['service_id']}"
tmp_path += f"/{custom_config['name']}.conf" tmp_path += f"/{custom_config['name']}.conf"
makedirs(dirname(tmp_path), exist_ok=True) 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"]) f.write(custom_config["data"])
if bw_integration != "Local": if bw_integration != "Local":
@ -277,7 +277,7 @@ if __name__ == "__main__":
tmp_path += f"/{custom_config['service_id']}" tmp_path += f"/{custom_config['service_id']}"
tmp_path += f"/{custom_config['name']}.conf" tmp_path += f"/{custom_config['name']}.conf"
makedirs(dirname(tmp_path), exist_ok=True) 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"]) f.write(custom_config["data"])
if bw_integration != "Local": 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 FROM python:3.11-rc-alpine
COPY --from=builder /opt/bunkerweb/deps/python /opt/bunkerweb/deps/python
# Copy files # Copy files
# can't exclude specific files/dir from . so we are copying everything by hand # can't exclude specific files/dir from . so we are copying everything by hand
COPY bw/api /opt/bunkerweb/api COPY bw/api /opt/bunkerweb/api
COPY bw/confs /opt/bunkerweb/confs COPY bw/confs /opt/bunkerweb/confs
COPY bw/core /opt/bunkerweb/core COPY bw/core /opt/bunkerweb/core
COPY bw/gen /opt/bunkerweb/gen COPY bw/gen /opt/bunkerweb/gen
COPY utils /opt/bunkerweb/utils
COPY bw/settings.json /opt/bunkerweb/settings.json COPY bw/settings.json /opt/bunkerweb/settings.json
COPY db /opt/bunkerweb/db
COPY utils /opt/bunkerweb/utils
COPY VERSION /opt/bunkerweb/VERSION COPY VERSION /opt/bunkerweb/VERSION
COPY ui/requirements.txt /opt/bunkerweb/ui/requirements.txt
# Install UI requirements # Copy python requirements
RUN apk add --no-cache --virtual build gcc python3-dev musl-dev libffi-dev openssl-dev cargo && \ COPY ui/deps/requirements.txt /opt/bunkerweb/ui/deps/requirements.txt
pip install -r /opt/bunkerweb/ui/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 apk del build
COPY ui /opt/bunkerweb/ui COPY ui /opt/bunkerweb/ui
@ -42,16 +36,13 @@ RUN apk add --no-cache bash file && \
find /opt/bunkerweb -type d -exec chmod 0750 {} \; && \ find /opt/bunkerweb -type d -exec chmod 0750 {} \; && \
chown -R ui:ui /data && \ chown -R ui:ui /data && \
chmod 770 /opt/bunkerweb/tmp && \ chmod 770 /opt/bunkerweb/tmp && \
chmod 750 /opt/bunkerweb/gen/main.py /opt/bunkerweb/deps/python/bin/* && \ chmod 750 /opt/bunkerweb/gen/main.py /opt/bunkerweb/ui/deps/python/bin/* && \
mkdir /etc/nginx && \
chown -R ui:ui /etc/nginx && \
chmod -R 770 /etc/nginx && \
ln -s /usr/local/bin/python /usr/bin/python3 ln -s /usr/local/bin/python /usr/bin/python3
# Fix CVEs # 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" 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 EXPOSE 7000
@ -59,4 +50,4 @@ WORKDIR /opt/bunkerweb/ui
USER ui: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 docker==6.0.0
python_dateutil==2.8.2 python_dateutil==2.8.2
python-magic==0.4.27 python-magic==0.4.27
bcrypt==4.0.0 bcrypt==4.0.1
gunicorn==20.1.0 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 bs4 import BeautifulSoup
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone 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 json import JSONDecodeError, load as json_load
from jinja2 import Template from jinja2 import Template
from logging import getLogger, INFO, ERROR, StreamHandler, Formatter 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 os.path import exists, isdir, isfile, join
from re import match as re_match from re import match as re_match
from requests import get from requests import get
from requests.utils import default_headers from requests.utils import default_headers
from shutil import rmtree, copytree, chown 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 tarfile import CompressionError, HeaderError, ReadError, TarError, open as tar_open
from threading import Thread from threading import Thread
from time import time from time import sleep, time
from traceback import format_exc from traceback import format_exc
from typing import Optional from typing import Optional
from uuid import uuid4 from uuid import uuid4
@ -40,12 +43,14 @@ from zipfile import BadZipFile, ZipFile
sys_path.append("/opt/bunkerweb/utils") sys_path.append("/opt/bunkerweb/utils")
sys_path.append("/opt/bunkerweb/api") sys_path.append("/opt/bunkerweb/api")
sys_path.append("/opt/bunkerweb/db")
from src.Instances import Instances from src.Instances import Instances
from src.ConfigFiles import ConfigFiles from src.ConfigFiles import ConfigFiles
from src.Config import Config from src.Config import Config
from src.ReverseProxied import ReverseProxied from src.ReverseProxied import ReverseProxied
from src.User import User from src.User import User
from utils import ( from utils import (
check_settings, check_settings,
env_to_summary_class, env_to_summary_class,
@ -57,20 +62,10 @@ from utils import (
get_variables, get_variables,
path_to_dict, path_to_dict,
) )
from API import API from logger import setup_logger
from ApiCaller import ApiCaller from Database import Database
# Set up logger logger = setup_logger("UI", getenv("LOG_LEVEL", "INFO"))
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)
# Flask app # Flask app
app = Flask( app = Flask(
@ -84,13 +79,25 @@ app.wsgi_app = ReverseProxied(app.wsgi_app)
# Set variables and instantiate objects # Set variables and instantiate objects
vars = get_variables() 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": if not vars["FLASK_ENV"] == "development" and vars["ADMIN_PASSWORD"] == "changeme":
logger.error("Please change the default admin password.") logger.error("Please change the default admin password.")
sys_exit(1) sys_exit(1)
if not vars["FLASK_ENV"] == "development" and ( if not vars["ABSOLUTE_URI"].endswith("/"):
vars["ABSOLUTE_URI"].endswith("/changeme/") vars["ABSOLUTE_URI"] += "/"
or vars["ABSOLUTE_URI"].endswith("/changeme")
if not vars["FLASK_ENV"] == "development" and vars["ABSOLUTE_URI"].endswith(
"/changeme/"
): ):
logger.error("Please change the default URL.") logger.error("Please change the default URL.")
sys_exit(1) sys_exit(1)
@ -102,7 +109,6 @@ login_manager = LoginManager()
login_manager.init_app(app) login_manager.init_app(app)
login_manager.login_view = "login" login_manager.login_view = "login"
user = User(vars["ADMIN_USERNAME"], vars["ADMIN_PASSWORD"]) user = User(vars["ADMIN_USERNAME"], vars["ADMIN_PASSWORD"])
api_caller = ApiCaller()
PLUGIN_KEYS = [ PLUGIN_KEYS = [
"id", "id",
"order", "order",
@ -112,39 +118,31 @@ PLUGIN_KEYS = [
"settings", "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: 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): except (docker_APIError, DockerException):
logger.warning("No docker host found")
docker_client = None docker_client = None
db = Database(logger, bw_integration=bw_integration)
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)
try: try:
app.config.update( app.config.update(
DEBUG=True, DEBUG=True,
SECRET_KEY=vars["FLASK_SECRET"], SECRET_KEY=vars["FLASK_SECRET"],
ABSOLUTE_URI=vars["ABSOLUTE_URI"], ABSOLUTE_URI=vars["ABSOLUTE_URI"],
INSTANCES=Instances(docker_client), INSTANCES=Instances(docker_client, bw_integration),
CONFIG=Config(), CONFIG=Config(logger, db),
CONFIGFILES=ConfigFiles(), CONFIGFILES=ConfigFiles(db),
SESSION_COOKIE_DOMAIN=vars["ABSOLUTE_URI"] SESSION_COOKIE_DOMAIN=vars["ABSOLUTE_URI"]
.replace("http://", "") .replace("http://", "")
.replace("https://", "") .replace("https://", "")
@ -610,6 +608,10 @@ def configs():
flash(operation) flash(operation)
error = app.config["CONFIGFILES"].save_configs()
if error:
flash("Couldn't save custom configs to database", "error")
# Reload instances # Reload instances
app.config["RELOADING"] = True app.config["RELOADING"] = True
Thread( Thread(

View file

@ -1,7 +1,8 @@
from copy import deepcopy from copy import deepcopy
from os import listdir from os import listdir
from time import sleep
from flask import flash from flask import flash
from os.path import isfile from os.path import exists, isfile
from typing import List, Tuple from typing import List, Tuple
from json import load as json_load from json import load as json_load
from uuid import uuid4 from uuid import uuid4
@ -11,10 +12,28 @@ from subprocess import run, DEVNULL, STDOUT
class Config: class Config:
def __init__(self): def __init__(self, logger, db) -> None:
with open("/opt/bunkerweb/settings.json", "r") as f: with open("/opt/bunkerweb/settings.json", "r") as f:
self.__settings: dict = json_load(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() self.reload_plugins()
def reload_plugins(self) -> None: def reload_plugins(self) -> None:
@ -145,6 +164,8 @@ class Config:
"/etc/nginx", "/etc/nginx",
"--variables", "--variables",
env_file, env_file,
"--method",
"ui",
], ],
stdin=DEVNULL, stdin=DEVNULL,
stderr=STDOUT, stderr=STDOUT,
@ -153,6 +174,12 @@ class Config:
if proc.returncode != 0: if proc.returncode != 0:
raise Exception(f"Error from generator (return code = {proc.returncode})") 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: def get_plugins_settings(self) -> dict:
return self.__plugins_settings return self.__plugins_settings
@ -173,7 +200,13 @@ class Config:
dict dict
The nginx variables env file as a 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]: def get_services(self) -> list[dict]:
"""Get nginx's services """Get nginx's services
@ -183,12 +216,18 @@ class Config:
list list
The services The services
""" """
services = [] if exists("/usr/sbin/nginx"):
for filename in iglob("/etc/nginx/**/variables.env"): services = []
env = self.__env_to_dict(filename) for filename in iglob("/etc/nginx/**/variables.env"):
services.append(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: def check_variables(self, variables: dict, _global: bool = False) -> int:
"""Testify that the variables passed are valid """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 re import compile as re_compile
from shutil import rmtree, move as shutil_move from shutil import rmtree, move as shutil_move
from typing import Tuple from typing import Tuple
@ -7,13 +8,41 @@ from utils import path_to_dict
class ConfigFiles: class ConfigFiles:
def __init__(self): def __init__(self, db):
self.__name_regex = re_compile(r"^[a-zA-Z0-9_-]{1,64}$") self.__name_regex = re_compile(r"^[a-zA-Z0-9_-]{1,64}$")
self.__root_dirs = [ self.__root_dirs = [
child["name"] child["name"]
for child in path_to_dict("/opt/bunkerweb/configs")["children"] for child in path_to_dict("/opt/bunkerweb/configs")["children"]
] ]
self.__file_creation_blacklist = ["http", "stream"] 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: def check_name(self, name: str) -> bool:
return self.__name_regex.match(name) return self.__name_regex.match(name)
@ -39,7 +68,7 @@ class ConfigFiles:
dirs = "/".join(dirs) dirs = "/".join(dirs)
if len(dirs) > 1: if len(dirs) > 1:
for x in range(nbr_children - 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])}" f"{root_path}{root_dir}/{'/'.join(dirs.split('/')[0:-x])}"
): ):
return f"{root_path}{root_dir}/{'/'.join(dirs.split('/')[0:-x])} doesn't exist" 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]: def delete_path(self, path: str) -> Tuple[str, int]:
try: try:
if os.path.isfile(path): if isfile(path):
os.remove(path) remove(path)
else: else:
rmtree(path) rmtree(path)
except OSError: except OSError:
@ -58,23 +87,23 @@ class ConfigFiles:
return f"{path} was successfully deleted", 0 return f"{path} was successfully deleted", 0
def create_folder(self, path: str, name: str) -> Tuple[str, int]: def create_folder(self, path: str, name: str) -> Tuple[str, int]:
folder_path = os.path.join(path, name) folder_path = join(path, name)
try: try:
os.mkdir(folder_path) mkdir(folder_path)
except OSError: except OSError:
return f"Could not create {folder_path}", 1 return f"Could not create {folder_path}", 1
return f"The folder {folder_path} was successfully created", 0 return f"The folder {folder_path} was successfully created", 0
def create_file(self, path: str, name: str, content: str) -> Tuple[str, int]: 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: with open(file_path, "w") as f:
f.write(content) f.write(content)
return f"The file {file_path} was successfully created", 0 return f"The file {file_path} was successfully created", 0
def edit_folder(self, path: str, name: str) -> Tuple[str, int]: 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: if path == new_folder_path:
return ( return (
@ -90,7 +119,7 @@ class ConfigFiles:
return f"The folder {path} was successfully renamed to {new_folder_path}", 0 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]: 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: try:
with open(path, "r") as f: with open(path, "r") as f:
file_content = f.read() file_content = f.read()
@ -104,7 +133,7 @@ class ConfigFiles:
) )
elif file_content == content: elif file_content == content:
try: try:
os.replace(path, new_path) replace(path, new_path)
return f"{path} was successfully renamed to {new_path}", 0 return f"{path} was successfully renamed to {new_path}", 0
except OSError: except OSError:
return f"Could not rename {path} into {new_path}", 1 return f"Could not rename {path} into {new_path}", 1
@ -112,7 +141,7 @@ class ConfigFiles:
new_path = path new_path = path
else: else:
try: try:
os.remove(path) remove(path)
except OSError: except OSError:
return f"Could not remove {path}", 1 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 typing import Any, Union
from subprocess import run from subprocess import run
from kubernetes import client as kube_client
from API import API from API import API
from ApiCaller import ApiCaller from ApiCaller import ApiCaller
@ -35,7 +37,7 @@ class Instance:
if "Health" in data.attrs["State"] if "Health" in data.attrs["State"]
else False else False
) )
if data if _type == "container" and data
else True else True
) )
self.env = data self.env = data
@ -44,8 +46,8 @@ class Instance:
def get_id(self) -> str: def get_id(self) -> str:
return self._id return self._id
def run_jobs(self) -> bool: # def run_jobs(self) -> bool:
return self.apiCaller._send_to_apis("POST", "/jobs") # return self.apiCaller._send_to_apis("POST", "/jobs")
def reload(self) -> bool: def reload(self) -> bool:
return self.apiCaller._send_to_apis("POST", "/reload") return self.apiCaller._send_to_apis("POST", "/reload")
@ -59,10 +61,14 @@ class Instance:
def restart(self) -> bool: def restart(self) -> bool:
return self.apiCaller._send_to_apis("POST", "/restart") 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: class Instances:
def __init__(self, docker_client): def __init__(self, docker_client, bw_integration: str):
self.__docker = docker_client self.__docker = docker_client
self.__bw_integration = bw_integration
def __instance_from_id(self, _id) -> Instance: def __instance_from_id(self, _id) -> Instance:
instances: list[Instance] = self.get_instances() 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 = sorted(
instances, instances,
key=lambda x: x.name, key=lambda x: x.name,
) )
# Local instance # Local instance
if os.path.exists("/usr/sbin/nginx"): if exists("/usr/sbin/nginx"):
instances.insert( instances.insert(
0, 0,
Instance( Instance(
@ -120,12 +193,24 @@ class Instances:
"local", "local",
"127.0.0.1", "127.0.0.1",
"local", "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 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]: def reload_instances(self) -> Union[list[str], str]:
not_reloaded: list[str] = [] not_reloaded: list[str] = []
for instance in self.get_instances(): for instance in self.get_instances():
@ -151,7 +236,7 @@ class Instances:
!= 0 != 0
) )
elif instance._type == "container": elif instance._type == "container":
result = instance.run_jobs() # result = instance.run_jobs()
result = result & instance.reload() result = result & instance.reload()
if result: if result: