pyrelukko 0.1.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyrelukko might be problematic. Click here for more details.

Files changed (31) hide show
  1. pyrelukko-0.3.0/.gitlab-ci.yml +137 -0
  2. {pyrelukko-0.1.0 → pyrelukko-0.3.0}/PKG-INFO +2 -1
  3. pyrelukko-0.3.0/cicd/build-py-rf-image.sh +48 -0
  4. pyrelukko-0.3.0/cicd/run_docs_pages.sh +17 -0
  5. pyrelukko-0.3.0/cicd/run_pylint.sh +24 -0
  6. pyrelukko-0.3.0/cicd/run_pytest.sh +14 -0
  7. pyrelukko-0.3.0/cicd/run_shellcheck.sh +14 -0
  8. pyrelukko-0.3.0/container/Containerfile +5 -0
  9. pyrelukko-0.3.0/container/requirements.txt +3 -0
  10. pyrelukko-0.3.0/pyproject.toml +61 -0
  11. {pyrelukko-0.1.0 → pyrelukko-0.3.0}/src/pyrelukko/pyrelukko.py +47 -22
  12. pyrelukko-0.3.0/src/pyrelukko/testcontainers.py +90 -0
  13. {pyrelukko-0.1.0 → pyrelukko-0.3.0}/src/pyrelukko/version.py +1 -1
  14. pyrelukko-0.3.0/tests/cert/5d868fca.0 +1 -0
  15. pyrelukko-0.3.0/tests/cert/README.md +11 -0
  16. pyrelukko-0.3.0/tests/cert/relukko.crt +18 -0
  17. pyrelukko-0.3.0/tests/cert/relukko.csr +16 -0
  18. pyrelukko-0.3.0/tests/cert/relukko.key +28 -0
  19. pyrelukko-0.3.0/tests/cert/rootCA.crt +19 -0
  20. pyrelukko-0.3.0/tests/cert/rootCA.der +0 -0
  21. pyrelukko-0.3.0/tests/cert/rootCA.key +28 -0
  22. pyrelukko-0.3.0/tests/cert/rootCA.srl +1 -0
  23. pyrelukko-0.3.0/tests/conftest.py +70 -0
  24. pyrelukko-0.3.0/tests/initdb.d/20240930160154_init.up.sql +19 -0
  25. pyrelukko-0.3.0/tests/test_relukko.py +470 -0
  26. pyrelukko-0.1.0/.pylintrc.toml +0 -4
  27. pyrelukko-0.1.0/pyproject.toml +0 -31
  28. {pyrelukko-0.1.0 → pyrelukko-0.3.0}/LICENSE +0 -0
  29. {pyrelukko-0.1.0 → pyrelukko-0.3.0}/README.md +0 -0
  30. {pyrelukko-0.1.0 → pyrelukko-0.3.0}/src/pyrelukko/__init__.py +0 -0
  31. {pyrelukko-0.1.0 → pyrelukko-0.3.0}/src/pyrelukko/retry.py +0 -0
