Merge branch 'master' into master
This commit is contained in:
commit
6faa7fbb04
|
|
@ -62,8 +62,6 @@ podAnnotations:
|
||||||
extraEnvs:
|
extraEnvs:
|
||||||
[]
|
[]
|
||||||
# Exemple of settings to make snapshot view working in the ui when using AWS
|
# Exemple of settings to make snapshot view working in the ui when using AWS
|
||||||
# - name: WALE_S3_ENDPOINT
|
|
||||||
# value: https+path://s3.us-east-1.amazonaws.com:443
|
|
||||||
# - name: SPILO_S3_BACKUP_PREFIX
|
# - name: SPILO_S3_BACKUP_PREFIX
|
||||||
# value: spilo/
|
# value: spilo/
|
||||||
# - name: AWS_ACCESS_KEY_ID
|
# - name: AWS_ACCESS_KEY_ID
|
||||||
|
|
@ -83,8 +81,6 @@ extraEnvs:
|
||||||
# key: AWS_DEFAULT_REGION
|
# key: AWS_DEFAULT_REGION
|
||||||
# - name: SPILO_S3_BACKUP_BUCKET
|
# - name: SPILO_S3_BACKUP_BUCKET
|
||||||
# value: <s3 bucket used by the operator>
|
# value: <s3 bucket used by the operator>
|
||||||
# - name: "USE_AWS_INSTANCE_PROFILE"
|
|
||||||
# value: "true"
|
|
||||||
|
|
||||||
# configure UI service
|
# configure UI service
|
||||||
service:
|
service:
|
||||||
|
|
|
||||||
|
|
@ -384,7 +384,7 @@ exceptions:
|
||||||
The interval of days can be set with `password_rotation_interval` (default
|
The interval of days can be set with `password_rotation_interval` (default
|
||||||
`90` = 90 days, minimum 1). On each rotation the user name and password values
|
`90` = 90 days, minimum 1). On each rotation the user name and password values
|
||||||
are replaced in the K8s secret. They belong to a newly created user named after
|
are replaced in the K8s secret. They belong to a newly created user named after
|
||||||
the original role plus rotation date in YYMMDD format. All priviliges are
|
the original role plus rotation date in YYMMDD format. All privileges are
|
||||||
inherited meaning that migration scripts should still grant and revoke rights
|
inherited meaning that migration scripts should still grant and revoke rights
|
||||||
against the original role. The timestamp of the next rotation (in RFC 3339
|
against the original role. The timestamp of the next rotation (in RFC 3339
|
||||||
format, UTC timezone) is written to the secret as well. Note, if the rotation
|
format, UTC timezone) is written to the secret as well. Note, if the rotation
|
||||||
|
|
@ -564,7 +564,7 @@ manifest affinity.
|
||||||
```
|
```
|
||||||
|
|
||||||
If `node_readiness_label_merge` is set to `"OR"` (default) the readiness label
|
If `node_readiness_label_merge` is set to `"OR"` (default) the readiness label
|
||||||
affinty will be appended with its own expressions block:
|
affinity will be appended with its own expressions block:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
affinity:
|
affinity:
|
||||||
|
|
@ -1140,7 +1140,7 @@ metadata:
|
||||||
iam.gke.io/gcp-service-account: <GCP_SERVICE_ACCOUNT_NAME>@<GCP_PROJECT_ID>.iam.gserviceaccount.com
|
iam.gke.io/gcp-service-account: <GCP_SERVICE_ACCOUNT_NAME>@<GCP_PROJECT_ID>.iam.gserviceaccount.com
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Specify the new custom service account in your [operator paramaters](./reference/operator_parameters.md)
|
2. Specify the new custom service account in your [operator parameters](./reference/operator_parameters.md)
|
||||||
|
|
||||||
If using manual deployment or kustomize, this is done by setting
|
If using manual deployment or kustomize, this is done by setting
|
||||||
`pod_service_account_name` in your configuration file specified in the
|
`pod_service_account_name` in your configuration file specified in the
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,7 @@ These parameters are grouped directly under the `spec` key in the manifest.
|
||||||
[kubernetes volumeSource](https://godoc.org/k8s.io/api/core/v1#VolumeSource).
|
[kubernetes volumeSource](https://godoc.org/k8s.io/api/core/v1#VolumeSource).
|
||||||
It allows you to mount existing PersistentVolumeClaims, ConfigMaps and Secrets inside the StatefulSet.
|
It allows you to mount existing PersistentVolumeClaims, ConfigMaps and Secrets inside the StatefulSet.
|
||||||
Also an `emptyDir` volume can be shared between initContainer and statefulSet.
|
Also an `emptyDir` volume can be shared between initContainer and statefulSet.
|
||||||
Additionaly, you can provide a `SubPath` for volume mount (a file in a configMap source volume, for example).
|
Additionally, you can provide a `SubPath` for volume mount (a file in a configMap source volume, for example).
|
||||||
Set `isSubPathExpr` to true if you want to include [API environment variables](https://kubernetes.io/docs/concepts/storage/volumes/#using-subpath-expanded-environment).
|
Set `isSubPathExpr` to true if you want to include [API environment variables](https://kubernetes.io/docs/concepts/storage/volumes/#using-subpath-expanded-environment).
|
||||||
You can also specify in which container the additional Volumes will be mounted with the `targetContainers` array option.
|
You can also specify in which container the additional Volumes will be mounted with the `targetContainers` array option.
|
||||||
If `targetContainers` is empty, additional volumes will be mounted only in the `postgres` container.
|
If `targetContainers` is empty, additional volumes will be mounted only in the `postgres` container.
|
||||||
|
|
@ -257,7 +257,7 @@ These parameters are grouped directly under the `spec` key in the manifest.
|
||||||
## Prepared Databases
|
## Prepared Databases
|
||||||
|
|
||||||
The operator can create databases with default owner, reader and writer roles
|
The operator can create databases with default owner, reader and writer roles
|
||||||
without the need to specifiy them under `users` or `databases` sections. Those
|
without the need to specify them under `users` or `databases` sections. Those
|
||||||
parameters are grouped under the `preparedDatabases` top-level key. For more
|
parameters are grouped under the `preparedDatabases` top-level key. For more
|
||||||
information, see [user docs](../user.md#prepared-databases-with-roles-and-default-privileges).
|
information, see [user docs](../user.md#prepared-databases-with-roles-and-default-privileges).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ under the `users` key.
|
||||||
For all `LOGIN` roles that are not database owners the operator can rotate
|
For all `LOGIN` roles that are not database owners the operator can rotate
|
||||||
credentials in the corresponding K8s secrets by replacing the username and
|
credentials in the corresponding K8s secrets by replacing the username and
|
||||||
password. This means, new users will be added on each rotation inheriting
|
password. This means, new users will be added on each rotation inheriting
|
||||||
all priviliges from the original roles. The rotation date (in YYMMDD format)
|
all privileges from the original roles. The rotation date (in YYMMDD format)
|
||||||
is appended to the names of the new user. The timestamp of the next rotation
|
is appended to the names of the new user. The timestamp of the next rotation
|
||||||
is written to the secret. The default is `false`.
|
is written to the secret. The default is `false`.
|
||||||
|
|
||||||
|
|
@ -552,7 +552,7 @@ configuration they are grouped under the `kubernetes` key.
|
||||||
pods with `InitialDelaySeconds: 6`, `PeriodSeconds: 10`, `TimeoutSeconds: 5`,
|
pods with `InitialDelaySeconds: 6`, `PeriodSeconds: 10`, `TimeoutSeconds: 5`,
|
||||||
`SuccessThreshold: 1` and `FailureThreshold: 3`. When enabling readiness
|
`SuccessThreshold: 1` and `FailureThreshold: 3`. When enabling readiness
|
||||||
probes it is recommended to switch the `pod_management_policy` to `parallel`
|
probes it is recommended to switch the `pod_management_policy` to `parallel`
|
||||||
to avoid unneccesary waiting times in case of multiple instances failing.
|
to avoid unnecessary waiting times in case of multiple instances failing.
|
||||||
The default is `false`.
|
The default is `false`.
|
||||||
|
|
||||||
* **storage_resize_mode**
|
* **storage_resize_mode**
|
||||||
|
|
@ -701,7 +701,7 @@ In the CRD-based configuration they are grouped under the `load_balancer` key.
|
||||||
replaced by the cluster name, `{namespace}` is replaced with the namespace
|
replaced by the cluster name, `{namespace}` is replaced with the namespace
|
||||||
and `{hostedzone}` is replaced with the hosted zone (the value of the
|
and `{hostedzone}` is replaced with the hosted zone (the value of the
|
||||||
`db_hosted_zone` parameter). The `{team}` placeholder can still be used,
|
`db_hosted_zone` parameter). The `{team}` placeholder can still be used,
|
||||||
although it is not recommened because the team of a cluster can change.
|
although it is not recommended because the team of a cluster can change.
|
||||||
If the cluster name starts with the `teamId` it will also be part of the
|
If the cluster name starts with the `teamId` it will also be part of the
|
||||||
DNS, aynway. No other placeholders are allowed!
|
DNS, aynway. No other placeholders are allowed!
|
||||||
|
|
||||||
|
|
@ -720,7 +720,7 @@ In the CRD-based configuration they are grouped under the `load_balancer` key.
|
||||||
is replaced by the cluster name, `{namespace}` is replaced with the
|
is replaced by the cluster name, `{namespace}` is replaced with the
|
||||||
namespace and `{hostedzone}` is replaced with the hosted zone (the value of
|
namespace and `{hostedzone}` is replaced with the hosted zone (the value of
|
||||||
the `db_hosted_zone` parameter). The `{team}` placeholder can still be used,
|
the `db_hosted_zone` parameter). The `{team}` placeholder can still be used,
|
||||||
although it is not recommened because the team of a cluster can change.
|
although it is not recommended because the team of a cluster can change.
|
||||||
If the cluster name starts with the `teamId` it will also be part of the
|
If the cluster name starts with the `teamId` it will also be part of the
|
||||||
DNS, aynway. No other placeholders are allowed!
|
DNS, aynway. No other placeholders are allowed!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -900,7 +900,7 @@ the PostgreSQL version between source and target cluster has to be the same.
|
||||||
|
|
||||||
To start a cluster as standby, add the following `standby` section in the YAML
|
To start a cluster as standby, add the following `standby` section in the YAML
|
||||||
file. You can stream changes from archived WAL files (AWS S3 or Google Cloud
|
file. You can stream changes from archived WAL files (AWS S3 or Google Cloud
|
||||||
Storage) or from a remote primary. Only one option can be specfied in the
|
Storage) or from a remote primary. Only one option can be specified in the
|
||||||
manifest:
|
manifest:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
@ -911,7 +911,7 @@ spec:
|
||||||
|
|
||||||
For GCS, you have to define STANDBY_GOOGLE_APPLICATION_CREDENTIALS as a
|
For GCS, you have to define STANDBY_GOOGLE_APPLICATION_CREDENTIALS as a
|
||||||
[custom pod environment variable](administrator.md#custom-pod-environment-variables).
|
[custom pod environment variable](administrator.md#custom-pod-environment-variables).
|
||||||
It is not set from the config to allow for overridding.
|
It is not set from the config to allow for overriding.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
spec:
|
spec:
|
||||||
|
|
@ -1282,7 +1282,7 @@ minutes if the certificates have changed and reloads postgres accordingly.
|
||||||
### TLS certificates for connection pooler
|
### TLS certificates for connection pooler
|
||||||
|
|
||||||
By default, the pgBouncer image generates its own TLS certificate like Spilo.
|
By default, the pgBouncer image generates its own TLS certificate like Spilo.
|
||||||
When the `tls` section is specfied in the manifest it will be used for the
|
When the `tls` section is specified in the manifest it will be used for the
|
||||||
connection pooler pod(s) as well. The security context options are hard coded
|
connection pooler pod(s) as well. The security context options are hard coded
|
||||||
to `runAsUser: 100` and `runAsGroup: 101`. The `fsGroup` will be the same
|
to `runAsUser: 100` and `runAsGroup: 101`. The `fsGroup` will be the same
|
||||||
like for Spilo.
|
like for Spilo.
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,6 @@ spec:
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
# Exemple of settings to make snapshot view working in the ui when using AWS
|
# Exemple of settings to make snapshot view working in the ui when using AWS
|
||||||
# - name: WALE_S3_ENDPOINT
|
|
||||||
# value: https+path://s3.us-east-1.amazonaws.com:443
|
|
||||||
# - name: SPILO_S3_BACKUP_PREFIX
|
# - name: SPILO_S3_BACKUP_PREFIX
|
||||||
# value: spilo/
|
# value: spilo/
|
||||||
# - name: AWS_ACCESS_KEY_ID
|
# - name: AWS_ACCESS_KEY_ID
|
||||||
|
|
@ -102,5 +100,3 @@ spec:
|
||||||
# key: AWS_DEFAULT_REGION
|
# key: AWS_DEFAULT_REGION
|
||||||
# - name: SPILO_S3_BACKUP_BUCKET
|
# - name: SPILO_S3_BACKUP_BUCKET
|
||||||
# value: <s3 bucket used by the operator>
|
# value: <s3 bucket used by the operator>
|
||||||
# - name: "USE_AWS_INSTANCE_PROFILE"
|
|
||||||
# value: "true"
|
|
||||||
|
|
|
||||||
|
|
@ -95,14 +95,6 @@ DEFAULT_MEMORY_LIMIT = getenv('DEFAULT_MEMORY_LIMIT', '300Mi')
|
||||||
DEFAULT_CPU = getenv('DEFAULT_CPU', '10m')
|
DEFAULT_CPU = getenv('DEFAULT_CPU', '10m')
|
||||||
DEFAULT_CPU_LIMIT = getenv('DEFAULT_CPU_LIMIT', '300m')
|
DEFAULT_CPU_LIMIT = getenv('DEFAULT_CPU_LIMIT', '300m')
|
||||||
|
|
||||||
WALE_S3_ENDPOINT = getenv(
|
|
||||||
'WALE_S3_ENDPOINT',
|
|
||||||
'https+path://s3.eu-central-1.amazonaws.com:443',
|
|
||||||
)
|
|
||||||
|
|
||||||
USE_AWS_INSTANCE_PROFILE = (
|
|
||||||
getenv('USE_AWS_INSTANCE_PROFILE', 'false').lower() != 'false'
|
|
||||||
)
|
|
||||||
|
|
||||||
AWS_ENDPOINT = getenv('AWS_ENDPOINT')
|
AWS_ENDPOINT = getenv('AWS_ENDPOINT')
|
||||||
|
|
||||||
|
|
@ -784,8 +776,6 @@ def get_versions(pg_cluster: str):
|
||||||
bucket=SPILO_S3_BACKUP_BUCKET,
|
bucket=SPILO_S3_BACKUP_BUCKET,
|
||||||
pg_cluster=pg_cluster,
|
pg_cluster=pg_cluster,
|
||||||
prefix=SPILO_S3_BACKUP_PREFIX,
|
prefix=SPILO_S3_BACKUP_PREFIX,
|
||||||
s3_endpoint=WALE_S3_ENDPOINT,
|
|
||||||
use_aws_instance_profile=USE_AWS_INSTANCE_PROFILE,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -797,9 +787,8 @@ def get_basebackups(pg_cluster: str, uid: str):
|
||||||
bucket=SPILO_S3_BACKUP_BUCKET,
|
bucket=SPILO_S3_BACKUP_BUCKET,
|
||||||
pg_cluster=pg_cluster,
|
pg_cluster=pg_cluster,
|
||||||
prefix=SPILO_S3_BACKUP_PREFIX,
|
prefix=SPILO_S3_BACKUP_PREFIX,
|
||||||
s3_endpoint=WALE_S3_ENDPOINT,
|
|
||||||
uid=uid,
|
uid=uid,
|
||||||
use_aws_instance_profile=USE_AWS_INSTANCE_PROFILE,
|
postgresql_versions=OPERATOR_UI_CONFIG.get('postgresql_versions', DEFAULT_UI_CONFIG['postgresql_versions']),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -991,8 +980,6 @@ def main(port, debug, clusters: list):
|
||||||
logger.info(f'Superuser team: {SUPERUSER_TEAM}')
|
logger.info(f'Superuser team: {SUPERUSER_TEAM}')
|
||||||
logger.info(f'Target namespace: {TARGET_NAMESPACE}')
|
logger.info(f'Target namespace: {TARGET_NAMESPACE}')
|
||||||
logger.info(f'Teamservice URL: {TEAM_SERVICE_URL}')
|
logger.info(f'Teamservice URL: {TEAM_SERVICE_URL}')
|
||||||
logger.info(f'Use AWS instance_profile: {USE_AWS_INSTANCE_PROFILE}')
|
|
||||||
logger.info(f'WAL-E S3 endpoint: {WALE_S3_ENDPOINT}')
|
|
||||||
logger.info(f'AWS S3 endpoint: {AWS_ENDPOINT}')
|
logger.info(f'AWS S3 endpoint: {AWS_ENDPOINT}')
|
||||||
|
|
||||||
if TARGET_NAMESPACE is None:
|
if TARGET_NAMESPACE is None:
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,8 @@ from os import environ, getenv
|
||||||
from requests import Session
|
from requests import Session
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from wal_e.cmd import configure_backup_cxt
|
|
||||||
|
|
||||||
from .utils import Attrs, defaulting, these
|
from .utils import defaulting, these
|
||||||
from operator_ui.adapters.logger import logger
|
from operator_ui.adapters.logger import logger
|
||||||
|
|
||||||
session = Session()
|
session = Session()
|
||||||
|
|
@ -284,10 +283,8 @@ def read_stored_clusters(bucket, prefix, delimiter='/'):
|
||||||
def read_versions(
|
def read_versions(
|
||||||
pg_cluster,
|
pg_cluster,
|
||||||
bucket,
|
bucket,
|
||||||
s3_endpoint,
|
|
||||||
prefix,
|
prefix,
|
||||||
delimiter='/',
|
delimiter='/',
|
||||||
use_aws_instance_profile=False,
|
|
||||||
):
|
):
|
||||||
return [
|
return [
|
||||||
'base' if uid == 'wal' else uid
|
'base' if uid == 'wal' else uid
|
||||||
|
|
@ -305,35 +302,72 @@ def read_versions(
|
||||||
if uid == 'wal' or defaulting(lambda: UUID(uid))
|
if uid == 'wal' or defaulting(lambda: UUID(uid))
|
||||||
]
|
]
|
||||||
|
|
||||||
BACKUP_VERSION_PREFIXES = ['', '10/', '11/', '12/', '13/', '14/', '15/', '16/', '17/']
|
def lsn_to_wal_segment_stop(finish_lsn, start_segment, wal_segment_size=16 * 1024 * 1024):
|
||||||
|
timeline = int(start_segment[:8], 16)
|
||||||
|
log_id = finish_lsn >> 32
|
||||||
|
seg_id = (finish_lsn & 0xFFFFFFFF) // wal_segment_size
|
||||||
|
return f"{timeline:08X}{log_id:08X}{seg_id:08X}"
|
||||||
|
|
||||||
|
def lsn_to_offset_hex(lsn, wal_segment_size=16 * 1024 * 1024):
|
||||||
|
return f"{lsn % wal_segment_size:08X}"
|
||||||
|
|
||||||
def read_basebackups(
|
def read_basebackups(
|
||||||
pg_cluster,
|
pg_cluster,
|
||||||
uid,
|
uid,
|
||||||
bucket,
|
bucket,
|
||||||
s3_endpoint,
|
|
||||||
prefix,
|
prefix,
|
||||||
delimiter='/',
|
postgresql_versions,
|
||||||
use_aws_instance_profile=False,
|
|
||||||
):
|
):
|
||||||
environ['WALE_S3_ENDPOINT'] = s3_endpoint
|
|
||||||
suffix = '' if uid == 'base' else '/' + uid
|
suffix = '' if uid == 'base' else '/' + uid
|
||||||
backups = []
|
backups = []
|
||||||
|
|
||||||
for vp in BACKUP_VERSION_PREFIXES:
|
for vp in postgresql_versions:
|
||||||
|
backup_prefix = f'{prefix}{pg_cluster}{suffix}/wal/{vp}/basebackups_005/'
|
||||||
|
logger.info(f"{bucket}/{backup_prefix}")
|
||||||
|
|
||||||
backups = backups + [
|
paginator = client('s3').get_paginator('list_objects_v2')
|
||||||
{
|
pages = paginator.paginate(Bucket=bucket, Prefix=backup_prefix)
|
||||||
key: value
|
|
||||||
for key, value in basebackup.__dict__.items()
|
for page in pages:
|
||||||
if isinstance(value, str) or isinstance(value, int)
|
for obj in page.get("Contents", []):
|
||||||
}
|
key = obj["Key"]
|
||||||
for basebackup in Attrs.call(
|
if not key.endswith("backup_stop_sentinel.json"):
|
||||||
f=configure_backup_cxt,
|
continue
|
||||||
aws_instance_profile=use_aws_instance_profile,
|
|
||||||
s3_prefix=f's3://{bucket}/{prefix}{pg_cluster}{suffix}/wal/{vp}',
|
response = client('s3').get_object(Bucket=bucket, Key=key)
|
||||||
)._backup_list(detail=True)
|
backup_info = loads(response["Body"].read().decode("utf-8"))
|
||||||
]
|
last_modified = response["LastModified"].astimezone(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
backup_name = key.split("/")[-1].replace("_backup_stop_sentinel.json", "")
|
||||||
|
start_seg, start_offset = backup_name.split("_")[1], backup_name.split("_")[-1] if "_" in backup_name else None
|
||||||
|
|
||||||
|
if "LSN" in backup_info and "FinishLSN" in backup_info:
|
||||||
|
# WAL-G
|
||||||
|
lsn = backup_info["LSN"]
|
||||||
|
finish_lsn = backup_info["FinishLSN"]
|
||||||
|
backups.append({
|
||||||
|
"expanded_size_bytes": backup_info.get("UncompressedSize"),
|
||||||
|
"last_modified": last_modified,
|
||||||
|
"name": backup_name,
|
||||||
|
"wal_segment_backup_start": start_seg,
|
||||||
|
"wal_segment_backup_stop": lsn_to_wal_segment_stop(finish_lsn, start_seg),
|
||||||
|
"wal_segment_offset_backup_start": lsn_to_offset_hex(lsn),
|
||||||
|
"wal_segment_offset_backup_stop": lsn_to_offset_hex(finish_lsn),
|
||||||
|
})
|
||||||
|
elif "wal_segment_backup_stop" in backup_info:
|
||||||
|
# WAL-E
|
||||||
|
stop_seg = backup_info["wal_segment_backup_stop"]
|
||||||
|
stop_offset = backup_info["wal_segment_offset_backup_stop"]
|
||||||
|
|
||||||
|
backups.append({
|
||||||
|
"expanded_size_bytes": backup_info.get("expanded_size_bytes"),
|
||||||
|
"last_modified": last_modified,
|
||||||
|
"name": backup_name,
|
||||||
|
"wal_segment_backup_start": start_seg,
|
||||||
|
"wal_segment_backup_stop": stop_seg,
|
||||||
|
"wal_segment_offset_backup_start": start_offset,
|
||||||
|
"wal_segment_offset_backup_stop": stop_offset,
|
||||||
|
})
|
||||||
|
|
||||||
return backups
|
return backups
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,4 @@ kubernetes==11.0.0
|
||||||
python-json-logger==2.0.7
|
python-json-logger==2.0.7
|
||||||
requests==2.32.2
|
requests==2.32.2
|
||||||
stups-tokens>=1.1.19
|
stups-tokens>=1.1.19
|
||||||
wal_e==1.1.1
|
|
||||||
werkzeug==3.0.6
|
werkzeug==3.0.6
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue