Compare commits

...

20 Commits

Author SHA1 Message Date
a4cc653757 new release: 0.12.3-stable 2020-10-20 10:24:45 +02:00
db4ff20906 outposts: fix service using incorrect pod selector 2020-10-20 10:18:05 +02:00
1f0fbd33b6 build(deps): bump urllib3 from 1.25.10 to 1.25.11 (#287)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.10 to 1.25.11.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/master/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.25.10...1.25.11)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-20 10:17:46 +02:00
5de8d2721e build(deps): bump uvicorn from 0.12.1 to 0.12.2 (#286)
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.12.1 to 0.12.2.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.12.1...0.12.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-20 10:09:37 +02:00
0d65da9a9e build(deps): bump boto3 from 1.15.18 to 1.16.0 (#288)
Bumps [boto3](https://github.com/boto/boto3) from 1.15.18 to 1.16.0.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.15.18...1.16.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-10-20 09:34:55 +02:00
4316ee4330 root: implement db backups with monitored task, update docs 2020-10-19 22:17:47 +02:00
2ed9a1dbe3 */tasks: update phrasing 2020-10-19 21:35:31 +02:00
8e03824d20 lib: always set task's UID, even for unexpected errors 2020-10-19 21:30:21 +02:00
754dbdd0e5 outpost: fix logs for kubernetes controller 2020-10-19 21:29:58 +02:00
e13d348315 new release: 0.12.2-stable 2020-10-19 19:36:36 +02:00
169f3ebe5b outposts: fix logger again 2020-10-19 18:52:17 +02:00
f8ad604e85 outposts: add more tests 2020-10-19 17:47:51 +02:00
774b9c8a61 outposts: update kubernetes controller to use pk as identifier instead of name 2020-10-19 17:39:12 +02:00
d8c522233e outposts: fix outpost mangling log output 2020-10-19 16:54:11 +02:00
82d50f7eaa outposts: fix list showing questionmark when only one outpost is registered 2020-10-19 16:34:16 +02:00
1c426c5136 outposts: trigger deployment re-create when selector changes 2020-10-19 16:21:39 +02:00
d6e14cc551 proxy: show version on startup 2020-10-19 16:21:13 +02:00
c3917ebc2e lifecycle: fix formatting 2020-10-19 16:13:45 +02:00
7203bd37a3 outposts: replace migration with string backup handler 2020-10-19 16:04:38 +02:00
597188c7ee lifecycle: fix migration trying to load all classes 2020-10-19 15:55:16 +02:00
34 changed files with 217 additions and 176 deletions

View File

@ -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>.*)

View File

@ -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
View File

@ -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": [

View File

@ -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:

View File

@ -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:

View File

@ -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: ""

View File

@ -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.

View File

@ -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"))

View File

@ -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

View File

@ -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}}

View File

@ -4,7 +4,7 @@
image:
name: beryju/passbook
name_static: beryju/passbook-static
tag: 0.12.1-stable
tag: 0.12.3-stable
nameOverride: ""

View File

@ -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)

View File

@ -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'"

View File

@ -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)

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = "0.12.1-stable"
__version__ = "0.12.3-stable"

View File

@ -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>

View File

@ -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__,

View 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))

View File

@ -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"""

View File

@ -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:

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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())

View File

@ -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

View File

@ -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")

View File

@ -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))

View File

@ -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

View File

@ -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:

View File

@ -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) {

View File

@ -1,3 +1,3 @@
package pkg
const VERSION = "0.12.1-stable"
const VERSION = "0.12.3-stable"