@@ -0,0 +1,137 @@
1
+ variables:
2
+ FF_TIMESTAMPS: true
3
+ BUILD_IMAGE:
4
+ description: Used in rules, if "true" and web triggered it builds the image!
5
+ value: "false"
6
+ options:
7
+ - "false"
8
+ - "true"
9
+ IMAGE_NAME: py-rf-image
10
+ IMAGE_TAG:
11
+ description: >-
12
+ The "tag" added to the image name for the test / lint image.
13
+ Remove this when you trigger by hand an image build and want it to use
14
+ the new built image with Git short sha tag (X icon on the right)!
15
+ value: "latest"
16
+ # RUN_DOCS_PUBLISH:
17
+ # description: >-
18
+ # Used in rules, if "true" and web triggered it generates the docs and
19
+ # publishes them!
20
+ # value: "false"
21
+ # options:
22
+ # - "false"
23
+ # - "true"
24
+
25
+ build-py-rf-image:
26
+ stage: build
27
+ image: docker:25
28
+ services:
29
+ - name: docker:25-dind
30
+ alias: docker
31
+ before_script:
32
+ - docker info
33
+ before_script:
34
+ - echo "Login to Gitlab container registry"
35
+ - echo "$CI_REGISTRY_PASSWORD" | docker login --username $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
36
+ script:
37
+ - echo "Build container image..."
38
+ - ./cicd/build-py-rf-image.sh
39
+ - echo "Export IMAGE_TAG var to next stage!"
40
+ - echo "IMAGE_TAG=${CI_COMMIT_SHORT_SHA}" > image_tag.env
41
+ - echo "Build complete."
42
+ artifacts:
43
+ reports:
44
+ dotenv: image_tag.env
45
+ rules:
46
+ - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
47
+ changes:
48
+ - .gitlab-ci.yml
49
+ - cicd/build-py-rf-image.sh
50
+ - container/**/*
51
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
52
+ changes:
53
+ - .gitlab-ci.yml
54
+ - cicd/build-py-rf-image.sh
55
+ - container/**/*
56
+ - if: $CI_PIPELINE_SOURCE == "web" && $BUILD_IMAGE == "true"
57
+ when: always
58
+
59
+ run-pylint:
60
+ stage: test
61
+ image: "${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:${IMAGE_TAG}"
62
+ rules:
63
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
64
+ changes:
65
+ - src/**/*.py
66
+ - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
67
+ changes:
68
+ - src/**/*.py
69
+ before_script:
70
+ - echo "Running PyLint."
71
+ script:
72
+ - ./cicd/run_pylint.sh
73
+ artifacts:
74
+ when: always
75
+ expire_in: 2 week
76
+ paths:
77
+ - pylint.out.*
78
+
79
+
80
+ run-pytest:
81
+ stage: test
82
+ image: "${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:${IMAGE_TAG}"
83
+ rules:
84
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
85
+ changes:
86
+ - src/**/*.py
87
+ - tests/test_*.py
88
+ - tests/conftest.py
89
+ - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
90
+ changes:
91
+ - src/**/*.py
92
+ - tests/test_*.py
93
+ - tests/conftest.py
94
+ before_script:
95
+ - echo "Running PyTest."
96
+ script:
97
+ - ./cicd/run_pytest.sh
98
+ artifacts:
99
+ when: always
100
+ expire_in: 2 week
101
+ paths:
102
+ - pytest-junit.xml
103
+ reports:
104
+ junit: pytest-junit.xml
105
+
106
+
107
+ run-shellcheck:
108
+ stage: test
109
+ image: "${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:${IMAGE_TAG}"
110
+ rules:
111
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
112
+ changes:
113
+ - cicd/*.sh
114
+ - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
115
+ changes:
116
+ - cicd/*.sh
117
+ before_script:
118
+ - echo "Running ShellCheck."
119
+ script:
120
+ - ./cicd/run_shellcheck.sh
121
+
122
+ # pages:
123
+ # stage: deploy
124
+ # image: "${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:${IMAGE_TAG}"
125
+ # rules:
126
+ # - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
127
+ # changes:
128
+ # - src/**/*.py
129
+ # - if: $CI_PIPELINE_SOURCE == "web" && $RUN_DOCS_PUBLISH == "true"
130
+ # when: always
131
+ # before_script:
132
+ # - echo "Running Docs generation and publishing..."
133
+ # script:
134
+ # - ./cicd/run_docs_pages.sh
135
+ # artifacts:
136
+ # paths:
137
+ # - public
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyrelukko
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: Relukko client.
5
5
  Author-email: Reto Zingg <g.d0b3rm4n@gmail.com>
6
6
  Requires-Python: >=3.12
@@ -11,6 +11,7 @@ Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Topic :: Internet :: WWW/HTTP
12
12
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
13
  Requires-Dist: requests >=2.32.3
14
+ Requires-Dist: websockets >= 13.1
14
15
  Project-URL: Homepage, https://gitlab.com/relukko/pyrelukko
15
16
  Project-URL: Issues, https://gitlab.com/relukko/pyrelukko/-/issues
16
17
 
