Compare commits
20 Commits
version/0.
...
version/0.
Author | SHA1 | Date | |
---|---|---|---|
a4cc653757 | |||
db4ff20906 | |||
1f0fbd33b6 | |||
5de8d2721e | |||
0d65da9a9e | |||
4316ee4330 | |||
2ed9a1dbe3 | |||
8e03824d20 | |||
754dbdd0e5 | |||
e13d348315 | |||
169f3ebe5b | |||
f8ad604e85 | |||
774b9c8a61 | |||
d8c522233e | |||
82d50f7eaa | |||
1c426c5136 | |||
d6e14cc551 | |||
c3917ebc2e | |||
7203bd37a3 | |||
597188c7ee |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.12.1-stable
|
||||
current_version = 0.12.3-stable
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
|
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -18,11 +18,11 @@ jobs:
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/passbook:0.12.1-stable
|
||||
-t beryju/passbook:0.12.3-stable
|
||||
-t beryju/passbook:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook:0.12.1-stable
|
||||
run: docker push beryju/passbook:0.12.3-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook:latest
|
||||
build-proxy:
|
||||
@ -48,11 +48,11 @@ jobs:
|
||||
cd proxy
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/passbook-proxy:0.12.1-stable \
|
||||
-t beryju/passbook-proxy:0.12.3-stable \
|
||||
-t beryju/passbook-proxy:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-proxy:0.12.1-stable
|
||||
run: docker push beryju/passbook-proxy:0.12.3-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-proxy:latest
|
||||
build-static:
|
||||
@ -77,11 +77,11 @@ jobs:
|
||||
run: docker build
|
||||
--no-cache
|
||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||
-t beryju/passbook-static:0.12.1-stable
|
||||
-t beryju/passbook-static:0.12.3-stable
|
||||
-t beryju/passbook-static:latest
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-static:0.12.1-stable
|
||||
run: docker push beryju/passbook-static:0.12.3-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-static:latest
|
||||
test-release:
|
||||
@ -114,5 +114,5 @@ jobs:
|
||||
SENTRY_PROJECT: passbook
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
tagName: 0.12.1-stable
|
||||
tagName: 0.12.3-stable
|
||||
environment: beryjuorg-prod
|
||||
|
36
Pipfile.lock
generated
36
Pipfile.lock
generated
@ -74,18 +74,18 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:9ab957090f7893172768bb8b8d2c5cce0afd36a9d36d73a9fb14168f72d75a8b",
|
||||
"sha256:f56148e2c6b9a2d704218da42f07d72f00270bfddb13bc1bdea20d3327daa51e"
|
||||
"sha256:2e16f02c8b832d401d958d7ca0a14c5bc7da17827918e6b24e5bc43dce8f496e",
|
||||
"sha256:ab5353a968a4e664b9da2dd950169b755066525fcbfdfc90e7e49c8333d95c19"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.15.18"
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:de5f9fc0c7e88ee7ba831fa27475be258ae09ece99143ed623d3618a3c84ee2c",
|
||||
"sha256:e224754230e7e015836ba20037cac6321e8e2ce9b8627c14d579fcb37249decd"
|
||||
"sha256:226effa72e3ddd0a802e812c0e204999393ca7982fee754cc0c770a7a1caef3a",
|
||||
"sha256:9bf8586b69f20cf0a8ed1e27338cd10ce847751d1a2fd98b92662565c8a2df24"
|
||||
],
|
||||
"version": "==1.18.18"
|
||||
"version": "==1.19.0"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
@ -1100,23 +1100,23 @@
|
||||
"secure"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
|
||||
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
|
||||
"sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
|
||||
"sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": null,
|
||||
"version": "==1.25.10"
|
||||
"version": "==1.25.11"
|
||||
},
|
||||
"uvicorn": {
|
||||
"extras": [
|
||||
"standard"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:a461e76406088f448f36323f5ac774d50e5a552b6ccb54e4fca8d83ef614a7c2",
|
||||
"sha256:d06a25caa8dc680ad92eb3ec67363f5281c092059613a1cc0100acba37fc0f45"
|
||||
"sha256:8ff7495c74b8286a341526ff9efa3988ebab9a4b2f561c7438c3cb420992d7dd",
|
||||
"sha256:e5dbed4a8a44c7b04376021021d63798d6a7bcfae9c654a0b153577b93854fba"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.12.1"
|
||||
"version": "==0.12.2"
|
||||
},
|
||||
"uvloop": {
|
||||
"hashes": [
|
||||
@ -1476,10 +1476,10 @@
|
||||
},
|
||||
"pbr": {
|
||||
"hashes": [
|
||||
"sha256:14bfd98f51c78a3dd22a1ef45cf194ad79eee4a19e8e1a0d5c7f8e81ffe182ea",
|
||||
"sha256:5adc0f9fc64319d8df5ca1e4e06eea674c26b80e6f00c530b18ce6a6592ead15"
|
||||
"sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9",
|
||||
"sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"
|
||||
],
|
||||
"version": "==5.5.0"
|
||||
"version": "==5.5.1"
|
||||
},
|
||||
"pep8-naming": {
|
||||
"hashes": [
|
||||
@ -1745,12 +1745,12 @@
|
||||
"secure"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
|
||||
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
|
||||
"sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
|
||||
"sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": null,
|
||||
"version": "==1.25.10"
|
||||
"version": "==1.25.11"
|
||||
},
|
||||
"wrapt": {
|
||||
"hashes": [
|
||||
|
@ -19,7 +19,7 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
server:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.1-stable}
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.3-stable}
|
||||
command: server
|
||||
environment:
|
||||
PASSBOOK_REDIS__HOST: redis
|
||||
@ -40,7 +40,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
worker:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.1-stable}
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.3-stable}
|
||||
command: worker
|
||||
networks:
|
||||
- internal
|
||||
@ -54,7 +54,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
static:
|
||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.1-stable}
|
||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.3-stable}
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
|
@ -13,7 +13,7 @@ Download the latest `docker-compose.yml` from [here](https://raw.githubuserconte
|
||||
|
||||
To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING__ENABLED=true >> .env`
|
||||
|
||||
To optionally deploy a different version run `echo PASSBOOK_TAG=0.12.1-stable >> .env`
|
||||
To optionally deploy a different version run `echo PASSBOOK_TAG=0.12.3-stable >> .env`
|
||||
|
||||
If this is a fresh passbook install run the following commands to generate a password:
|
||||
|
||||
|
@ -11,7 +11,7 @@ This installation automatically applies database migrations on startup. After th
|
||||
image:
|
||||
name: beryju/passbook
|
||||
name_static: beryju/passbook-static
|
||||
tag: 0.12.1-stable
|
||||
tag: 0.12.3-stable
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
|
@ -6,6 +6,10 @@
|
||||
|
||||
### Backup
|
||||
|
||||
!!! notice
|
||||
|
||||
Local backups are **enabled** by default, and will be run daily at 00:00
|
||||
|
||||
Local backups can be created by running the following command in your passbook installation directory
|
||||
|
||||
```
|
||||
@ -14,15 +18,6 @@ docker-compose run --rm worker backup
|
||||
|
||||
This will dump the current database into the `./backups` folder. By defaults, the last 10 Backups are kept.
|
||||
|
||||
To schedule these backups, use the following snippet in a crontab
|
||||
|
||||
```
|
||||
0 0 * * * bash -c "cd <passbook install location> && docker-compose run --rm worker backup" >/dev/null
|
||||
```
|
||||
|
||||
!!! notice
|
||||
|
||||
passbook does support automatic backups on a schedule, however this is currently not recommended, as there is no way to monitor these scheduled tasks.
|
||||
|
||||
### Restore
|
||||
|
||||
@ -42,11 +37,7 @@ After you've restored the backup, it is recommended to restart all services with
|
||||
|
||||
### S3 Configuration
|
||||
|
||||
!!! notice
|
||||
|
||||
To trigger backups with S3 enabled, use the same commands as above.
|
||||
|
||||
#### S3 Preparation
|
||||
#### Preparation
|
||||
|
||||
passbook expects the bucket you select to already exist. The IAM User given to passbook should have the following permissions
|
||||
|
||||
@ -101,11 +92,11 @@ Simply enable these options in your values.yaml file
|
||||
```yaml
|
||||
# Enable Database Backups to S3
|
||||
backup:
|
||||
access_key: access-key
|
||||
secret_key: secret-key
|
||||
accessKey: access-key
|
||||
secretKey: secret-key
|
||||
bucket: s3-bucket
|
||||
region: eu-central-1
|
||||
host: s3-host
|
||||
```
|
||||
|
||||
Afterwards, run a `helm upgrade` to update the ConfigMap. Because passbook-scheduled backups are not recommended currently, a Kubernetes CronJob is created that runs the backup daily.
|
||||
Afterwards, run a `helm upgrade` to update the ConfigMap. Backups are done automatically as above, at 00:00 every day.
|
||||
|
@ -317,6 +317,7 @@ class TestSourceOAuth1(SeleniumTestCase):
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[name='confirm']").click()
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
sleep(2)
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||
self.driver.get(self.url("passbook_core:user-settings"))
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
apiVersion: v2
|
||||
appVersion: "0.12.1-stable"
|
||||
appVersion: "0.12.3-stable"
|
||||
description: A Helm chart for passbook.
|
||||
name: passbook
|
||||
version: "0.12.1-stable"
|
||||
version: "0.12.3-stable"
|
||||
icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
|
@ -1,42 +0,0 @@
|
||||
{{- if .Values.backup }}
|
||||
apiVersion: batch/v1beta1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: {{ include "passbook.fullname" . }}-backup
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
spec:
|
||||
schedule: "0 0 * * *"
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
args: [server]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
prefix: PASSBOOK_
|
||||
env:
|
||||
- name: PASSBOOK_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ include "passbook.fullname" . }}-secret-key"
|
||||
key: "secret_key"
|
||||
- name: PASSBOOK_REDIS__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-redis"
|
||||
key: "redis-password"
|
||||
- name: PASSBOOK_POSTGRESQL__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-postgresql"
|
||||
key: "postgresql-password"
|
||||
{{- end}}
|
@ -4,7 +4,7 @@
|
||||
image:
|
||||
name: beryju/passbook
|
||||
name_static: beryju/passbook-static
|
||||
tag: 0.12.1-stable
|
||||
tag: 0.12.3-stable
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
|
@ -47,7 +47,9 @@ if __name__ == "__main__":
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
for _, sub in getmembers(mod, isclass):
|
||||
for name, sub in getmembers(mod, isclass):
|
||||
if name != "Migration":
|
||||
continue
|
||||
migration = sub(curr, conn)
|
||||
if migration.needs_migration():
|
||||
LOGGER.info("Migration needs to be applied", migration=sub)
|
||||
|
@ -25,7 +25,7 @@ delete from django_migrations where app = 'passbook_stages_password' and
|
||||
name = '0002_passwordstage_change_flow';"""
|
||||
|
||||
|
||||
class To010Migration(BaseMigration):
|
||||
class Migration(BaseMigration):
|
||||
def needs_migration(self) -> bool:
|
||||
self.cur.execute(
|
||||
"select * from information_schema.tables where table_name='oidc_provider_client'"
|
||||
|
@ -1,28 +0,0 @@
|
||||
from pickle import loads # nosec
|
||||
|
||||
from redis import Redis
|
||||
|
||||
from lifecycle.migrate import BaseMigration
|
||||
from passbook.lib.config import CONFIG
|
||||
|
||||
|
||||
class To012Migration(BaseMigration):
|
||||
def __init__(self) -> None:
|
||||
self.redis = Redis(
|
||||
host=CONFIG.y("redis.host"),
|
||||
port=6379,
|
||||
db=CONFIG.y("redis.cache_db"),
|
||||
password=CONFIG.y("redis.password"),
|
||||
)
|
||||
|
||||
def needs_migration(self) -> bool:
|
||||
keys = self.redis.keys(":1:outpost_*")
|
||||
for key in keys:
|
||||
value = loads(self.redis.get(key)) # nosec
|
||||
if isinstance(value, str):
|
||||
return True
|
||||
return False
|
||||
|
||||
def run(self):
|
||||
keys_to_delete = self.redis.keys(":1:outpost_*")
|
||||
self.redis.delete(*keys_to_delete)
|
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.12.1-stable"
|
||||
__version__ = "0.12.3-stable"
|
||||
|
@ -49,7 +49,7 @@
|
||||
</span>
|
||||
</td>
|
||||
{% with states=outpost.state %}
|
||||
{% if states|length > 1 %}
|
||||
{% if states|length > 0 %}
|
||||
<td role="cell">
|
||||
{% for state in states %}
|
||||
<div>
|
||||
|
@ -79,11 +79,18 @@ class MonitoredTask(Task):
|
||||
|
||||
_result: TaskResult
|
||||
|
||||
_uid: Optional[str]
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.save_on_success = True
|
||||
self._uid = None
|
||||
self._result = TaskResult(status=TaskResultStatus.ERROR, messages=[])
|
||||
|
||||
def set_uid(self, uid: str):
|
||||
"""Set UID, so in the case of an unexpected error its saved correctly"""
|
||||
self._uid = uid
|
||||
|
||||
def set_status(self, result: TaskResult):
|
||||
"""Set result for current run, will overwrite previous result."""
|
||||
self._result = result
|
||||
@ -92,6 +99,8 @@ class MonitoredTask(Task):
|
||||
def after_return(
|
||||
self, status, retval, task_id, args: List[Any], kwargs: Dict[str, Any], einfo
|
||||
):
|
||||
if not self._result.uid:
|
||||
self._result.uid = self._uid
|
||||
if self.save_on_success:
|
||||
TaskInfo(
|
||||
task_name=self.__name__,
|
||||
@ -107,6 +116,8 @@ class MonitoredTask(Task):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
||||
if not self._result.uid:
|
||||
self._result.uid = self._uid
|
||||
TaskInfo(
|
||||
task_name=self.__name__,
|
||||
task_description=self.__doc__,
|
34
passbook/lib/tasks/backup.py
Normal file
34
passbook/lib/tasks/backup.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""Database backup task"""
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
|
||||
from botocore.exceptions import BotoCoreError, ClientError
|
||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||
from django.core import management
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||
from passbook.root.celery import CELERY_APP
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
def backup_database(self: MonitoredTask): # pragma: no cover
|
||||
"""Database backup"""
|
||||
try:
|
||||
start = datetime.now()
|
||||
out = StringIO()
|
||||
management.call_command("dbbackup", quiet=True, stdout=out)
|
||||
self.set_status(
|
||||
TaskResult(
|
||||
TaskResultStatus.SUCCESSFUL,
|
||||
[
|
||||
f"Successfully finished database backup {naturaltime(start)}",
|
||||
out.getvalue(),
|
||||
],
|
||||
)
|
||||
)
|
||||
LOGGER.info("Successfully backed up database.")
|
||||
except (IOError, BotoCoreError, ClientError) as exc:
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
@ -21,9 +21,7 @@ class BaseController:
|
||||
|
||||
def __init__(self, outpost: Outpost):
|
||||
self.outpost = outpost
|
||||
self.logger = get_logger(
|
||||
controller=self.__class__.__name__, outpost=self.outpost
|
||||
)
|
||||
self.logger = get_logger()
|
||||
self.deployment_ports = {}
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@ -35,7 +33,7 @@ class BaseController:
|
||||
"""Call .up() but capture all log output and return it."""
|
||||
with capture_logs() as logs:
|
||||
self.up()
|
||||
return [f"{x['controller']}: {x['event']}" for x in logs]
|
||||
return [x["event"] for x in logs]
|
||||
|
||||
def down(self):
|
||||
"""Handler to delete everything we've created"""
|
||||
|
@ -35,9 +35,7 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||
def __init__(self, controller: "KubernetesController"):
|
||||
self.controller = controller
|
||||
self.namespace = controller.outpost.config.kubernetes_namespace
|
||||
self.logger = get_logger(
|
||||
controller=self.__class__.__name__, outpost=controller.outpost
|
||||
)
|
||||
self.logger = get_logger()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""Kubernetes Deployment Reconciler"""
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
|
||||
from kubernetes.client import (
|
||||
AppsV1Api,
|
||||
@ -41,7 +41,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return f"passbook-outpost-{self.outpost.name}"
|
||||
return f"passbook-outpost-{self.controller.outpost.uuid.hex}"
|
||||
|
||||
def reconcile(self, current: V1Deployment, reference: V1Deployment):
|
||||
if current.spec.replicas != reference.spec.replicas:
|
||||
@ -52,6 +52,14 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
||||
):
|
||||
raise NeedsUpdate()
|
||||
|
||||
def get_pod_meta(self) -> Dict[str, str]:
|
||||
"""Get common object metadata"""
|
||||
return {
|
||||
"app.kubernetes.io/name": "passbook-outpost",
|
||||
"app.kubernetes.io/managed-by": "passbook.beryju.org",
|
||||
"passbook.beryju.org/outpost-uuid": self.controller.outpost.uuid.hex,
|
||||
}
|
||||
|
||||
def get_reference_object(self) -> V1Deployment:
|
||||
"""Get deployment object for outpost"""
|
||||
# Generate V1ContainerPort objects
|
||||
@ -59,13 +67,14 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
||||
for port_name, port in self.controller.deployment_ports.items():
|
||||
container_ports.append(V1ContainerPort(container_port=port, name=port_name))
|
||||
meta = self.get_object_meta(name=self.name)
|
||||
secret_name = f"passbook-outpost-{self.controller.outpost.uuid.hex}-api"
|
||||
return V1Deployment(
|
||||
metadata=meta,
|
||||
spec=V1DeploymentSpec(
|
||||
replicas=self.outpost.config.kubernetes_replicas,
|
||||
selector=V1LabelSelector(match_labels=meta.labels),
|
||||
selector=V1LabelSelector(match_labels=self.get_pod_meta()),
|
||||
template=V1PodTemplateSpec(
|
||||
metadata=V1ObjectMeta(labels=meta.labels),
|
||||
metadata=V1ObjectMeta(labels=self.get_pod_meta()),
|
||||
spec=V1PodSpec(
|
||||
containers=[
|
||||
V1Container(
|
||||
@ -77,7 +86,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
||||
name="PASSBOOK_HOST",
|
||||
value_from=V1EnvVarSource(
|
||||
secret_key_ref=V1SecretKeySelector(
|
||||
name=f"passbook-outpost-{self.outpost.name}-api",
|
||||
name=secret_name,
|
||||
key="passbook_host",
|
||||
)
|
||||
),
|
||||
@ -86,7 +95,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
||||
name="PASSBOOK_TOKEN",
|
||||
value_from=V1EnvVarSource(
|
||||
secret_key_ref=V1SecretKeySelector(
|
||||
name=f"passbook-outpost-{self.outpost.name}-api",
|
||||
name=secret_name,
|
||||
key="token",
|
||||
)
|
||||
),
|
||||
@ -95,7 +104,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
||||
name="PASSBOOK_INSECURE",
|
||||
value_from=V1EnvVarSource(
|
||||
secret_key_ref=V1SecretKeySelector(
|
||||
name=f"passbook-outpost-{self.outpost.name}-api",
|
||||
name=secret_name,
|
||||
key="passbook_host_insecure",
|
||||
)
|
||||
),
|
||||
@ -117,9 +126,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
||||
)
|
||||
|
||||
def retrieve(self) -> V1Deployment:
|
||||
return self.api.read_namespaced_deployment(
|
||||
f"passbook-outpost-{self.outpost.name}", self.namespace
|
||||
)
|
||||
return self.api.read_namespaced_deployment(self.name, self.namespace)
|
||||
|
||||
def update(self, current: V1Deployment, reference: V1Deployment):
|
||||
return self.api.patch_namespaced_deployment(
|
||||
|
@ -27,7 +27,7 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return f"passbook-outpost-{self.controller.outpost.name}-api"
|
||||
return f"passbook-outpost-{self.controller.outpost.uuid.hex}-api"
|
||||
|
||||
def reconcile(self, current: V1Secret, reference: V1Secret):
|
||||
for key in reference.data.keys():
|
||||
@ -59,9 +59,7 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
|
||||
)
|
||||
|
||||
def retrieve(self) -> V1Secret:
|
||||
return self.api.read_namespaced_secret(
|
||||
f"passbook-outpost-{self.controller.outpost.name}-api", self.namespace
|
||||
)
|
||||
return self.api.read_namespaced_secret(self.name, self.namespace)
|
||||
|
||||
def update(self, current: V1Secret, reference: V1Secret):
|
||||
return self.api.patch_namespaced_secret(
|
||||
|
@ -7,6 +7,7 @@ from passbook.outposts.controllers.k8s.base import (
|
||||
KubernetesObjectReconciler,
|
||||
NeedsUpdate,
|
||||
)
|
||||
from passbook.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from passbook.outposts.controllers.kubernetes import KubernetesController
|
||||
@ -21,7 +22,7 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return f"passbook-outpost-{self.controller.outpost.name}"
|
||||
return f"passbook-outpost-{self.controller.outpost.uuid.hex}"
|
||||
|
||||
def reconcile(self, current: V1Service, reference: V1Service):
|
||||
if len(current.spec.ports) != len(reference.spec.ports):
|
||||
@ -36,9 +37,10 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
||||
ports = []
|
||||
for port_name, port in self.controller.deployment_ports.items():
|
||||
ports.append(V1ServicePort(name=port_name, port=port))
|
||||
selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
|
||||
return V1Service(
|
||||
metadata=meta,
|
||||
spec=V1ServiceSpec(ports=ports, selector=meta.labels, type="ClusterIP"),
|
||||
spec=V1ServiceSpec(ports=ports, selector=selector_labels, type="ClusterIP"),
|
||||
)
|
||||
|
||||
def create(self, reference: V1Service):
|
||||
@ -50,9 +52,7 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
||||
)
|
||||
|
||||
def retrieve(self) -> V1Service:
|
||||
return self.api.read_namespaced_service(
|
||||
f"passbook-outpost-{self.controller.outpost.name}", self.namespace
|
||||
)
|
||||
return self.api.read_namespaced_service(self.name, self.namespace)
|
||||
|
||||
def update(self, current: V1Service, reference: V1Service):
|
||||
return self.api.patch_namespaced_service(
|
||||
|
@ -5,6 +5,7 @@ from typing import Dict, List, Type
|
||||
from kubernetes.client import OpenApiException
|
||||
from kubernetes.config import load_incluster_config, load_kube_config
|
||||
from kubernetes.config.config_exception import ConfigException
|
||||
from structlog.testing import capture_logs
|
||||
from yaml import dump_all
|
||||
|
||||
from passbook.outposts.controllers.base import BaseController, ControllerException
|
||||
@ -43,6 +44,18 @@ class KubernetesController(BaseController):
|
||||
except OpenApiException as exc:
|
||||
raise ControllerException from exc
|
||||
|
||||
def up_with_logs(self) -> List[str]:
|
||||
try:
|
||||
all_logs = []
|
||||
for reconcile_key in self.reconcile_order:
|
||||
with capture_logs() as logs:
|
||||
reconciler = self.reconcilers[reconcile_key](self)
|
||||
reconciler.up()
|
||||
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
|
||||
return all_logs
|
||||
except OpenApiException as exc:
|
||||
raise ControllerException from exc
|
||||
|
||||
def down(self):
|
||||
try:
|
||||
for reconcile_key in self.reconcile_order:
|
||||
|
@ -204,7 +204,11 @@ class OutpostState:
|
||||
def for_channel(outpost: Outpost, channel: str) -> "OutpostState":
|
||||
"""Get state for a single channel"""
|
||||
key = f"{outpost.state_cache_prefix}_{channel}"
|
||||
data = cache.get(key, {"uid": channel})
|
||||
default_data = {"uid": channel}
|
||||
data = cache.get(key, default_data)
|
||||
if isinstance(data, str):
|
||||
cache.delete(key)
|
||||
data = default_data
|
||||
state = from_dict(OutpostState, data)
|
||||
state.uid = channel
|
||||
# pylint: disable=protected-access
|
||||
|
@ -35,9 +35,10 @@ def outpost_controller_all():
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
def outpost_controller(self: MonitoredTask, outpost_pk: str):
|
||||
"""Launch controller deployment of Outpost"""
|
||||
"""Create/update/monitor the deployment of an Outpost"""
|
||||
logs = []
|
||||
outpost: Outpost = Outpost.objects.get(pk=outpost_pk)
|
||||
self.set_uid(slugify(outpost.name))
|
||||
try:
|
||||
if outpost.type == OutpostType.PROXY:
|
||||
if outpost.deployment_type == OutpostDeploymentType.KUBERNETES:
|
||||
@ -45,15 +46,9 @@ def outpost_controller(self: MonitoredTask, outpost_pk: str):
|
||||
if outpost.deployment_type == OutpostDeploymentType.DOCKER:
|
||||
logs = ProxyDockerController(outpost).up_with_logs()
|
||||
except ControllerException as exc:
|
||||
self.set_status(
|
||||
TaskResult(TaskResultStatus.ERROR, uid=slugify(outpost.name)).with_error(
|
||||
exc
|
||||
)
|
||||
)
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
else:
|
||||
self.set_status(
|
||||
TaskResult(TaskResultStatus.SUCCESSFUL, logs, uid=slugify(outpost.name))
|
||||
)
|
||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, logs))
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
|
@ -1,9 +1,16 @@
|
||||
"""outpost tests"""
|
||||
from os import environ
|
||||
from unittest.case import skipUnless
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from guardian.models import UserObjectPermission
|
||||
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.outposts.controllers.k8s.base import NeedsUpdate
|
||||
from passbook.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||
from passbook.outposts.controllers.kubernetes import KubernetesController
|
||||
from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType
|
||||
from passbook.providers.proxy.models import ProxyProvider
|
||||
|
||||
@ -58,3 +65,50 @@ class OutpostTests(TestCase):
|
||||
permissions = UserObjectPermission.objects.filter(user=outpost.user)
|
||||
self.assertEqual(len(permissions), 1)
|
||||
self.assertEqual(permissions[0].object_pk, str(outpost.pk))
|
||||
|
||||
|
||||
@skipUnless("PB_TEST_K8S" in environ, "Kubernetes test cluster required")
|
||||
class OutpostKubernetesTests(TestCase):
|
||||
"""Test Kubernetes Controllers"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.provider: ProxyProvider = ProxyProvider.objects.create(
|
||||
name="test",
|
||||
internal_host="http://localhost",
|
||||
external_host="http://localhost",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
)
|
||||
self.outpost: Outpost = Outpost.objects.create(
|
||||
name="test",
|
||||
type=OutpostType.PROXY,
|
||||
deployment_type=OutpostDeploymentType.KUBERNETES,
|
||||
)
|
||||
self.outpost.providers.add(self.provider)
|
||||
self.outpost.save()
|
||||
|
||||
def test_deployment_reconciler(self):
|
||||
"""test that deployment requires update"""
|
||||
controller = KubernetesController(self.outpost)
|
||||
deployment_reconciler = DeploymentReconciler(controller)
|
||||
|
||||
self.assertIsNotNone(deployment_reconciler.retrieve())
|
||||
|
||||
config = self.outpost.config
|
||||
config.kubernetes_replicas = 3
|
||||
self.outpost.config = config
|
||||
|
||||
with self.assertRaises(NeedsUpdate):
|
||||
deployment_reconciler.reconcile(
|
||||
deployment_reconciler.retrieve(),
|
||||
deployment_reconciler.get_reference_object(),
|
||||
)
|
||||
|
||||
with patch.object(deployment_reconciler, "image_base", "test"):
|
||||
with self.assertRaises(NeedsUpdate):
|
||||
deployment_reconciler.reconcile(
|
||||
deployment_reconciler.retrieve(),
|
||||
deployment_reconciler.get_reference_object(),
|
||||
)
|
||||
|
||||
deployment_reconciler.delete(deployment_reconciler.get_reference_object())
|
||||
|
@ -34,7 +34,7 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return f"passbook-outpost-{self.controller.outpost.name}"
|
||||
return f"passbook-outpost-{self.controller.outpost.uuid.hex}"
|
||||
|
||||
def reconcile(
|
||||
self, current: NetworkingV1beta1Ingress, reference: NetworkingV1beta1Ingress
|
||||
@ -117,9 +117,7 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
|
||||
)
|
||||
|
||||
def retrieve(self) -> NetworkingV1beta1Ingress:
|
||||
return self.api.read_namespaced_ingress(
|
||||
f"passbook-outpost-{self.controller.outpost.name}", self.namespace
|
||||
)
|
||||
return self.api.read_namespaced_ingress(self.name, self.namespace)
|
||||
|
||||
def update(
|
||||
self, current: NetworkingV1beta1Ingress, reference: NetworkingV1beta1Ingress
|
||||
|
@ -269,9 +269,14 @@ CELERY_TASK_SOFT_TIME_LIMIT = 600
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"clean_expired_models": {
|
||||
"task": "passbook.core.tasks.clean_expired_models",
|
||||
"schedule": crontab(minute="*/5"), # Run every 5 minutes
|
||||
"schedule": crontab(minute="*/5"),
|
||||
"options": {"queue": "passbook_scheduled"},
|
||||
}
|
||||
},
|
||||
"db_backup": {
|
||||
"task": "passbook.lib.tasks.backup.backup_database",
|
||||
"schedule": crontab(minute=0, hour=0),
|
||||
"options": {"queue": "passbook_scheduled"},
|
||||
},
|
||||
}
|
||||
CELERY_TASK_CREATE_MISSING_QUEUES = True
|
||||
CELERY_TASK_DEFAULT_QUEUE = "passbook"
|
||||
@ -404,6 +409,7 @@ _LOGGING_HANDLER_MAP = {
|
||||
"websockets": "WARNING",
|
||||
"daphne": "WARNING",
|
||||
"dbbackup": "ERROR",
|
||||
"kubernetes": "INFO",
|
||||
}
|
||||
for handler_name, level in _LOGGING_HANDLER_MAP.items():
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
@ -444,6 +450,7 @@ for _app in INSTALLED_APPS:
|
||||
if DEBUG:
|
||||
INSTALLED_APPS.append("debug_toolbar")
|
||||
MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
|
||||
CELERY_TASK_ALWAYS_EAGER = True
|
||||
|
||||
INSTALLED_APPS.append("passbook.core.apps.PassbookCoreConfig")
|
||||
|
||||
|
@ -20,8 +20,9 @@ def ldap_sync_all():
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
def ldap_sync(self: MonitoredTask, source_pk: int):
|
||||
"""Sync a single source"""
|
||||
"""Synchronization of an LDAP Source"""
|
||||
source: LDAPSource = LDAPSource.objects.get(pk=source_pk)
|
||||
self.set_uid(slugify(source.name))
|
||||
try:
|
||||
syncer = LDAPSynchronizer(source)
|
||||
user_count = syncer.sync_users()
|
||||
@ -33,10 +34,7 @@ def ldap_sync(self: MonitoredTask, source_pk: int):
|
||||
TaskResult(
|
||||
TaskResultStatus.SUCCESSFUL,
|
||||
[f"Synced {user_count} users", f"Synced {group_count} groups"],
|
||||
uid=slugify(source.name),
|
||||
)
|
||||
)
|
||||
except LDAPException as exc:
|
||||
self.set_status(
|
||||
TaskResult(TaskResultStatus.ERROR, uid=slugify(source.name)).with_error(exc)
|
||||
)
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
|
@ -13,7 +13,7 @@ LOGGER = get_logger()
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
def clean_temporary_users(self: MonitoredTask):
|
||||
"""Remove old temporary users"""
|
||||
"""Remove temporary users created by SAML Sources"""
|
||||
_now = now()
|
||||
messages = []
|
||||
deleted_users = 0
|
||||
|
@ -37,6 +37,8 @@ def send_mails(stage: EmailStage, *messages: List[EmailMultiAlternatives]):
|
||||
def send_mail(self: MonitoredTask, email_stage_pk: int, message: Dict[Any, Any]):
|
||||
"""Send Email for Email Stage. Retries are scheduled automatically."""
|
||||
self.save_on_success = False
|
||||
message_id = make_msgid(domain=DNS_NAME)
|
||||
self.set_uid(message_id)
|
||||
try:
|
||||
stage: EmailStage = EmailStage.objects.get(pk=email_stage_pk)
|
||||
backend = stage.backend
|
||||
@ -48,7 +50,6 @@ def send_mail(self: MonitoredTask, email_stage_pk: int, message: Dict[Any, Any])
|
||||
setattr(message_object, key, value)
|
||||
message_object.from_email = stage.from_address
|
||||
# Because we use the Message-ID as UID for the task, manually assign it
|
||||
message_id = make_msgid(domain=DNS_NAME)
|
||||
message_object.extra_headers["Message-ID"] = message_id
|
||||
|
||||
LOGGER.debug("Sending mail", to=message_object.to)
|
||||
@ -57,7 +58,6 @@ def send_mail(self: MonitoredTask, email_stage_pk: int, message: Dict[Any, Any])
|
||||
TaskResult(
|
||||
TaskResultStatus.SUCCESSFUL,
|
||||
messages=["Successfully sent Mail."],
|
||||
uid=message_id,
|
||||
)
|
||||
)
|
||||
except (SMTPException, ConnectionError) as exc:
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BeryJu/passbook/proxy/pkg"
|
||||
"github.com/BeryJu/passbook/proxy/pkg/client"
|
||||
"github.com/BeryJu/passbook/proxy/pkg/client/outposts"
|
||||
"github.com/getsentry/sentry-go"
|
||||
@ -70,6 +71,7 @@ func doGlobalSetup(config map[string]interface{}) {
|
||||
default:
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
log.WithField("version", pkg.VERSION).Info("Starting passbook proxy")
|
||||
|
||||
var dsn string
|
||||
if config[ConfigErrorReportingEnabled].(bool) {
|
||||
|
@ -1,3 +1,3 @@
|
||||
package pkg
|
||||
|
||||
const VERSION = "0.12.1-stable"
|
||||
const VERSION = "0.12.3-stable"
|
||||
|
Reference in New Issue
Block a user