@@ -0,0 +1,48 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+
5
+ SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
6
+ REPO_DIR="${SCRIPT_DIR}/.."
7
+ CONTAINER_DIR="${REPO_DIR}/container"
8
+ CONTAINER_CMD="docker"
9
+
10
+ # IMAGE_NAME should normally come from .gitlab-ci.yml file,
11
+ # set default for local usage.
12
+ if [ -z "${IMAGE_NAME+x}" ]; then
13
+ IMAGE_NAME=py-rf-image-local
14
+ fi
15
+
16
+ cd "${CONTAINER_DIR}" || exit
17
+
18
+ ${CONTAINER_CMD} build -t "${IMAGE_NAME}:latest" --file Containerfile .
19
+
20
+ ${CONTAINER_CMD} images
21
+
22
+ if [ -n "${CI_REGISTRY_IMAGE}" ]; then
23
+ # In Gitlab
24
+
25
+ # Tag with Git commit short sha and push
26
+ IMAGE_WITH_GIT_SHA="${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}"
27
+ echo "IMAGE_WITH_GIT_SHA: ${IMAGE_WITH_GIT_SHA}"
28
+
29
+ ${CONTAINER_CMD} tag "${IMAGE_NAME}:latest" "${IMAGE_WITH_GIT_SHA}"
30
+ ${CONTAINER_CMD} push "${IMAGE_WITH_GIT_SHA}"
31
+
32
+ if [ "${CI_COMMIT_BRANCH}" = "${CI_DEFAULT_BRANCH}" ]; then
33
+ # We run in default branch, also tag with latest
34
+ # Tag with "latest" (overwrites last 'latest' in registry) and push
35
+ IMAGE_WITH_LATEST="${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:latest"
36
+
37
+ echo "Tag image with as 'latest': ${IMAGE_WITH_LATEST}"
38
+
39
+ ${CONTAINER_CMD} tag "${IMAGE_NAME}:latest" "${IMAGE_WITH_LATEST}"
40
+ ${CONTAINER_CMD} push "${IMAGE_WITH_LATEST}"
41
+ fi
42
+ else
43
+ echo "Runs locally, no pushing!"
44
+ fi
45
+
46
+ cd -
47
+
48
+ # vim:set softtabstop=4 shiftwidth=4 tabstop=4 expandtab:
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+
3
+ SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
4
+ REPO_DIR="${SCRIPT_DIR}/.."
5
+ PUBLIC_DIR="${REPO_DIR}/public"
6
+
7
+ # Main script execution
8
+ main() {
9
+ mkdir -p "${PUBLIC_DIR}"
10
+ # libdoc --pythonpath ./src Relukko "${PUBLIC_DIR}/index.html"
11
+ echo "Not Implemented yet"
12
+ }
13
+
14
+ # Execute the main function
15
+ main
16
+
17
+ # vim:set softtabstop=4 shiftwidth=4 tabstop=4 expandtab:
@@ -0,0 +1,24 @@
1
+ #!/bin/bash
2
+
3
+ SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
4
+ REPO_DIR="${SCRIPT_DIR}/.."
5
+
6
+ # Main script execution
7
+ main() {
8
+ pylint --verbose \
9
+ --rcfile "${REPO_DIR}/pyproject.toml" \
10
+ --output-format=json:pylint.out.json,parseable:pylint.out.txt,text \
11
+ "${REPO_DIR}"
12
+ pylint_exit_code=$?
13
+
14
+ pylint-json2html \
15
+ -o "${REPO_DIR}/pylint.out.html" \
16
+ "${REPO_DIR}/pylint.out.json"
17
+
18
+ return ${pylint_exit_code}
19
+ }
20
+
21
+ # Execute the main function
22
+ main
23
+
24
+ # vim:set softtabstop=4 shiftwidth=4 tabstop=4 expandtab:
@@ -0,0 +1,14 @@
1
+ #!/bin/bash
2
+
3
+ SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
4
+ REPO_DIR="${SCRIPT_DIR}/.."
5
+
6
+ # Main script execution
7
+ main() {
8
+ pytest --junit-xml "${REPO_DIR}/pytest-junit.xml"
9
+ }
10
+
11
+ # Execute the main function
12
+ main
13
+
14
+ # vim:set softtabstop=4 shiftwidth=4 tabstop=4 expandtab:
@@ -0,0 +1,14 @@
1
+ #!/bin/bash
2
+
3
+ SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
4
+ REPO_DIR="${SCRIPT_DIR}/.."
5
+
6
+ # Main script execution
7
+ main() {
8
+ shellcheck "${REPO_DIR}"/cicd/*.sh
9
+ }
10
+
11
+ # Execute the main function
12
+ main
13
+
14
+ # vim:set softtabstop=4 shiftwidth=4 tabstop=4 expandtab:
@@ -0,0 +1,5 @@
1
+ FROM python:3.13-alpine
2
+
3
+ RUN apk add --no-cache bash shellcheck
4
+ COPY requirements.txt /opt/requirements.txt
5
+ RUN pip install --no-cache-dir -r /opt/requirements.txt
@@ -0,0 +1,3 @@
1
+ pylint==3.3.1
2
+ pylint-json2html==0.5.0
3
+ pytest==8.3.3
@@ -0,0 +1,61 @@
1
+ [project]
2
+ name = "pyrelukko"
3
+ authors = [
4
+ {name = "Reto Zingg", email = "g.d0b3rm4n@gmail.com"},
5
+ ]
6
+ readme = "README.md"
7
+
8
+ # https://pypi.org/pypi?%3Aaction=list_classifiers
9
+ classifiers = [
10
+ "Development Status :: 3 - Alpha",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Topic :: Internet :: WWW/HTTP",
14
+ "Topic :: Software Development :: Libraries :: Python Modules",
15
+ ]
16
+ requires-python = ">=3.12"
17
+ dynamic = ["version", "description"]
18
+ dependencies = [
19
+ "requests >=2.32.3",
20
+ "websockets >= 13.1",
21
+ ]
22
+
23
+ [tool.flit.sdist]
24
+ exclude = [".gitignore", "demo.py"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://gitlab.com/relukko/pyrelukko"
28
+ Issues = "https://gitlab.com/relukko/pyrelukko/-/issues"
29
+
30
+ [build-system]
31
+ requires = ["flit_core >=3.2,<4", "semantic-version >= 2.10"]
32
+ build-backend = "flit_core.buildapi"
33
+
34
+ [tool.pytest.ini_options]
35
+ testpaths = [
36
+ "tests",
37
+ ]
38
+ pythonpath = [
39
+ "src",
40
+ ]
41
+
42
+ [tool.tox]
43
+ requires = ["tox>=4.23"]
44
+ env_list = ["3.13", "3.12"]
45
+
46
+ [tool.tox.env_run_base]
47
+ description = "Run test under {base_python}"
48
+ commands = [["pytest"]]
49
+ deps = ["httpx", "pytest", "testcontainers"]
50
+ set_env = { VIRTUALENV_DISCOVERY = "pyenv" }
51
+ pass_env = [ "TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", "DOCKER_HOST" ]
52
+
53
+ [tool.pylint.main]
54
+ # Files or directories to be skipped. They should be base names, not paths.
55
+ ignore = ["LICENSE", "pyproject.toml", "version.py",
56
+ "README.md", "demo.py", "conftest.py", ".tox"]
57
+
58
+ # Files or directories matching the regular expression patterns are skipped. The
59
+ # regex matches against base names, not paths. The default value ignores Emacs
60
+ # file locks
61
+ ignore-patterns = ["^\\.#", "^test_"]
@@ -2,22 +2,20 @@
2
2
  TBD
3
3
  """
4
4
  import asyncio
5
+ import json
5
6
  import logging
6
7
  import os
7
- import json
8
- import threading
9
8
  import ssl
9
+ import threading
10
10
  import time
11
-
12
11
  from datetime import datetime
13
- from typing import Dict, List, Union
14
12
  from pathlib import Path
15
- from urllib3.util import Url, parse_url
16
- from urllib3.util.retry import Retry
13
+ from typing import Dict, List, Union
17
14
 
18
15
  import requests
16
+ from urllib3.util import Url, parse_url
17
+ from urllib3.util.retry import Retry
19
18
  from websockets import ConnectionClosed as WsConnectionClosed
20
-
21
19
  from websockets.asyncio.client import connect as ws_connect
22
20
 
23
21
  from .retry import retry
@@ -30,6 +28,14 @@ SSL_KWARGS = [
30
28
  'options',
31
29
  ]
32
30
 
31
+ RETRY_KWARGS = [
32
+ 'tries',
33
+ 'delay',
34
+ 'backoff',
35
+ 'max_delay',
36
+ 'exceptions'
37
+ ]
38
+
33
39
  logger = logging.getLogger(__name__)
34
40
 
35
41
 
@@ -50,12 +56,21 @@ class RelukkoClient:
50
56
  """
51
57
  self.session = requests.Session()
52
58
  self.api_key = api_key
59
+ self.tries=4
60
+ self.delay=5
61
+ self.backoff=2.0
62
+ self.max_delay=None
63
+ self.exceptions = (
64
+ requests.ConnectionError,
65
+ RelukkoDoRetry,
66
+ )
53
67
  self._setup_session(api_key, **kwargs)
54
68
  self._setup_http_adapters_retry(**kwargs)
69
+ self._setup_pyrelukko_retry(**kwargs)
55
70
 
56
71
  self.base_url = self._setup_base_url(base_url)
57
72
  self.ws_url = self._setup_ws_url(str(self.base_url))
58
- self.ssl_ctx = None
73
+ self.ssl_ctx: ssl.SSLContext = None
59
74
  self._setup_ssl_ctx(**kwargs)
60
75
 
61
76
  # event for websocket thread to signal it got a message
@@ -75,6 +90,15 @@ class RelukkoClient:
75
90
  self.base_url = self._setup_base_url(base_url or self.base_url)
76
91
  self.ws_url = self._setup_ws_url(str(self.base_url))
77
92
  self._setup_ssl_ctx(**kwargs)
93
+ self._setup_pyrelukko_retry(**kwargs)
94
+
95
+ def _setup_pyrelukko_retry(self, **kwargs):
96
+ for kwarg in RETRY_KWARGS:
97
+ setattr(
98
+ self,
99
+ kwarg,
100
+ kwargs.get(kwarg, getattr(self, kwarg))
101
+ )
78
102
 
79
103
  def _setup_http_adapters_retry(self, **kwargs):
80
104
  for _, http_adapter in self.session.adapters.items():
@@ -85,7 +109,7 @@ class RelukkoClient:
85
109
  http_adapter.max_retries = http_retry
86
110
 
87
111
  def _setup_session(self, api_key: str, **kwargs):
88
- self.session.headers = {'X-api-Key': api_key}
112
+ self.session.headers['X-api-Key'] = api_key
89
113
  for key, value in kwargs.items():
90
114
  if hasattr(self.session, key):
91
115
  setattr(self.session, key, value)
@@ -116,7 +140,7 @@ class RelukkoClient:
116
140
 
117
141
  # values from kwargs take precedence env vars
118
142
  ca_file = kwargs.get('cafile', ca_bundle_file)
119
- ca_path = kwargs.get('capath', ca_bundle_path)
143
+ ca_path = kwargs.get('capath', f"{ca_bundle_path}/")
120
144
  ca_data = kwargs.get('cadata')
121
145
 
122
146
  if ca_file or ca_path or ca_data:
@@ -127,7 +151,7 @@ class RelukkoClient:
127
151
 
128
152
  def _setup_ws_url(self, ws_url: str) -> Url:
129
153
  url = ws_url.replace("http", "ws", 1)
130
- return parse_url(f"{url}/deletions")
154
+ return parse_url(f"{url}/ws/broadcast")
131
155
 
132
156
  def _setup_base_url(self, base_url: Union[Url, str]) -> Url:
133
157
  if isinstance(base_url, str):
@@ -196,7 +220,7 @@ class RelukkoClient:
196
220
 
197
221
  def _check_response(self, response: requests.Response):
198
222
  match response.status_code:
199
- case 200 | 201 | 404:
223
+ case 200 | 201 | 404 | 422:
200
224
  return response.json()
201
225
  case 400 | 403:
202
226
  err = response.json()
@@ -207,10 +231,12 @@ class RelukkoClient:
207
231
  logger.info(err.get('status'), err.get('message'))
208
232
  return None
209
233
  case 500 | 502 | 503 | 504:
210
- logger.warning(response.status_code, response.text)
234
+ logger.warning("[%d](%s) %s",
235
+ response.status_code, response.reason, response.text)
211
236
  raise RelukkoDoRetry()
212
237
  case _:
213
- logger.warning(response.status_code, response.text)
238
+ logger.warning("[%d](%s) %s",
239
+ response.status_code, response.reason, response.text)
214
240
  raise RuntimeError()
215
241
 
216
242
  def _make_request(
@@ -219,12 +245,10 @@ class RelukkoClient:
219
245
  method: str,
220
246
  payload: Dict=None) -> requests.Response:
221
247
 
222
- excpetions = (
223
- requests.ConnectionError,
224
- RelukkoDoRetry,
225
- )
226
248
 
227
- @retry(logger, exceptions=excpetions, delay=10)
249
+ @retry(logger, exceptions=self.exceptions, tries=self.tries,
250
+ delay=self.delay, backoff=self.backoff,
251
+ max_delay=self.max_delay)
228
252
  def _do_request():
229
253
  response = self.session.request(
230
254
  method=method,
@@ -274,13 +298,14 @@ class RelukkoClient:
274
298
  url = f"{self.base_url}/v1/locks/"
275
299
  return self._make_request(url, "GET")
276
300
 
277
- def update_relukko(self, lock_id: str, creator: str, expires_at: datetime):
301
+ def update_relukko(
302
+ self, lock_id: str, creator: str=None, expires_at: datetime=None):
278
303
  """
279
304
  TBD
280
305
  """
281
306
  if isinstance(expires_at, datetime):
282
307
  expires_at = expires_at.isoformat()
283
- else:
308
+ elif expires_at is not None:
284
309
  raise ValueError("has to be datetime!")
285
310
 
286
311
  payload = {
@@ -288,7 +313,7 @@ class RelukkoClient:
288
313
  "expires_at": expires_at,
289
314
  }
290
315
  url = f"{self.base_url}/v1/locks/{lock_id}"
291
- return self._make_request(url, "POST", payload)
316
+ return self._make_request(url, "PUT", payload)
292
317
 
293
318
  def delete_relukko(self, lock_id: str):
294
319
  """
@@ -0,0 +1,90 @@
1
+ # pylint: skip-file
2
+ """
3
+ Testcontainers to be used in PyTests, for a fixture see tests/conftest.py
4
+ (relukko_backend):
5
+
6
+ @pytest.fixture(scope="session")
7
+ def relukko_backend():
8
+ with Network() as rl_net:
9
+ with RelukkoDbContainer(net=rl_net, initdb_dir=INITDB_DIR,
10
+ image="postgres:16", hostname="relukkodb") as _db:
11
+ db_url = "postgresql://relukko:relukko@relukkodb/relukko"
12
+ with RelukkoContainer(rl_net, db_url=db_url) as backend:
13
+ relukko = RelukkoClient(
14
+ base_url=backend.get_api_url(), api_key="somekey")
15
+ yield relukko, backend
16
+ """
17
+ import socket
18
+ from pathlib import Path
19
+
20
+ from testcontainers.core.network import Network
21
+ from testcontainers.core.waiting_utils import wait_container_is_ready
22
+ from testcontainers.generic import ServerContainer
23
+ from testcontainers.postgres import PostgresContainer
24
+
25
+
26
+ class RelukkoContainer(ServerContainer):
27
+ def __init__(self, net: Network,
28
+ image="registry.gitlab.com/relukko/relukko:0.10.0", db_url=None):
29
+ self.db_url = db_url
30
+ self.net = net
31
+ super(RelukkoContainer, self).__init__(image=image, port=3000)
32
+
33
+ def _configure(self):
34
+ self.with_env("DATABASE_URL", self.db_url)
35
+ self.with_env("RELUKKO_API_KEY", "somekey")
36
+ self.with_env("RELUKKO_USER", "relukko")
37
+ self.with_env("RELUKKO_PASSWORD", "relukko")
38
+ self.with_env("RELUKKO_BIND_ADDR", "0.0.0.0")
39
+ self.with_network(self.net)
40
+
41
+ def get_api_url(self) -> str:
42
+ return f"http://localhost:{self.get_exposed_port(3000)}"
43
+
44
+ def _create_connection_url(self) -> str:
45
+ return f"{self.get_api_url()}/healthchecker"
46
+
47
+
48
+ class RelukkoDbContainer(PostgresContainer):
49
+ def __init__(
50
+ self, net: Network,
51
+ initdb_dir: Path,
52
+ image: str = "postgres:latest",
53
+ port: int = 5432,
54
+ username: str | None = None,
55
+ password: str | None = None,
56
+ dbname: str | None = None,
57
+ driver: str | None = "psycopg2",
58
+ **kwargs) -> None:
59
+ self.net = net
60
+ self.initdb_dir = initdb_dir
61
+ super().__init__(image, port, username, password, dbname, driver, **kwargs)
62
+
63
+ def _configure(self) -> None:
64
+ self.with_volume_mapping(self.initdb_dir, "/docker-entrypoint-initdb.d", "Z")
65
+ self.with_env("POSTGRES_USER", "relukko")
66
+ self.with_env("POSTGRES_PASSWORD", "relukko")
67
+ self.with_env("POSTGRES_DB", "relukko")
68
+ self.with_network(self.net)
69
+
70
+ @wait_container_is_ready()
71
+ def _connect(self) -> None:
72
+ packet = bytes([
73
+ 0x00, 0x00, 0x00, 0x52, 0x00, 0x03, 0x00, 0x00,
74
+ 0x75, 0x73, 0x65, 0x72, 0x00, 0x72, 0x65, 0x6c,
75
+ 0x75, 0x6b, 0x6b, 0x6f, 0x00, 0x64, 0x61, 0x74,
76
+ 0x61, 0x62, 0x61, 0x73, 0x65, 0x00, 0x72, 0x65,
77
+ 0x6c, 0x75, 0x6b, 0x6b, 0x6f, 0x00, 0x61, 0x70,
78
+ 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
79
+ 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x00, 0x70,
80
+ 0x73, 0x71, 0x6c, 0x00, 0x63, 0x6c, 0x69, 0x65,
81
+ 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64,
82
+ 0x69, 0x6e, 0x67, 0x00, 0x55, 0x54, 0x46, 0x38,
83
+ 0x00, 0x00
84
+ ])
85
+ port = self.get_exposed_port(self.port)
86
+ with socket.create_connection(("localhost", port)) as sock:
87
+ sock.send(packet)
88
+ buf = sock.recv(40)
89
+ if len(buf) == 0 and "SCRAM-SHA" not in buf:
90
+ raise ConnectionError
@@ -1,2 +1,2 @@
1
1
  # pylint: disable=all
2
- __version__ = "0.1.0"
2
+ __version__ = "0.3.0"
@@ -0,0 +1 @@
1
+ rootCA.crt
@@ -0,0 +1,11 @@
1
+ # How To
2
+ ```
3
+ openssl req -subj '/CN=Relukko CA' -x509 -sha256 -days 7300 -noenc -newkey rsa:2048 -keyout rootCA.key -out rootCA.crt -addext keyUsage=critical,cRLSign,digitalSignature,keyCertSign
4
+
5
+ ln -s rootCA.crt "$(openssl x509 -hash -noout -in rootCA.crt).0"
6
+
7
+ openssl req -subj '/CN=relukko' -newkey rsa:2048 -noenc -keyout relukko.key -out relukko.csr -addext keyUsage=critical,digitalSignature,keyEncipherment,keyAgreement -addext extendedKeyUsage=critical,serverAuth
8
+
9
+ openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in relukko.csr -out relukko.crt -days 7299 -CAcreateserial
10
+ openssl x509 -inform PEM -in rootCA.crt -outform DER -out rootCA.der
11
+ ```
@@ -0,0 +1,18 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIC9zCCAd+gAwIBAgIUXFQN+AOO7KFdiVNVFman8BLkRfkwDQYJKoZIhvcNAQEL
3
+ BQAwFTETMBEGA1UEAwwKUmVsdWtrbyBDQTAeFw0yNDExMTYyMTM5MTNaFw00NDEx
4
+ MTAyMTM5MTNaMBIxEDAOBgNVBAMMB3JlbHVra28wggEiMA0GCSqGSIb3DQEBAQUA
5
+ A4IBDwAwggEKAoIBAQCljry3blOWFJRdinzcH4XzRnvKlhhofBhtRsPH+pvjmrHO
6
+ v52rXj1XeHqXaEBevhNiN7lP5eg2UgOrs0KAVXYR2Na9Mxrh8ACX+IqeEJF9BKgO
7
+ 4t2RvItDU3SWxV3dIYMasBhe/iDXDKKZd/jVOiluL9zuxo9ltDagZxc72jWMnvrO
8
+ lvlnZVEtH4n9i+hnx1daWRZG87DjO6d8I8F8nX6NTqJgBlrRm8jlcq2N/G8eHTFq
9
+ OrLY4xk4CF+aVExpJrEGprfhNkbE025tJIbGNM9Xv+W0sI/nhEwJdv4ifEP/b+44
10
+ n+KHrAurOboFYmRcoZ9Vn4C1KuYBvDOpgkwOs1wtAgMBAAGjQjBAMB0GA1UdDgQW
11
+ BBSy6DZz/N1PW1mkQY7HUj3Fz6yZOjAfBgNVHSMEGDAWgBT+sHazfmyczKb0BV6r
12
+ Z1JQ8Sfx9zANBgkqhkiG9w0BAQsFAAOCAQEAL1c1l3Azna1mh86haErurYk3EsiH
13
+ KDGprRubdB5qfpa2GdAkhitp9juKYdKGdw877eOl5z7AMeMWAA8WXQ3z7jkQkUhU
14
+ 9wsZxNyHQajq6Lo9KsRxZiRKZX/fxagcnDem+4xTvzSlJoAT5p/CO5E31odwWkZN
15
+ bg79I36nk87opISXzNAbMBcbQEV5R8l4PzNGs/DcrLzTC66FKxHHzyih4mlBF5mS
16
+ /sMY5l9QNaJHKPK7fKZ5yxohkda7tDwtb5p0bbEUQlMeZIQ8r/go1b7/ppXXM5ei
17
+ mTDcwKgeOBgVujqhvgGXOKtcHVfkrqcID+qgDMxYd7XXE/icGF3zmnHwDQ==
18
+ -----END CERTIFICATE-----
@@ -0,0 +1,16 @@
1
+ -----BEGIN CERTIFICATE REQUEST-----
2
+ MIICkDCCAXgCAQAwEjEQMA4GA1UEAwwHcmVsdWtrbzCCASIwDQYJKoZIhvcNAQEB
3
+ BQADggEPADCCAQoCggEBAKWOvLduU5YUlF2KfNwfhfNGe8qWGGh8GG1Gw8f6m+Oa
4
+ sc6/natePVd4epdoQF6+E2I3uU/l6DZSA6uzQoBVdhHY1r0zGuHwAJf4ip4QkX0E
5
+ qA7i3ZG8i0NTdJbFXd0hgxqwGF7+INcMopl3+NU6KW4v3O7Gj2W0NqBnFzvaNYye
6
+ +s6W+WdlUS0fif2L6GfHV1pZFkbzsOM7p3wjwXydfo1OomAGWtGbyOVyrY38bx4d
7
+ MWo6stjjGTgIX5pUTGkmsQamt+E2RsTTbm0khsY0z1e/5bSwj+eETAl2/iJ8Q/9v
8
+ 7jif4oesC6s5ugViZFyhn1WfgLUq5gG8M6mCTA6zXC0CAwEAAaA5MDcGCSqGSIb3
9
+ DQEJDjEqMCgwDgYDVR0PAQH/BAQDAgOoMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMB
10
+ MA0GCSqGSIb3DQEBCwUAA4IBAQBfEyjmGQtITqSsmoAQXEb11Cm/olsL5w6kEzgr
11
+ /0K8ASvDvW+3yJ3U9OBngvgh6iQCJjYaixb8NtQS7yVWYEqJoaA0Hp1q0QDmRS/l
12
+ MDLe2cILgbLQUKOf1jLtYsLG93Lb74HSGztjIIEoi3Wgrm5LLGc/cd28q8yw5c/j
13
+ s1EntUHeNWCYAUIRvwmt+1UPcc8EBHH2DugGz2verzARdvLWAajL5r9ZIJkkU0kY
14
+ fh5gnmVb6I6RtIXhWcXMUL9nBwzPCyJMd4cC/fueC+HxzLNhGldvVxj3cg7W57vR
15
+ QnbG/qcJYN5WGhoHrWIiI90GufG4jifVvyQeBC/vQcymRlAL
16
+ -----END CERTIFICATE REQUEST-----